From 44e68c301a66443d6af95a832792a9d25f873b0b Mon Sep 17 00:00:00 2001 From: Jake Potrebic Date: Fri, 26 Apr 2024 11:39:20 -0700 Subject: fix some more issues --- patches/api/0153-Add-Material-Tags.patch | 4 +- patches/api/0254-Add-missing-effects.patch | 26 +- .../0420-Add-methods-to-get-translation-keys.patch | 26 +- ...56-Add-missing-default-perms-for-commands.patch | 9 +- patches/server/0520-Item-Rarity-API.patch | 4 +- patches/server/0552-More-Enchantment-API.patch | 40 - patches/server/0885-Bandaid-fix-for-Effect.patch | 34 +- patches/server/0932-Add-MaterialTagsTest.patch | 58 - ...ightningEffect-powers-lightning-rods-and-.patch | 72 + ...to-fish-event-for-all-player-interactions.patch | 75 + ...ightningEffect-powers-lightning-rods-and-.patch | 72 - ...to-fish-event-for-all-player-interactions.patch | 75 - ...-Fix-several-issues-with-EntityBreedEvent.patch | 118 + .../0935-Add-UUID-attribute-modifier-API.patch | 56 + ...-Fix-several-issues-with-EntityBreedEvent.patch | 118 - .../0936-Add-UUID-attribute-modifier-API.patch | 56 - ...issing-event-call-for-entity-teleport-API.patch | 28 + ...issing-event-call-for-entity-teleport-API.patch | 28 - ...-Lazily-create-LootContext-for-criterions.patch | 30 + ...38-Don-t-fire-sync-events-during-worldgen.patch | 208 + ...-Lazily-create-LootContext-for-criterions.patch | 30 - patches/server/0939-Add-Structure-check-API.patch | 31 + ...39-Don-t-fire-sync-events-during-worldgen.patch | 208 - patches/server/0940-Add-Structure-check-API.patch | 31 - ...taItem-getAttributeModifier-duplication-c.patch | 19 + ...taItem-getAttributeModifier-duplication-c.patch | 19 - ...941-Restore-vanilla-entity-drops-behavior.patch | 243 + .../0942-Dont-resend-blocks-on-interactions.patch | 171 + ...942-Restore-vanilla-entity-drops-behavior.patch | 243 - .../0943-Dont-resend-blocks-on-interactions.patch | 171 - patches/server/0943-add-more-scoreboard-API.patch | 79 + patches/server/0944-Improve-Registry.patch | 102 + patches/server/0944-add-more-scoreboard-API.patch | 79 - ...x-NPE-on-null-loc-for-EntityTeleportEvent.patch | 66 + patches/server/0945-Improve-Registry.patch | 102 - .../server/0946-Add-experience-points-API.patch | 73 + ...x-NPE-on-null-loc-for-EntityTeleportEvent.patch | 66 - .../server/0947-Add-drops-to-shear-events.patch | 364 + .../server/0947-Add-experience-points-API.patch | 73 - .../server/0948-Add-PlayerShieldDisableEvent.patch | 53 + .../server/0948-Add-drops-to-shear-events.patch | 326 - .../server/0949-Add-PlayerShieldDisableEvent.patch | 53 - ...-Validate-ResourceLocation-in-NBT-reading.patch | 143 + ...handle-experience-dropping-on-block-break.patch | 94 + ...-Validate-ResourceLocation-in-NBT-reading.patch | 143 - .../server/0951-Fixup-NamespacedKey-handling.patch | 125 + ...handle-experience-dropping-on-block-break.patch | 94 - .../0952-Expose-LootTable-of-DecoratedPot.patch | 50 + .../server/0952-Fixup-NamespacedKey-handling.patch | 125 - .../0953-Expose-LootTable-of-DecoratedPot.patch | 50 - ...uce-allocation-of-Vec3D-by-entity-tracker.patch | 59 + ...-PlayerTradeEvent-and-PlayerPurchaseEvent.patch | 240 + ...uce-allocation-of-Vec3D-by-entity-tracker.patch | 59 - ...-PlayerTradeEvent-and-PlayerPurchaseEvent.patch | 240 - .../server/0955-Add-ShulkerDuplicateEvent.patch | 22 + .../server/0956-Add-ShulkerDuplicateEvent.patch | 22 - ...0956-Add-api-for-spawn-egg-texture-colors.patch | 26 + .../server/0957-Add-Lifecycle-Event-system.patch | 781 + ...0957-Add-api-for-spawn-egg-texture-colors.patch | 26 - .../server/0958-Add-Lifecycle-Event-system.patch | 781 - patches/server/0958-ItemStack-Tooltip-API.patch | 31 + ...tChunkSnapshot-includeLightData-parameter.patch | 70 + patches/server/0959-ItemStack-Tooltip-API.patch | 31 - patches/server/0960-Add-FluidState-API.patch | 208 + ...tChunkSnapshot-includeLightData-parameter.patch | 70 - patches/server/0961-Add-FluidState-API.patch | 208 - patches/server/0961-add-number-format-api.patch | 138 + patches/server/0962-add-number-format-api.patch | 138 - patches/server/0962-improve-BanList-types.patch | 32 + patches/server/0963-Expanded-Hopper-API.patch | 31 + patches/server/0963-improve-BanList-types.patch | 32 - .../0964-Add-BlockBreakProgressUpdateEvent.patch | 28 + patches/server/0964-Expanded-Hopper-API.patch | 31 - .../0965-Add-BlockBreakProgressUpdateEvent.patch | 28 - .../server/0965-Deprecate-ItemStack-setType.patch | 35 + patches/server/0966-Add-CartographyItemEvent.patch | 43 + .../server/0966-Deprecate-ItemStack-setType.patch | 35 - patches/server/0967-Add-CartographyItemEvent.patch | 43 - patches/server/0967-More-Raid-API.patch | 106 + ...boarding-message-for-initial-server-start.patch | 103 + patches/server/0968-More-Raid-API.patch | 106 - ...boarding-message-for-initial-server-start.patch | 103 - .../0969-Configurable-max-block-fluid-ticks.patch | 22 + .../0970-Configurable-max-block-fluid-ticks.patch | 22 - .../server/0970-Fix-bees-aging-inside-hives.patch | 41 + .../0971-Disable-memory-reserve-allocating.patch | 19 + .../server/0971-Fix-bees-aging-inside-hives.patch | 41 - .../0972-Disable-memory-reserve-allocating.patch | 19 - ...DamageByEntityEvent-for-unowned-wither-sk.patch | 19 + ...DamageByEntityEvent-for-unowned-wither-sk.patch | 19 - patches/server/0973-Fix-DamageSource-API.patch | 204 + patches/server/0974-Fix-DamageSource-API.patch | 204 - ...n-of-invalid-block-entity-during-world-ge.patch | 40 + ...n-of-invalid-block-entity-during-world-ge.patch | 40 - ...ble-StackOverflowError-for-some-dispenses.patch | 108 + ...ble-StackOverflowError-for-some-dispenses.patch | 108 - .../server/0976-Improve-tag-parser-handling.patch | 202 + .../server/0977-Improve-tag-parser-handling.patch | 202 - patches/server/0977-Item-Mutation-Fixes.patch | 38 + patches/server/0978-Item-Mutation-Fixes.patch | 38 - .../0978-Per-world-ticks-per-spawn-settings.patch | 35 + .../0979-Per-world-ticks-per-spawn-settings.patch | 35 - ...ack-the-changed-item-from-dispense-events.patch | 103 + ...ack-the-changed-item-from-dispense-events.patch | 103 - .../server/0980-Rewrite-dataconverter-system.patch | 28922 +++++++++++++++++++ .../server/0981-Rewrite-dataconverter-system.patch | 28922 ------------------- .../server/0981-Suspicious-Effect-Entry-API.patch | 222 + .../server/0982-Suspicious-Effect-Entry-API.patch | 222 - ...982-check-if-itemstack-is-stackable-first.patch | 19 + ...983-check-if-itemstack-is-stackable-first.patch | 19 - .../0983-disable-forced-empty-world-ticks.patch | 19 + patches/server/0984-Starlight.patch | 5429 ++++ .../0984-disable-forced-empty-world-ticks.patch | 19 - patches/server/0985-Rewrite-chunk-system.patch | 21905 ++++++++++++++ patches/server/0985-Starlight.patch | 5429 ---- patches/server/0986-Rewrite-chunk-system.patch | 21905 -------------- .../0986-incremental-chunk-and-player-saving.patch | 167 + ...InWorldBounds-and-getBlockState-for-inlin.patch | 99 + .../0987-incremental-chunk-and-player-saving.patch | 167 - ...s-in-item-frames-performance-and-bug-fixe.patch | 128 + ...InWorldBounds-and-getBlockState-for-inlin.patch | 99 - ...s-in-item-frames-performance-and-bug-fixe.patch | 128 - ...aytracing-for-EntityLiving-hasLineOfSight.patch | 158 + ...twork-Manager-and-add-advanced-packet-sup.patch | 395 + ...aytracing-for-EntityLiving-hasLineOfSight.patch | 158 - .../0991-Allow-Saving-of-Oversized-Chunks.patch | 204 + ...twork-Manager-and-add-advanced-packet-sup.patch | 395 - .../0992-Allow-Saving-of-Oversized-Chunks.patch | 204 - .../0992-Fix-World-isChunkGenerated-calls.patch | 243 + .../0993-Fix-World-isChunkGenerated-calls.patch | 243 - .../0993-Flat-bedrock-generator-settings.patch | 292 + .../server/0994-Entity-Activation-Range-2.0.patch | 812 + .../0994-Flat-bedrock-generator-settings.patch | 292 - .../server/0995-Entity-Activation-Range-2.0.patch | 812 - .../0995-Optional-per-player-mob-spawns.patch | 255 + patches/server/0996-Anti-Xray.patch | 1638 ++ .../0996-Optional-per-player-mob-spawns.patch | 255 - patches/server/0997-Anti-Xray.patch | 1638 -- ...celling-PreCreatureSpawnEvent-with-per-pl.patch | 89 + ...celling-PreCreatureSpawnEvent-with-per-pl.patch | 89 - ...998-Optimize-Collision-to-not-load-chunks.patch | 109 + ...999-Optimize-Collision-to-not-load-chunks.patch | 109 - ...ize-GoalSelector-Goal.Flag-Set-operations.patch | 160 + .../1000-Entity-load-save-limit-per-chunk.patch | 58 + ...ize-GoalSelector-Goal.Flag-Set-operations.patch | 160 - .../1001-Entity-load-save-limit-per-chunk.patch | 58 - .../server/1001-Improved-Watchdog-Support.patch | 544 + .../server/1002-Improved-Watchdog-Support.patch | 544 - .../server/1002-Optimize-Voxel-Shape-Merging.patch | 123 + .../server/1003-Optimize-Voxel-Shape-Merging.patch | 123 - patches/server/1003-Write-SavedData-IO-async.patch | 173 + ...rock-and-End-Portal-Frames-from-being-des.patch | 169 + patches/server/1004-Write-SavedData-IO-async.patch | 173 - ...rock-and-End-Portal-Frames-from-being-des.patch | 169 - ...e-distance-map-to-optimise-entity-tracker.patch | 341 + .../1006-Optimize-Bit-Operations-by-inlining.patch | 213 + ...e-distance-map-to-optimise-entity-tracker.patch | 341 - .../1007-Optimize-Bit-Operations-by-inlining.patch | 213 - .../server/1007-Remove-streams-from-hot-code.patch | 215 + .../1008-Eigencraft-redstone-implementation.patch | 1145 + .../server/1008-Remove-streams-from-hot-code.patch | 215 - .../1009-Eigencraft-redstone-implementation.patch | 1145 - ...thfinder-Remove-Streams-Optimized-collect.patch | 133 + .../1010-Improve-boat-collision-performance.patch | 68 + ...thfinder-Remove-Streams-Optimized-collect.patch | 133 - .../1011-Improve-boat-collision-performance.patch | 68 - .../server/1011-Optimise-general-POI-access.patch | 1060 + ...e-implementation-for-blockstate-state-loo.patch | 343 + .../server/1012-Optimise-general-POI-access.patch | 1060 - ...e-implementation-for-blockstate-state-loo.patch | 343 - .../server/1013-Execute-chunk-tasks-mid-tick.patch | 176 + .../server/1014-Execute-chunk-tasks-mid-tick.patch | 176 - .../1014-Optimise-random-block-ticking.patch | 456 + ...recalculate-regionfile-header-if-it-is-co.patch | 777 + .../1015-Optimise-random-block-ticking.patch | 456 - ...recalculate-regionfile-header-if-it-is-co.patch | 777 - ...e-Velocity-compression-and-cipher-natives.patch | 375 + ...Detail-more-information-in-watchdog-dumps.patch | 296 + ...e-Velocity-compression-and-cipher-natives.patch | 375 - patches/server/1018-Collision-optimisations.patch | 4726 +++ ...Detail-more-information-in-watchdog-dumps.patch | 296 - patches/server/1019-Collision-optimisations.patch | 4726 --- ...llision-checking-in-player-move-packet-ha.patch | 170 + ...ripwire-disarming-not-working-as-intended.patch | 22 + ...llision-checking-in-player-move-packet-ha.patch | 170 - ...entity-type-tags-suggestions-in-selectors.patch | 156 + ...ripwire-disarming-not-working-as-intended.patch | 22 - ...Alternate-Current-redstone-implementation.patch | 2114 ++ ...entity-type-tags-suggestions-in-selectors.patch | 156 - ...Alternate-Current-redstone-implementation.patch | 2114 -- .../1023-optimize-dirt-and-snow-spreading.patch | 78 + patches/server/1024-Properly-resend-entities.patch | 232 + .../1024-optimize-dirt-and-snow-spreading.patch | 78 - patches/server/1025-Optimize-Hoppers.patch | 637 + patches/server/1025-Properly-resend-entities.patch | 232 - .../1026-Improve-performance-of-mass-crafts.patch | 74 + patches/server/1026-Optimize-Hoppers.patch | 637 - .../server/1027-Actually-optimise-explosions.patch | 521 + .../1027-Improve-performance-of-mass-crafts.patch | 74 - .../server/1028-Actually-optimise-explosions.patch | 521 - .../1028-Optimise-chunk-tick-iteration.patch | 380 + patches/server/1029-Lag-compensation-ticks.patch | 129 + .../1029-Optimise-chunk-tick-iteration.patch | 380 - patches/server/1030-Lag-compensation-ticks.patch | 129 - .../1030-Optimise-nearby-player-retrieval.patch | 228 + .../1031-Distance-manager-tick-timings.patch | 34 + .../1031-Optimise-nearby-player-retrieval.patch | 228 - .../1032-Distance-manager-tick-timings.patch | 34 - ...Handle-Oversized-block-entities-in-chunks.patch | 64 + ...Handle-Oversized-block-entities-in-chunks.patch | 64 - ...l-pos-packets-for-hard-colliding-entities.patch | 23 + ...34-Fix-and-optimise-world-force-upgrading.patch | 395 + ...l-pos-packets-for-hard-colliding-entities.patch | 23 - .../server/1035-API-for-checking-sent-chunks.patch | 62 + ...35-Fix-and-optimise-world-force-upgrading.patch | 395 - .../server/1036-API-for-checking-sent-chunks.patch | 62 - ...ly-handle-pdc-and-custom-tags-in-ItemMeta.patch | 37 + ...verting-old-serialized-names-to-new-names.patch | 32 + 218 files changed, 83104 insertions(+), 83046 deletions(-) delete mode 100644 patches/server/0932-Add-MaterialTagsTest.patch create mode 100644 patches/server/0932-Fix-strikeLightningEffect-powers-lightning-rods-and-.patch create mode 100644 patches/server/0933-Add-hand-to-fish-event-for-all-player-interactions.patch delete mode 100644 patches/server/0933-Fix-strikeLightningEffect-powers-lightning-rods-and-.patch delete mode 100644 patches/server/0934-Add-hand-to-fish-event-for-all-player-interactions.patch create mode 100644 patches/server/0934-Fix-several-issues-with-EntityBreedEvent.patch create mode 100644 patches/server/0935-Add-UUID-attribute-modifier-API.patch delete mode 100644 patches/server/0935-Fix-several-issues-with-EntityBreedEvent.patch delete mode 100644 patches/server/0936-Add-UUID-attribute-modifier-API.patch create mode 100644 patches/server/0936-Fix-missing-event-call-for-entity-teleport-API.patch delete mode 100644 patches/server/0937-Fix-missing-event-call-for-entity-teleport-API.patch create mode 100644 patches/server/0937-Lazily-create-LootContext-for-criterions.patch create mode 100644 patches/server/0938-Don-t-fire-sync-events-during-worldgen.patch delete mode 100644 patches/server/0938-Lazily-create-LootContext-for-criterions.patch create mode 100644 patches/server/0939-Add-Structure-check-API.patch delete mode 100644 patches/server/0939-Don-t-fire-sync-events-during-worldgen.patch delete mode 100644 patches/server/0940-Add-Structure-check-API.patch create mode 100644 patches/server/0940-Fix-CraftMetaItem-getAttributeModifier-duplication-c.patch delete mode 100644 patches/server/0941-Fix-CraftMetaItem-getAttributeModifier-duplication-c.patch create mode 100644 patches/server/0941-Restore-vanilla-entity-drops-behavior.patch create mode 100644 patches/server/0942-Dont-resend-blocks-on-interactions.patch delete mode 100644 patches/server/0942-Restore-vanilla-entity-drops-behavior.patch delete mode 100644 patches/server/0943-Dont-resend-blocks-on-interactions.patch create mode 100644 patches/server/0943-add-more-scoreboard-API.patch create mode 100644 patches/server/0944-Improve-Registry.patch delete mode 100644 patches/server/0944-add-more-scoreboard-API.patch create mode 100644 patches/server/0945-Fix-NPE-on-null-loc-for-EntityTeleportEvent.patch delete mode 100644 patches/server/0945-Improve-Registry.patch create mode 100644 patches/server/0946-Add-experience-points-API.patch delete mode 100644 patches/server/0946-Fix-NPE-on-null-loc-for-EntityTeleportEvent.patch create mode 100644 patches/server/0947-Add-drops-to-shear-events.patch delete mode 100644 patches/server/0947-Add-experience-points-API.patch create mode 100644 patches/server/0948-Add-PlayerShieldDisableEvent.patch delete mode 100644 patches/server/0948-Add-drops-to-shear-events.patch delete mode 100644 patches/server/0949-Add-PlayerShieldDisableEvent.patch create mode 100644 patches/server/0949-Validate-ResourceLocation-in-NBT-reading.patch create mode 100644 patches/server/0950-Properly-handle-experience-dropping-on-block-break.patch delete mode 100644 patches/server/0950-Validate-ResourceLocation-in-NBT-reading.patch create mode 100644 patches/server/0951-Fixup-NamespacedKey-handling.patch delete mode 100644 patches/server/0951-Properly-handle-experience-dropping-on-block-break.patch create mode 100644 patches/server/0952-Expose-LootTable-of-DecoratedPot.patch delete mode 100644 patches/server/0952-Fixup-NamespacedKey-handling.patch delete mode 100644 patches/server/0953-Expose-LootTable-of-DecoratedPot.patch create mode 100644 patches/server/0953-Reduce-allocation-of-Vec3D-by-entity-tracker.patch create mode 100644 patches/server/0954-Add-PlayerTradeEvent-and-PlayerPurchaseEvent.patch delete mode 100644 patches/server/0954-Reduce-allocation-of-Vec3D-by-entity-tracker.patch delete mode 100644 patches/server/0955-Add-PlayerTradeEvent-and-PlayerPurchaseEvent.patch create mode 100644 patches/server/0955-Add-ShulkerDuplicateEvent.patch delete mode 100644 patches/server/0956-Add-ShulkerDuplicateEvent.patch create mode 100644 patches/server/0956-Add-api-for-spawn-egg-texture-colors.patch create mode 100644 patches/server/0957-Add-Lifecycle-Event-system.patch delete mode 100644 patches/server/0957-Add-api-for-spawn-egg-texture-colors.patch delete mode 100644 patches/server/0958-Add-Lifecycle-Event-system.patch create mode 100644 patches/server/0958-ItemStack-Tooltip-API.patch create mode 100644 patches/server/0959-Add-getChunkSnapshot-includeLightData-parameter.patch delete mode 100644 patches/server/0959-ItemStack-Tooltip-API.patch create mode 100644 patches/server/0960-Add-FluidState-API.patch delete mode 100644 patches/server/0960-Add-getChunkSnapshot-includeLightData-parameter.patch delete mode 100644 patches/server/0961-Add-FluidState-API.patch create mode 100644 patches/server/0961-add-number-format-api.patch delete mode 100644 patches/server/0962-add-number-format-api.patch create mode 100644 patches/server/0962-improve-BanList-types.patch create mode 100644 patches/server/0963-Expanded-Hopper-API.patch delete mode 100644 patches/server/0963-improve-BanList-types.patch create mode 100644 patches/server/0964-Add-BlockBreakProgressUpdateEvent.patch delete mode 100644 patches/server/0964-Expanded-Hopper-API.patch delete mode 100644 patches/server/0965-Add-BlockBreakProgressUpdateEvent.patch create mode 100644 patches/server/0965-Deprecate-ItemStack-setType.patch create mode 100644 patches/server/0966-Add-CartographyItemEvent.patch delete mode 100644 patches/server/0966-Deprecate-ItemStack-setType.patch delete mode 100644 patches/server/0967-Add-CartographyItemEvent.patch create mode 100644 patches/server/0967-More-Raid-API.patch create mode 100644 patches/server/0968-Add-onboarding-message-for-initial-server-start.patch delete mode 100644 patches/server/0968-More-Raid-API.patch delete mode 100644 patches/server/0969-Add-onboarding-message-for-initial-server-start.patch create mode 100644 patches/server/0969-Configurable-max-block-fluid-ticks.patch delete mode 100644 patches/server/0970-Configurable-max-block-fluid-ticks.patch create mode 100644 patches/server/0970-Fix-bees-aging-inside-hives.patch create mode 100644 patches/server/0971-Disable-memory-reserve-allocating.patch delete mode 100644 patches/server/0971-Fix-bees-aging-inside-hives.patch delete mode 100644 patches/server/0972-Disable-memory-reserve-allocating.patch create mode 100644 patches/server/0972-Fire-EntityDamageByEntityEvent-for-unowned-wither-sk.patch delete mode 100644 patches/server/0973-Fire-EntityDamageByEntityEvent-for-unowned-wither-sk.patch create mode 100644 patches/server/0973-Fix-DamageSource-API.patch delete mode 100644 patches/server/0974-Fix-DamageSource-API.patch create mode 100644 patches/server/0974-Fix-creation-of-invalid-block-entity-during-world-ge.patch delete mode 100644 patches/server/0975-Fix-creation-of-invalid-block-entity-during-world-ge.patch create mode 100644 patches/server/0975-Fix-possible-StackOverflowError-for-some-dispenses.patch delete mode 100644 patches/server/0976-Fix-possible-StackOverflowError-for-some-dispenses.patch create mode 100644 patches/server/0976-Improve-tag-parser-handling.patch delete mode 100644 patches/server/0977-Improve-tag-parser-handling.patch create mode 100644 patches/server/0977-Item-Mutation-Fixes.patch delete mode 100644 patches/server/0978-Item-Mutation-Fixes.patch create mode 100644 patches/server/0978-Per-world-ticks-per-spawn-settings.patch delete mode 100644 patches/server/0979-Per-world-ticks-per-spawn-settings.patch create mode 100644 patches/server/0979-Properly-track-the-changed-item-from-dispense-events.patch delete mode 100644 patches/server/0980-Properly-track-the-changed-item-from-dispense-events.patch create mode 100644 patches/server/0980-Rewrite-dataconverter-system.patch delete mode 100644 patches/server/0981-Rewrite-dataconverter-system.patch create mode 100644 patches/server/0981-Suspicious-Effect-Entry-API.patch delete mode 100644 patches/server/0982-Suspicious-Effect-Entry-API.patch create mode 100644 patches/server/0982-check-if-itemstack-is-stackable-first.patch delete mode 100644 patches/server/0983-check-if-itemstack-is-stackable-first.patch create mode 100644 patches/server/0983-disable-forced-empty-world-ticks.patch create mode 100644 patches/server/0984-Starlight.patch delete mode 100644 patches/server/0984-disable-forced-empty-world-ticks.patch create mode 100644 patches/server/0985-Rewrite-chunk-system.patch delete mode 100644 patches/server/0985-Starlight.patch delete mode 100644 patches/server/0986-Rewrite-chunk-system.patch create mode 100644 patches/server/0986-incremental-chunk-and-player-saving.patch create mode 100644 patches/server/0987-Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch delete mode 100644 patches/server/0987-incremental-chunk-and-player-saving.patch create mode 100644 patches/server/0988-Improve-Maps-in-item-frames-performance-and-bug-fixe.patch delete mode 100644 patches/server/0988-Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch delete mode 100644 patches/server/0989-Improve-Maps-in-item-frames-performance-and-bug-fixe.patch create mode 100644 patches/server/0989-Strip-raytracing-for-EntityLiving-hasLineOfSight.patch create mode 100644 patches/server/0990-Optimize-Network-Manager-and-add-advanced-packet-sup.patch delete mode 100644 patches/server/0990-Strip-raytracing-for-EntityLiving-hasLineOfSight.patch create mode 100644 patches/server/0991-Allow-Saving-of-Oversized-Chunks.patch delete mode 100644 patches/server/0991-Optimize-Network-Manager-and-add-advanced-packet-sup.patch delete mode 100644 patches/server/0992-Allow-Saving-of-Oversized-Chunks.patch create mode 100644 patches/server/0992-Fix-World-isChunkGenerated-calls.patch delete mode 100644 patches/server/0993-Fix-World-isChunkGenerated-calls.patch create mode 100644 patches/server/0993-Flat-bedrock-generator-settings.patch create mode 100644 patches/server/0994-Entity-Activation-Range-2.0.patch delete mode 100644 patches/server/0994-Flat-bedrock-generator-settings.patch delete mode 100644 patches/server/0995-Entity-Activation-Range-2.0.patch create mode 100644 patches/server/0995-Optional-per-player-mob-spawns.patch create mode 100644 patches/server/0996-Anti-Xray.patch delete mode 100644 patches/server/0996-Optional-per-player-mob-spawns.patch delete mode 100644 patches/server/0997-Anti-Xray.patch create mode 100644 patches/server/0997-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch delete mode 100644 patches/server/0998-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch create mode 100644 patches/server/0998-Optimize-Collision-to-not-load-chunks.patch delete mode 100644 patches/server/0999-Optimize-Collision-to-not-load-chunks.patch create mode 100644 patches/server/0999-Optimize-GoalSelector-Goal.Flag-Set-operations.patch create mode 100644 patches/server/1000-Entity-load-save-limit-per-chunk.patch delete mode 100644 patches/server/1000-Optimize-GoalSelector-Goal.Flag-Set-operations.patch delete mode 100644 patches/server/1001-Entity-load-save-limit-per-chunk.patch create mode 100644 patches/server/1001-Improved-Watchdog-Support.patch delete mode 100644 patches/server/1002-Improved-Watchdog-Support.patch create mode 100644 patches/server/1002-Optimize-Voxel-Shape-Merging.patch delete mode 100644 patches/server/1003-Optimize-Voxel-Shape-Merging.patch create mode 100644 patches/server/1003-Write-SavedData-IO-async.patch create mode 100644 patches/server/1004-Protect-Bedrock-and-End-Portal-Frames-from-being-des.patch delete mode 100644 patches/server/1004-Write-SavedData-IO-async.patch delete mode 100644 patches/server/1005-Protect-Bedrock-and-End-Portal-Frames-from-being-des.patch create mode 100644 patches/server/1005-Use-distance-map-to-optimise-entity-tracker.patch create mode 100644 patches/server/1006-Optimize-Bit-Operations-by-inlining.patch delete mode 100644 patches/server/1006-Use-distance-map-to-optimise-entity-tracker.patch delete mode 100644 patches/server/1007-Optimize-Bit-Operations-by-inlining.patch create mode 100644 patches/server/1007-Remove-streams-from-hot-code.patch create mode 100644 patches/server/1008-Eigencraft-redstone-implementation.patch delete mode 100644 patches/server/1008-Remove-streams-from-hot-code.patch delete mode 100644 patches/server/1009-Eigencraft-redstone-implementation.patch create mode 100644 patches/server/1009-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch create mode 100644 patches/server/1010-Improve-boat-collision-performance.patch delete mode 100644 patches/server/1010-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch delete mode 100644 patches/server/1011-Improve-boat-collision-performance.patch create mode 100644 patches/server/1011-Optimise-general-POI-access.patch create mode 100644 patches/server/1012-Custom-table-implementation-for-blockstate-state-loo.patch delete mode 100644 patches/server/1012-Optimise-general-POI-access.patch delete mode 100644 patches/server/1013-Custom-table-implementation-for-blockstate-state-loo.patch create mode 100644 patches/server/1013-Execute-chunk-tasks-mid-tick.patch delete mode 100644 patches/server/1014-Execute-chunk-tasks-mid-tick.patch create mode 100644 patches/server/1014-Optimise-random-block-ticking.patch create mode 100644 patches/server/1015-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch delete mode 100644 patches/server/1015-Optimise-random-block-ticking.patch delete mode 100644 patches/server/1016-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch create mode 100644 patches/server/1016-Use-Velocity-compression-and-cipher-natives.patch create mode 100644 patches/server/1017-Detail-more-information-in-watchdog-dumps.patch delete mode 100644 patches/server/1017-Use-Velocity-compression-and-cipher-natives.patch create mode 100644 patches/server/1018-Collision-optimisations.patch delete mode 100644 patches/server/1018-Detail-more-information-in-watchdog-dumps.patch delete mode 100644 patches/server/1019-Collision-optimisations.patch create mode 100644 patches/server/1019-Optimise-collision-checking-in-player-move-packet-ha.patch create mode 100644 patches/server/1020-Fix-tripwire-disarming-not-working-as-intended.patch delete mode 100644 patches/server/1020-Optimise-collision-checking-in-player-move-packet-ha.patch create mode 100644 patches/server/1021-Fix-entity-type-tags-suggestions-in-selectors.patch delete mode 100644 patches/server/1021-Fix-tripwire-disarming-not-working-as-intended.patch create mode 100644 patches/server/1022-Add-Alternate-Current-redstone-implementation.patch delete mode 100644 patches/server/1022-Fix-entity-type-tags-suggestions-in-selectors.patch delete mode 100644 patches/server/1023-Add-Alternate-Current-redstone-implementation.patch create mode 100644 patches/server/1023-optimize-dirt-and-snow-spreading.patch create mode 100644 patches/server/1024-Properly-resend-entities.patch delete mode 100644 patches/server/1024-optimize-dirt-and-snow-spreading.patch create mode 100644 patches/server/1025-Optimize-Hoppers.patch delete mode 100644 patches/server/1025-Properly-resend-entities.patch create mode 100644 patches/server/1026-Improve-performance-of-mass-crafts.patch delete mode 100644 patches/server/1026-Optimize-Hoppers.patch create mode 100644 patches/server/1027-Actually-optimise-explosions.patch delete mode 100644 patches/server/1027-Improve-performance-of-mass-crafts.patch delete mode 100644 patches/server/1028-Actually-optimise-explosions.patch create mode 100644 patches/server/1028-Optimise-chunk-tick-iteration.patch create mode 100644 patches/server/1029-Lag-compensation-ticks.patch delete mode 100644 patches/server/1029-Optimise-chunk-tick-iteration.patch delete mode 100644 patches/server/1030-Lag-compensation-ticks.patch create mode 100644 patches/server/1030-Optimise-nearby-player-retrieval.patch create mode 100644 patches/server/1031-Distance-manager-tick-timings.patch delete mode 100644 patches/server/1031-Optimise-nearby-player-retrieval.patch delete mode 100644 patches/server/1032-Distance-manager-tick-timings.patch create mode 100644 patches/server/1032-Handle-Oversized-block-entities-in-chunks.patch delete mode 100644 patches/server/1033-Handle-Oversized-block-entities-in-chunks.patch create mode 100644 patches/server/1033-Send-full-pos-packets-for-hard-colliding-entities.patch create mode 100644 patches/server/1034-Fix-and-optimise-world-force-upgrading.patch delete mode 100644 patches/server/1034-Send-full-pos-packets-for-hard-colliding-entities.patch create mode 100644 patches/server/1035-API-for-checking-sent-chunks.patch delete mode 100644 patches/server/1035-Fix-and-optimise-world-force-upgrading.patch delete mode 100644 patches/server/1036-API-for-checking-sent-chunks.patch create mode 100644 patches/server/1036-Properly-handle-pdc-and-custom-tags-in-ItemMeta.patch create mode 100644 patches/server/1037-handle-converting-old-serialized-names-to-new-names.patch diff --git a/patches/api/0153-Add-Material-Tags.patch b/patches/api/0153-Add-Material-Tags.patch index 87e49d154c..9c8781e5b6 100644 --- a/patches/api/0153-Add-Material-Tags.patch +++ b/patches/api/0153-Add-Material-Tags.patch @@ -115,7 +115,7 @@ index 0000000000000000000000000000000000000000..a02a02aa0c87e0f0ed9e509e4dcab015 +} diff --git a/src/main/java/com/destroystokyo/paper/MaterialTags.java b/src/main/java/com/destroystokyo/paper/MaterialTags.java new file mode 100644 -index 0000000000000000000000000000000000000000..6dc176b694ff4f50ca4b665c3cf82212cf21f16f +index 0000000000000000000000000000000000000000..be212b4fbeabab32a4dab6ae554768c368efaa88 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/MaterialTags.java @@ -0,0 +1,717 @@ @@ -438,7 +438,7 @@ index 0000000000000000000000000000000000000000..6dc176b694ff4f50ca4b665c3cf82212 + */ + public static final MaterialSetTag SPAWN_EGGS = new MaterialSetTag(keyFor("spawn_eggs")) + .endsWith("_SPAWN_EGG") -+ .ensureSize("SPAWN_EGGS", 78).lock(); ++ .ensureSize("SPAWN_EGGS", 80).lock(); + + /** + * Covers all colors of stained glass. diff --git a/patches/api/0254-Add-missing-effects.patch b/patches/api/0254-Add-missing-effects.patch index 63710b51cc..1bda7a8851 100644 --- a/patches/api/0254-Add-missing-effects.patch +++ b/patches/api/0254-Add-missing-effects.patch @@ -6,7 +6,7 @@ Subject: [PATCH] Add missing effects Co-authored-by: Jake Potrebic diff --git a/src/main/java/org/bukkit/Effect.java b/src/main/java/org/bukkit/Effect.java -index fd9cf25f2fdef5ee3b7143f27e5f43dfe3f21a38..2d50f5e3f95e280ae5741514d5eba6440d9966ce 100644 +index fd9cf25f2fdef5ee3b7143f27e5f43dfe3f21a38..326c99d91ae125b28054b5730b9576b214e4595e 100644 --- a/src/main/java/org/bukkit/Effect.java +++ b/src/main/java/org/bukkit/Effect.java @@ -131,9 +131,9 @@ public enum Effect { @@ -21,7 +21,17 @@ index fd9cf25f2fdef5ee3b7143f27e5f43dfe3f21a38..2d50f5e3f95e280ae5741514d5eba644 * additional info. */ POTION_BREAK(2002, Type.VISUAL, Color.class), -@@ -336,21 +336,146 @@ public enum Effect { +@@ -177,7 +177,9 @@ public enum Effect { + /** + * Particles displayed when a villager grows a plant, data + * is the number of particles ++ * @deprecated partially replaced by {@link #BEE_GROWTH} + */ ++ @Deprecated(forRemoval = true, since = "1.20.5") // Paper + VILLAGER_PLANT_GROW(2005, Type.VISUAL, Integer.class), + /** + * The sound/particles used by the enderdragon's breath +@@ -336,21 +338,152 @@ public enum Effect { * block. */ OXIDISED_COPPER_SCRAPE(3005, Type.VISUAL), @@ -128,6 +138,11 @@ index fd9cf25f2fdef5ee3b7143f27e5f43dfe3f21a38..2d50f5e3f95e280ae5741514d5eba644 + */ + SHOOT_WHITE_SMOKE(2010, Type.VISUAL, BlockFace.class), + ++ /** ++ * {@link Integer} param is the number of particles ++ */ ++ BEE_GROWTH(2011, Type.VISUAL, Integer.class), ++ + PARTICLES_SCULK_CHARGE(3006, Type.VISUAL, Integer.class), + + PARTICLES_SCULK_SHRIEK(3007, Type.SOUND), @@ -136,6 +151,7 @@ index fd9cf25f2fdef5ee3b7143f27e5f43dfe3f21a38..2d50f5e3f95e280ae5741514d5eba644 + + PARTICLES_EGG_CRACK(3009, Type.VISUAL), + ++ @Deprecated(forRemoval = true, since = "1.20.5") + GUST_DUST(3010, Type.VISUAL), + + TRIAL_SPAWNER_SPAWN(3011, Type.VISUAL), @@ -172,7 +188,7 @@ index fd9cf25f2fdef5ee3b7143f27e5f43dfe3f21a38..2d50f5e3f95e280ae5741514d5eba644 } /** -@@ -366,8 +491,10 @@ public enum Effect { +@@ -366,8 +499,10 @@ public enum Effect { /** * @return The type of the effect. @@ -183,7 +199,7 @@ index fd9cf25f2fdef5ee3b7143f27e5f43dfe3f21a38..2d50f5e3f95e280ae5741514d5eba644 public Type getType() { return this.type; } -@@ -378,8 +505,15 @@ public enum Effect { +@@ -378,8 +513,15 @@ public enum Effect { */ @Nullable public Class getData() { @@ -200,7 +216,7 @@ index fd9cf25f2fdef5ee3b7143f27e5f43dfe3f21a38..2d50f5e3f95e280ae5741514d5eba644 /** * Gets the Effect associated with the given ID. -@@ -396,12 +530,26 @@ public enum Effect { +@@ -396,12 +538,26 @@ public enum Effect { static { for (Effect effect : values()) { diff --git a/patches/server/0420-Add-methods-to-get-translation-keys.patch b/patches/server/0420-Add-methods-to-get-translation-keys.patch index cb88aca4df..e61dce6cd3 100644 --- a/patches/server/0420-Add-methods-to-get-translation-keys.patch +++ b/patches/server/0420-Add-methods-to-get-translation-keys.patch @@ -5,7 +5,8 @@ Subject: [PATCH] Add methods to get translation keys == AT == public org.bukkit.craftbukkit.inventory.CraftMetaFirework -public org.bukkit.craftbukkit.inventory.CraftMetaFirework getNBT(Lorg/bukkit/FireworkEffect$Type;)I +public org.bukkit.craftbukkit.inventory.CraftMetaFirework power +public org.bukkit.craftbukkit.inventory.CraftMetaFirework getNBT(Lorg/bukkit/FireworkEffect$Type;)Lnet/minecraft/world/item/component/FireworkExplosion$Shape; Co-authored-by: MeFisto94 @@ -40,8 +41,21 @@ index d839a0edf31f6803e5813b28a23fa17e20916b2c..bb8a3e670924ebcce48669c5dce57e2d // Paper end @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java +index ebdb19d7ee0460d50c082b0a41b3a6a57a3534ee..b444bd26d6c3def3494d3cc0520e462408272be3 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaFirework.java +@@ -111,7 +111,7 @@ class CraftMetaFirework extends CraftMetaItem implements FireworkMeta { + return new FireworkExplosion(CraftMetaFirework.getNBT(effect.getType()), colors, fadeColors, effect.hasTrail(), effect.hasFlicker()); + } + +- static FireworkExplosion.Shape getNBT(Type type) { ++ public static FireworkExplosion.Shape getNBT(Type type) { // Paper - package-private -> public + switch (type) { + case BALL: + return FireworkExplosion.Shape.SMALL_BALL; diff --git a/src/test/java/io/papermc/paper/world/TranslationKeyTest.java b/src/test/java/io/papermc/paper/world/TranslationKeyTest.java -index 7f8b6462d2a1bbd39a870d2543bebc135f7eb45b..dbd1dc4453bd26fb6116b62f6ccbf69e92e09fc4 100644 +index 7f8b6462d2a1bbd39a870d2543bebc135f7eb45b..441e8fbd548e425ca9b0dfd69d08f7b83081888c 100644 --- a/src/test/java/io/papermc/paper/world/TranslationKeyTest.java +++ b/src/test/java/io/papermc/paper/world/TranslationKeyTest.java @@ -1,12 +1,27 @@ @@ -73,7 +87,7 @@ index 7f8b6462d2a1bbd39a870d2543bebc135f7eb45b..dbd1dc4453bd26fb6116b62f6ccbf69e @Test public void testChatVisibilityKeys() { -@@ -15,4 +30,67 @@ public class TranslationKeyTest { +@@ -15,4 +30,69 @@ public class TranslationKeyTest { Assertions.assertEquals(ChatVisiblity.valueOf(chatVisibility.name()).getKey(), chatVisibility.translationKey(), chatVisibility + "'s translation key doesn't match"); } } @@ -101,8 +115,10 @@ index 7f8b6462d2a1bbd39a870d2543bebc135f7eb45b..dbd1dc4453bd26fb6116b62f6ccbf69e + + @Test + public void testFireworkEffectType() { -+ for (FireworkEffect.Type type : FireworkEffect.Type.values()) { -+ Assertions.assertEquals(net.minecraft.world.item.FireworkRocketItem.Shape.byId(org.bukkit.craftbukkit.inventory.CraftMetaFirework.getNBT(type)).getName(), org.bukkit.FireworkEffect.Type.NAMES.key(type), "translation key mismatch for " + type); ++ for (final FireworkEffect.Type type : FireworkEffect.Type.values()) { ++ final net.minecraft.world.item.component.FireworkExplosion.Shape nmsType = org.bukkit.craftbukkit.inventory.CraftMetaFirework.getNBT(type); ++ Assertions.assertTrue(nmsType.getName().getContents() instanceof TranslatableContents, "contents aren't translatable"); ++ Assertions.assertEquals(((TranslatableContents) nmsType.getName().getContents()).getKey(), type.translationKey(), "translation key mismatch for " + type); + } + } + diff --git a/patches/server/0456-Add-missing-default-perms-for-commands.patch b/patches/server/0456-Add-missing-default-perms-for-commands.patch index c2cc3ce8cc..d49628ee32 100644 --- a/patches/server/0456-Add-missing-default-perms-for-commands.patch +++ b/patches/server/0456-Add-missing-default-perms-for-commands.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Add missing default perms for commands diff --git a/src/main/java/org/bukkit/craftbukkit/util/permissions/CommandPermissions.java b/src/main/java/org/bukkit/craftbukkit/util/permissions/CommandPermissions.java -index a9ea2e38e4673686c9994a58c94ad19e59fd423c..dd95b3bfe59f2bb635afe92317288efcd2986326 100644 +index a9ea2e38e4673686c9994a58c94ad19e59fd423c..52649f82351ab4f675c3cc3cd6640956b0f76b91 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/permissions/CommandPermissions.java +++ b/src/main/java/org/bukkit/craftbukkit/util/permissions/CommandPermissions.java -@@ -24,13 +24,74 @@ public final class CommandPermissions { +@@ -24,13 +24,75 @@ public final class CommandPermissions { DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "stop", "Allows the user to stop the server", PermissionDefault.OP, commands); DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "list", "Allows the user to list all online players", PermissionDefault.OP, commands); DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "gamemode", "Allows the user to change the gamemode of another player", PermissionDefault.OP, commands); @@ -75,6 +75,7 @@ index a9ea2e38e4673686c9994a58c94ad19e59fd423c..dd95b3bfe59f2bb635afe92317288efc + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "time", "Allows the user to change or query the world's game time", PermissionDefault.OP, commands); + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "tick", "Allows the user to control the tick rate of the server", PermissionDefault.OP, commands); + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "title", "Allows the user to manage screen titles", PermissionDefault.OP, commands); ++ DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "transfer", "Allows the user to transfer to another server", PermissionDefault.OP, commands); + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "weather", "Allows the user to set the weather", PermissionDefault.OP, commands); + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "whitelist", "Allows the user to manage the server whitelist", PermissionDefault.OP, commands); + DefaultPermissions.registerPermission(CommandPermissions.PREFIX + "worldborder", "Allows the user to manage the world border", PermissionDefault.OP, commands); @@ -87,7 +88,7 @@ index a9ea2e38e4673686c9994a58c94ad19e59fd423c..dd95b3bfe59f2bb635afe92317288efc diff --git a/src/test/java/io/papermc/paper/permissions/MinecraftCommandPermissionsTest.java b/src/test/java/io/papermc/paper/permissions/MinecraftCommandPermissionsTest.java new file mode 100644 -index 0000000000000000000000000000000000000000..afeb4271fffb7546209f1e651214065187c88302 +index 0000000000000000000000000000000000000000..ca71c688b37ce2c8b712a4f9216cf872c8edf78e --- /dev/null +++ b/src/test/java/io/papermc/paper/permissions/MinecraftCommandPermissionsTest.java @@ -0,0 +1,82 @@ @@ -132,7 +133,7 @@ index 0000000000000000000000000000000000000000..afeb4271fffb7546209f1e6512140651 + CraftDefaultPermissions.registerCorePermissions(); + Set perms = collectMinecraftCommandPerms(); + -+ Commands commands = new Commands(Commands.CommandSelection.DEDICATED, CommandBuildContext.configurable(AbstractTestingBase.REGISTRY_CUSTOM, FeatureFlags.VANILLA_SET)); ++ Commands commands = new Commands(Commands.CommandSelection.DEDICATED, CommandBuildContext.simple(AbstractTestingBase.REGISTRY_CUSTOM, FeatureFlags.VANILLA_SET)); + RootCommandNode root = commands.getDispatcher().getRoot(); + Set missing = new LinkedHashSet<>(); + Set foundPerms = new HashSet<>(); diff --git a/patches/server/0520-Item-Rarity-API.patch b/patches/server/0520-Item-Rarity-API.patch index 789b4967f0..0939bc6a87 100644 --- a/patches/server/0520-Item-Rarity-API.patch +++ b/patches/server/0520-Item-Rarity-API.patch @@ -33,7 +33,7 @@ index c0592a85a1a0781b83232533b9ca1b30b116eb98..125ed63d8ae812284cd452b4387fd0d6 /** diff --git a/src/test/java/io/papermc/paper/inventory/ItemRarityTest.java b/src/test/java/io/papermc/paper/inventory/ItemRarityTest.java new file mode 100644 -index 0000000000000000000000000000000000000000..d310f86c57e4521ad7666d3f738f53ac83d221f2 +index 0000000000000000000000000000000000000000..8086e6a754cb94ce15428ac0cdbaa43590beb9fe --- /dev/null +++ b/src/test/java/io/papermc/paper/inventory/ItemRarityTest.java @@ -0,0 +1,24 @@ @@ -57,7 +57,7 @@ index 0000000000000000000000000000000000000000..d310f86c57e4521ad7666d3f738f53ac + @Test + public void testRarityFormatting() { + for (Rarity nmsRarity : Rarity.values()) { -+ assertEquals(nmsRarity.color, PaperAdventure.asVanilla(ItemRarity.values()[nmsRarity.ordinal()].color), "rarity formatting is mis-matched"); ++ assertEquals(nmsRarity.color(), PaperAdventure.asVanilla(ItemRarity.values()[nmsRarity.ordinal()].color), "rarity formatting is mis-matched"); + } + } +} diff --git a/patches/server/0552-More-Enchantment-API.patch b/patches/server/0552-More-Enchantment-API.patch index 5fa7b54c59..81b0f3ac5e 100644 --- a/patches/server/0552-More-Enchantment-API.patch +++ b/patches/server/0552-More-Enchantment-API.patch @@ -85,43 +85,3 @@ index bb8a3e670924ebcce48669c5dce57e2df03fad77..bb45dde0fd91796d5fd83a5fd20f5f1d // Paper end @Override -diff --git a/src/test/java/io/papermc/paper/entity/EntityCategoryTest.java b/src/test/java/io/papermc/paper/entity/EntityCategoryTest.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4ee48ef89f0e31a7bda4b04453fca8177874f540 ---- /dev/null -+++ b/src/test/java/io/papermc/paper/entity/EntityCategoryTest.java -@@ -0,0 +1,34 @@ -+package io.papermc.paper.entity; -+ -+import com.google.common.base.Joiner; -+import com.google.common.collect.Maps; -+import com.google.common.collect.Sets; -+import net.minecraft.world.entity.MobType; -+import org.bukkit.craftbukkit.entity.CraftLivingEntity; -+import org.bukkit.entity.EntityCategory; -+import org.junit.jupiter.api.Test; -+ -+import java.lang.reflect.Field; -+import java.util.Map; -+import java.util.Set; -+ -+import static org.junit.jupiter.api.Assertions.assertTrue; -+ -+public class EntityCategoryTest { -+ -+ @Test -+ public void test() throws IllegalAccessException { -+ -+ Map enumMonsterTypeFieldMap = Maps.newHashMap(); -+ for (Field field : MobType.class.getDeclaredFields()) { -+ if (field.getType() == MobType.class) { -+ enumMonsterTypeFieldMap.put( (MobType) field.get(null), field.getName()); -+ } -+ } -+ -+ for (EntityCategory entityCategory : EntityCategory.values()) { -+ enumMonsterTypeFieldMap.remove(CraftLivingEntity.fromBukkitEntityCategory(entityCategory)); -+ } -+ assertTrue(enumMonsterTypeFieldMap.size() == 0, MobType.class.getName() + " instance(s): " + Joiner.on(", ").join(enumMonsterTypeFieldMap.values()) + " do not have bukkit equivalents"); -+ } -+} diff --git a/patches/server/0885-Bandaid-fix-for-Effect.patch b/patches/server/0885-Bandaid-fix-for-Effect.patch index 8700267771..3ae9b87f07 100644 --- a/patches/server/0885-Bandaid-fix-for-Effect.patch +++ b/patches/server/0885-Bandaid-fix-for-Effect.patch @@ -9,19 +9,20 @@ upstream. Until then, this test and these fixes should address all the known issues with them diff --git a/src/main/java/org/bukkit/craftbukkit/CraftEffect.java b/src/main/java/org/bukkit/craftbukkit/CraftEffect.java -index 71733f918ed84b9879ac1b142ef6205c5e768a9c..7ee13c0a0cd12979c7f8cc2e403baf9300fd61c8 100644 +index 71733f918ed84b9879ac1b142ef6205c5e768a9c..239bbc2f5d2010e203eda2750777e507fc5937d0 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftEffect.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftEffect.java -@@ -15,6 +15,8 @@ public class CraftEffect { +@@ -15,6 +15,9 @@ public class CraftEffect { public static int getDataValue(Effect effect, T data) { int datavalue; switch (effect) { + case PARTICLES_SCULK_CHARGE: // Paper - add missing effects + case TRIAL_SPAWNER_DETECT_PLAYER: // Paper - add missing effects ++ case BEE_GROWTH: // Paper - add missing effects case VILLAGER_PLANT_GROW: datavalue = (Integer) data; break; -@@ -26,6 +28,13 @@ public class CraftEffect { +@@ -26,6 +29,13 @@ public class CraftEffect { Preconditions.checkArgument(data == Material.AIR || ((Material) data).isRecord(), "Invalid record type for Material %s!", data); datavalue = Item.getId(CraftItemType.bukkitToMinecraft((Material) data)); break; @@ -35,7 +36,7 @@ index 71733f918ed84b9879ac1b142ef6205c5e768a9c..7ee13c0a0cd12979c7f8cc2e403baf93 case SMOKE: switch ((BlockFace) data) { case DOWN: -@@ -57,8 +66,15 @@ public class CraftEffect { +@@ -57,8 +67,15 @@ public class CraftEffect { } break; case STEP_SOUND: @@ -79,24 +80,28 @@ index 1c264135c34117407bfbb2d230e2ea1f9ebd0671..463b24fd34f76632b590ad659676ba49 Preconditions.checkArgument(effect.getData() == null || effect == Effect.ELECTRIC_SPARK, "Wrong kind of data for the %s effect", effect); diff --git a/src/test/java/org/bukkit/EffectTest.java b/src/test/java/org/bukkit/EffectTest.java new file mode 100644 -index 0000000000000000000000000000000000000000..875eacc2e5776901ba8593d0183844db2571f71b +index 0000000000000000000000000000000000000000..eaccc9a154ff2f498d765a2932b6f3edb449f4f7 --- /dev/null +++ b/src/test/java/org/bukkit/EffectTest.java -@@ -0,0 +1,64 @@ +@@ -0,0 +1,79 @@ +package org.bukkit; + ++import com.google.common.base.Joiner; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; ++import java.util.HashSet; +import java.util.List; +import java.util.Map; ++import java.util.Set; +import net.minecraft.world.level.block.LevelEvent; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; ++import static org.junit.jupiter.api.Assertions.fail; + +public class EffectTest { + @@ -125,12 +130,17 @@ index 0000000000000000000000000000000000000000..875eacc2e5776901ba8593d0183844db + } + } + ++ final Set missingEvents = new HashSet<>(); + for (final Integer event : collectNmsLevelEvents()) { -+ assertNotNull(toId.get(event), "missing API Effect: " + event); ++ if (toId.get(event) == null) { ++ missingEvents.add(event); ++ } ++ } ++ if (!missingEvents.isEmpty()) { ++ fail("Missing API Effects:\n" + Joiner.on("\n").join(missingEvents)); + } + } + -+ @SuppressWarnings("deprecation") + @Test + public void checkNoExtraApi() throws ReflectiveOperationException { + Map toId = new HashMap<>(); @@ -142,8 +152,14 @@ index 0000000000000000000000000000000000000000..875eacc2e5776901ba8593d0183844db + } + + final List nmsEvents = collectNmsLevelEvents(); ++ final Set extraApiEffects = new HashSet<>(); + for (final Map.Entry entry : toId.entrySet()) { -+ assertTrue(nmsEvents.contains(entry.getKey()), "Extra API Effect: " + entry.getValue()); ++ if (!nmsEvents.contains(entry.getKey())) { ++ extraApiEffects.add(entry.getValue()); ++ } ++ } ++ if (!extraApiEffects.isEmpty()) { ++ fail("Extra API Effects:\n" + Joiner.on("\n").join(extraApiEffects)); + } + } +} diff --git a/patches/server/0932-Add-MaterialTagsTest.patch b/patches/server/0932-Add-MaterialTagsTest.patch deleted file mode 100644 index 34233ba1ca..0000000000 --- a/patches/server/0932-Add-MaterialTagsTest.patch +++ /dev/null @@ -1,58 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Shane Freeder -Date: Sat, 4 Nov 2023 18:39:18 -0400 -Subject: [PATCH] Add MaterialTagsTest - - -diff --git a/src/test/java/io/papermc/paper/inventory/item/MaterialTagsTest.java b/src/test/java/io/papermc/paper/inventory/item/MaterialTagsTest.java -new file mode 100644 -index 0000000000000000000000000000000000000000..3f2734eae2053b4b8fccd7149178699e36ce8b78 ---- /dev/null -+++ b/src/test/java/io/papermc/paper/inventory/item/MaterialTagsTest.java -@@ -0,0 +1,46 @@ -+package io.papermc.paper.inventory.item; -+ -+import com.destroystokyo.paper.MaterialTags; -+import it.unimi.dsi.fastutil.objects.ObjectArrayList; -+import java.util.List; -+import java.util.stream.Collectors; -+import java.util.stream.Stream; -+import net.minecraft.core.registries.BuiltInRegistries; -+import net.minecraft.world.item.Item; -+import net.minecraft.world.item.enchantment.EnchantmentCategory; -+import org.bukkit.craftbukkit.util.CraftMagicNumbers; -+import org.bukkit.support.AbstractTestingBase; -+import org.jetbrains.annotations.NotNull; -+import org.junit.jupiter.api.Assertions; -+import org.junit.jupiter.params.ParameterizedTest; -+import org.junit.jupiter.params.provider.MethodSource; -+ -+public class MaterialTagsTest extends AbstractTestingBase { -+ -+ private final static EnchantmentCategory[] ENCHANTMENT_CATEGORIES = EnchantmentCategory.values(); -+ -+ @ParameterizedTest -+ @MethodSource("items") -+ public void testEnchantables(@NotNull final Item item) { -+ final List enchantableCategories = new ObjectArrayList<>(); -+ for (final EnchantmentCategory enchantmentCategory : ENCHANTMENT_CATEGORIES) { -+ if (enchantmentCategory.canEnchant(item)) enchantableCategories.add(enchantmentCategory); -+ } -+ -+ final boolean taggedAsEnchantable = MaterialTags.ENCHANTABLE.isTagged(CraftMagicNumbers.getMaterial(item)); -+ final boolean requiresTagByInternals = !enchantableCategories.isEmpty(); -+ Assertions.assertEquals( -+ requiresTagByInternals, -+ taggedAsEnchantable, -+ () -> "%s matches enchantment categories [%s] but was tagged by material tags as enchantable: %s".formatted( -+ item.getDescriptionId(), -+ enchantableCategories.stream().map(Enum::name).collect(Collectors.joining(", ")), -+ taggedAsEnchantable -+ ) -+ ); -+ } -+ -+ private static Stream items() { -+ return BuiltInRegistries.ITEM.stream(); -+ } -+} diff --git a/patches/server/0932-Fix-strikeLightningEffect-powers-lightning-rods-and-.patch b/patches/server/0932-Fix-strikeLightningEffect-powers-lightning-rods-and-.patch new file mode 100644 index 0000000000..3d51508d96 --- /dev/null +++ b/patches/server/0932-Fix-strikeLightningEffect-powers-lightning-rods-and-.patch @@ -0,0 +1,72 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tamion <70228790+notTamion@users.noreply.github.com> +Date: Sat, 30 Sep 2023 12:36:14 +0200 +Subject: [PATCH] Fix strikeLightningEffect powers lightning rods and clears + copper + + +diff --git a/src/main/java/net/minecraft/world/entity/LightningBolt.java b/src/main/java/net/minecraft/world/entity/LightningBolt.java +index 0471d9c85af02133f99cca4e181b83b58a3f1abc..4f701788bd21b61cad251a3a88f9bc416fb99051 100644 +--- a/src/main/java/net/minecraft/world/entity/LightningBolt.java ++++ b/src/main/java/net/minecraft/world/entity/LightningBolt.java +@@ -47,6 +47,7 @@ public class LightningBolt extends Entity { + private ServerPlayer cause; + private final Set hitEntities = Sets.newHashSet(); + private int blocksSetOnFire; ++ public boolean isEffect; // Paper - Properly handle lightning effects api + + public LightningBolt(EntityType type, Level world) { + super(type, world); +@@ -87,7 +88,7 @@ public class LightningBolt extends Entity { + @Override + public void tick() { + super.tick(); +- if (this.life == 2) { ++ if (!this.isEffect && this.life == 2) { // Paper - Properly handle lightning effects api + if (this.level().isClientSide()) { + this.level().playLocalSound(this.getX(), this.getY(), this.getZ(), SoundEvents.LIGHTNING_BOLT_THUNDER, SoundSource.WEATHER, 10000.0F, 0.8F + this.random.nextFloat() * 0.2F, false); + this.level().playLocalSound(this.getX(), this.getY(), this.getZ(), SoundEvents.LIGHTNING_BOLT_IMPACT, SoundSource.WEATHER, 2.0F, 0.5F + this.random.nextFloat() * 0.2F, false); +@@ -134,7 +135,7 @@ public class LightningBolt extends Entity { + } + } + +- if (this.life >= 0 && !this.visualOnly) { // CraftBukkit - add !this.visualOnly ++ if (this.life >= 0 && !this.isEffect) { // CraftBukkit - add !this.visualOnly // Paper - Properly handle lightning effects api + if (!(this.level() instanceof ServerLevel)) { + this.level().setSkyFlashTime(2); + } else if (!this.visualOnly) { +@@ -163,7 +164,7 @@ public class LightningBolt extends Entity { + } + + private void spawnFire(int spreadAttempts) { +- if (!this.visualOnly && !this.level().isClientSide && this.level().getGameRules().getBoolean(GameRules.RULE_DOFIRETICK)) { ++ if (!this.visualOnly && !this.isEffect && !this.level().isClientSide && this.level().getGameRules().getBoolean(GameRules.RULE_DOFIRETICK)) { // Paper - Properly handle lightning effects api + BlockPos blockposition = this.blockPosition(); + BlockState iblockdata = BaseFireBlock.getState(this.level(), blockposition); + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 3abb776f546edb84307f374943427be02dc4c911..039efd0b8eb141de044668a5633f26eee238c2b0 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -768,7 +768,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + LightningBolt lightning = EntityType.LIGHTNING_BOLT.create(this.world); + lightning.moveTo(loc.getX(), loc.getY(), loc.getZ()); +- lightning.setVisualOnly(isVisual); ++ lightning.isEffect = isVisual; // Paper - Properly handle lightning effects api + this.world.strikeLightning(lightning, LightningStrikeEvent.Cause.CUSTOM); + return (LightningStrike) lightning.getBukkitEntity(); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftLightningStrike.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftLightningStrike.java +index 6fed8075aa75e3852dc826a45ca44603c0446a56..e9f471e60af0725ec34e2985d63ae9ea9f88590a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLightningStrike.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLightningStrike.java +@@ -13,7 +13,7 @@ public class CraftLightningStrike extends CraftEntity implements LightningStrike + + @Override + public boolean isEffect() { +- return this.getHandle().visualOnly; ++ return this.getHandle().isEffect; // Paper - Properly handle lightning effects api + } + + public int getFlashes() { diff --git a/patches/server/0933-Add-hand-to-fish-event-for-all-player-interactions.patch b/patches/server/0933-Add-hand-to-fish-event-for-all-player-interactions.patch new file mode 100644 index 0000000000..3bc8cd9b24 --- /dev/null +++ b/patches/server/0933-Add-hand-to-fish-event-for-all-player-interactions.patch @@ -0,0 +1,75 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: booky10 +Date: Mon, 3 Jul 2023 01:55:32 +0200 +Subject: [PATCH] Add hand to fish event for all player interactions + + +diff --git a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java +index a9ea83ea03ab23abc1eb463dadcdd742bfa20036..35299affb699d745804a3b60cb78b9323f3e068e 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java +@@ -476,7 +476,15 @@ public class FishingHook extends Projectile { + @Override + public void readAdditionalSaveData(CompoundTag nbt) {} + ++ // Paper start - Add hand parameter to PlayerFishEvent ++ @Deprecated ++ @io.papermc.paper.annotation.DoNotUse + public int retrieve(ItemStack usedItem) { ++ return this.retrieve(net.minecraft.world.InteractionHand.MAIN_HAND, usedItem); ++ } ++ ++ public int retrieve(net.minecraft.world.InteractionHand hand, ItemStack usedItem) { ++ // Paper end - Add hand parameter to PlayerFishEvent + net.minecraft.world.entity.player.Player entityhuman = this.getPlayerOwner(); + + if (!this.level().isClientSide && entityhuman != null && !this.shouldStopFishing(entityhuman)) { +@@ -484,7 +492,7 @@ public class FishingHook extends Projectile { + + if (this.hookedIn != null) { + // CraftBukkit start +- PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) entityhuman.getBukkitEntity(), this.hookedIn.getBukkitEntity(), (FishHook) this.getBukkitEntity(), PlayerFishEvent.State.CAUGHT_ENTITY); ++ PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) entityhuman.getBukkitEntity(), this.hookedIn.getBukkitEntity(), (FishHook) this.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), PlayerFishEvent.State.CAUGHT_ENTITY); // Paper - Add hand parameter to PlayerFishEvent + this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent); + + if (playerFishEvent.isCancelled()) { +@@ -513,7 +521,7 @@ public class FishingHook extends Projectile { + } + // Paper end + // CraftBukkit start +- PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) entityhuman.getBukkitEntity(), entityitem != null ? entityitem.getBukkitEntity() : null, (FishHook) this.getBukkitEntity(), PlayerFishEvent.State.CAUGHT_FISH); // Paper - entityitem may be null ++ PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) entityhuman.getBukkitEntity(), entityitem != null ? entityitem.getBukkitEntity() : null, (FishHook) this.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), PlayerFishEvent.State.CAUGHT_FISH); // Paper - entityitem may be null // Paper - Add hand parameter to PlayerFishEvent + playerFishEvent.setExpToDrop(this.random.nextInt(6) + 1); + this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent); + +@@ -547,7 +555,7 @@ public class FishingHook extends Projectile { + + if (this.onGround()) { + // CraftBukkit start +- PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) entityhuman.getBukkitEntity(), null, (FishHook) this.getBukkitEntity(), PlayerFishEvent.State.IN_GROUND); ++ PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) entityhuman.getBukkitEntity(), null, (FishHook) this.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), PlayerFishEvent.State.IN_GROUND); // Paper - Add hand parameter to PlayerFishEvent + this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent); + + if (playerFishEvent.isCancelled()) { +@@ -558,7 +566,7 @@ public class FishingHook extends Projectile { + } + // CraftBukkit start + if (i == 0) { +- PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) entityhuman.getBukkitEntity(), null, (FishHook) this.getBukkitEntity(), PlayerFishEvent.State.REEL_IN); ++ PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) entityhuman.getBukkitEntity(), null, (FishHook) this.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), PlayerFishEvent.State.REEL_IN); // Paper - Add hand parameter to PlayerFishEvent + this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent); + if (playerFishEvent.isCancelled()) { + return 0; +diff --git a/src/main/java/net/minecraft/world/item/FishingRodItem.java b/src/main/java/net/minecraft/world/item/FishingRodItem.java +index 87e1f74c3d37c9dd23350c9cbc190252e8cb88cd..fd71efd124e54168e6f9b35b1a5ab836141ab2d2 100644 +--- a/src/main/java/net/minecraft/world/item/FishingRodItem.java ++++ b/src/main/java/net/minecraft/world/item/FishingRodItem.java +@@ -30,7 +30,7 @@ public class FishingRodItem extends Item { + + if (user.fishing != null) { + if (!world.isClientSide) { +- i = user.fishing.retrieve(itemstack); ++ i = user.fishing.retrieve(hand, itemstack); // Paper - Add hand parameter to PlayerFishEvent + itemstack.hurtAndBreak(i, user, LivingEntity.getSlotForHand(hand)); + } + diff --git a/patches/server/0933-Fix-strikeLightningEffect-powers-lightning-rods-and-.patch b/patches/server/0933-Fix-strikeLightningEffect-powers-lightning-rods-and-.patch deleted file mode 100644 index 3d51508d96..0000000000 --- a/patches/server/0933-Fix-strikeLightningEffect-powers-lightning-rods-and-.patch +++ /dev/null @@ -1,72 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Tamion <70228790+notTamion@users.noreply.github.com> -Date: Sat, 30 Sep 2023 12:36:14 +0200 -Subject: [PATCH] Fix strikeLightningEffect powers lightning rods and clears - copper - - -diff --git a/src/main/java/net/minecraft/world/entity/LightningBolt.java b/src/main/java/net/minecraft/world/entity/LightningBolt.java -index 0471d9c85af02133f99cca4e181b83b58a3f1abc..4f701788bd21b61cad251a3a88f9bc416fb99051 100644 ---- a/src/main/java/net/minecraft/world/entity/LightningBolt.java -+++ b/src/main/java/net/minecraft/world/entity/LightningBolt.java -@@ -47,6 +47,7 @@ public class LightningBolt extends Entity { - private ServerPlayer cause; - private final Set hitEntities = Sets.newHashSet(); - private int blocksSetOnFire; -+ public boolean isEffect; // Paper - Properly handle lightning effects api - - public LightningBolt(EntityType type, Level world) { - super(type, world); -@@ -87,7 +88,7 @@ public class LightningBolt extends Entity { - @Override - public void tick() { - super.tick(); -- if (this.life == 2) { -+ if (!this.isEffect && this.life == 2) { // Paper - Properly handle lightning effects api - if (this.level().isClientSide()) { - this.level().playLocalSound(this.getX(), this.getY(), this.getZ(), SoundEvents.LIGHTNING_BOLT_THUNDER, SoundSource.WEATHER, 10000.0F, 0.8F + this.random.nextFloat() * 0.2F, false); - this.level().playLocalSound(this.getX(), this.getY(), this.getZ(), SoundEvents.LIGHTNING_BOLT_IMPACT, SoundSource.WEATHER, 2.0F, 0.5F + this.random.nextFloat() * 0.2F, false); -@@ -134,7 +135,7 @@ public class LightningBolt extends Entity { - } - } - -- if (this.life >= 0 && !this.visualOnly) { // CraftBukkit - add !this.visualOnly -+ if (this.life >= 0 && !this.isEffect) { // CraftBukkit - add !this.visualOnly // Paper - Properly handle lightning effects api - if (!(this.level() instanceof ServerLevel)) { - this.level().setSkyFlashTime(2); - } else if (!this.visualOnly) { -@@ -163,7 +164,7 @@ public class LightningBolt extends Entity { - } - - private void spawnFire(int spreadAttempts) { -- if (!this.visualOnly && !this.level().isClientSide && this.level().getGameRules().getBoolean(GameRules.RULE_DOFIRETICK)) { -+ if (!this.visualOnly && !this.isEffect && !this.level().isClientSide && this.level().getGameRules().getBoolean(GameRules.RULE_DOFIRETICK)) { // Paper - Properly handle lightning effects api - BlockPos blockposition = this.blockPosition(); - BlockState iblockdata = BaseFireBlock.getState(this.level(), blockposition); - -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index 3abb776f546edb84307f374943427be02dc4c911..039efd0b8eb141de044668a5633f26eee238c2b0 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -@@ -768,7 +768,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { - - LightningBolt lightning = EntityType.LIGHTNING_BOLT.create(this.world); - lightning.moveTo(loc.getX(), loc.getY(), loc.getZ()); -- lightning.setVisualOnly(isVisual); -+ lightning.isEffect = isVisual; // Paper - Properly handle lightning effects api - this.world.strikeLightning(lightning, LightningStrikeEvent.Cause.CUSTOM); - return (LightningStrike) lightning.getBukkitEntity(); - } -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftLightningStrike.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftLightningStrike.java -index 6fed8075aa75e3852dc826a45ca44603c0446a56..e9f471e60af0725ec34e2985d63ae9ea9f88590a 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftLightningStrike.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftLightningStrike.java -@@ -13,7 +13,7 @@ public class CraftLightningStrike extends CraftEntity implements LightningStrike - - @Override - public boolean isEffect() { -- return this.getHandle().visualOnly; -+ return this.getHandle().isEffect; // Paper - Properly handle lightning effects api - } - - public int getFlashes() { diff --git a/patches/server/0934-Add-hand-to-fish-event-for-all-player-interactions.patch b/patches/server/0934-Add-hand-to-fish-event-for-all-player-interactions.patch deleted file mode 100644 index 3bc8cd9b24..0000000000 --- a/patches/server/0934-Add-hand-to-fish-event-for-all-player-interactions.patch +++ /dev/null @@ -1,75 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: booky10 -Date: Mon, 3 Jul 2023 01:55:32 +0200 -Subject: [PATCH] Add hand to fish event for all player interactions - - -diff --git a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java -index a9ea83ea03ab23abc1eb463dadcdd742bfa20036..35299affb699d745804a3b60cb78b9323f3e068e 100644 ---- a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java -+++ b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java -@@ -476,7 +476,15 @@ public class FishingHook extends Projectile { - @Override - public void readAdditionalSaveData(CompoundTag nbt) {} - -+ // Paper start - Add hand parameter to PlayerFishEvent -+ @Deprecated -+ @io.papermc.paper.annotation.DoNotUse - public int retrieve(ItemStack usedItem) { -+ return this.retrieve(net.minecraft.world.InteractionHand.MAIN_HAND, usedItem); -+ } -+ -+ public int retrieve(net.minecraft.world.InteractionHand hand, ItemStack usedItem) { -+ // Paper end - Add hand parameter to PlayerFishEvent - net.minecraft.world.entity.player.Player entityhuman = this.getPlayerOwner(); - - if (!this.level().isClientSide && entityhuman != null && !this.shouldStopFishing(entityhuman)) { -@@ -484,7 +492,7 @@ public class FishingHook extends Projectile { - - if (this.hookedIn != null) { - // CraftBukkit start -- PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) entityhuman.getBukkitEntity(), this.hookedIn.getBukkitEntity(), (FishHook) this.getBukkitEntity(), PlayerFishEvent.State.CAUGHT_ENTITY); -+ PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) entityhuman.getBukkitEntity(), this.hookedIn.getBukkitEntity(), (FishHook) this.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), PlayerFishEvent.State.CAUGHT_ENTITY); // Paper - Add hand parameter to PlayerFishEvent - this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent); - - if (playerFishEvent.isCancelled()) { -@@ -513,7 +521,7 @@ public class FishingHook extends Projectile { - } - // Paper end - // CraftBukkit start -- PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) entityhuman.getBukkitEntity(), entityitem != null ? entityitem.getBukkitEntity() : null, (FishHook) this.getBukkitEntity(), PlayerFishEvent.State.CAUGHT_FISH); // Paper - entityitem may be null -+ PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) entityhuman.getBukkitEntity(), entityitem != null ? entityitem.getBukkitEntity() : null, (FishHook) this.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), PlayerFishEvent.State.CAUGHT_FISH); // Paper - entityitem may be null // Paper - Add hand parameter to PlayerFishEvent - playerFishEvent.setExpToDrop(this.random.nextInt(6) + 1); - this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent); - -@@ -547,7 +555,7 @@ public class FishingHook extends Projectile { - - if (this.onGround()) { - // CraftBukkit start -- PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) entityhuman.getBukkitEntity(), null, (FishHook) this.getBukkitEntity(), PlayerFishEvent.State.IN_GROUND); -+ PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) entityhuman.getBukkitEntity(), null, (FishHook) this.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), PlayerFishEvent.State.IN_GROUND); // Paper - Add hand parameter to PlayerFishEvent - this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent); - - if (playerFishEvent.isCancelled()) { -@@ -558,7 +566,7 @@ public class FishingHook extends Projectile { - } - // CraftBukkit start - if (i == 0) { -- PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) entityhuman.getBukkitEntity(), null, (FishHook) this.getBukkitEntity(), PlayerFishEvent.State.REEL_IN); -+ PlayerFishEvent playerFishEvent = new PlayerFishEvent((Player) entityhuman.getBukkitEntity(), null, (FishHook) this.getBukkitEntity(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand), PlayerFishEvent.State.REEL_IN); // Paper - Add hand parameter to PlayerFishEvent - this.level().getCraftServer().getPluginManager().callEvent(playerFishEvent); - if (playerFishEvent.isCancelled()) { - return 0; -diff --git a/src/main/java/net/minecraft/world/item/FishingRodItem.java b/src/main/java/net/minecraft/world/item/FishingRodItem.java -index 87e1f74c3d37c9dd23350c9cbc190252e8cb88cd..fd71efd124e54168e6f9b35b1a5ab836141ab2d2 100644 ---- a/src/main/java/net/minecraft/world/item/FishingRodItem.java -+++ b/src/main/java/net/minecraft/world/item/FishingRodItem.java -@@ -30,7 +30,7 @@ public class FishingRodItem extends Item { - - if (user.fishing != null) { - if (!world.isClientSide) { -- i = user.fishing.retrieve(itemstack); -+ i = user.fishing.retrieve(hand, itemstack); // Paper - Add hand parameter to PlayerFishEvent - itemstack.hurtAndBreak(i, user, LivingEntity.getSlotForHand(hand)); - } - diff --git a/patches/server/0934-Fix-several-issues-with-EntityBreedEvent.patch b/patches/server/0934-Fix-several-issues-with-EntityBreedEvent.patch new file mode 100644 index 0000000000..96a98989cc --- /dev/null +++ b/patches/server/0934-Fix-several-issues-with-EntityBreedEvent.patch @@ -0,0 +1,118 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Thu, 15 Dec 2022 00:14:44 -0800 +Subject: [PATCH] Fix several issues with EntityBreedEvent + +Upstream did not account for different hands when storing +the breed item for later use in the event. Also they only +stored a reference to the stack, not a copy so if the stack +changed after love mode was started, the breed item in the event +also changed. Also in several places, the breed item was stored after +it was decreased by one to consume the item. + +diff --git a/src/main/java/net/minecraft/world/entity/animal/Animal.java b/src/main/java/net/minecraft/world/entity/animal/Animal.java +index 3bd13c9b8c7573726ae7defdc3c0f32876d1cf1f..5193cf1d3c922d750a11e492b7636215e23ad0d6 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Animal.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Animal.java +@@ -147,8 +147,9 @@ public abstract class Animal extends AgeableMob { + int i = this.getAge(); + + if (!this.level().isClientSide && i == 0 && this.canFallInLove()) { ++ final ItemStack breedCopy = itemstack.copy(); // Paper - Fix EntityBreedEvent copying + this.usePlayerItem(player, hand, itemstack); +- this.setInLove(player); ++ this.setInLove(player, breedCopy); // Paper - Fix EntityBreedEvent copying + return InteractionResult.SUCCESS; + } + +@@ -174,10 +175,18 @@ public abstract class Animal extends AgeableMob { + return this.inLove <= 0; + } + ++ @Deprecated @io.papermc.paper.annotation.DoNotUse // Paper - Fix EntityBreedEvent copying + public void setInLove(@Nullable Player player) { ++ // Paper start - Fix EntityBreedEvent copying ++ this.setInLove(player, null); ++ } ++ public void setInLove(@Nullable Player player, @Nullable ItemStack breedItemCopy) { ++ if (breedItemCopy != null) this.breedItem = breedItemCopy; ++ // Paper end - Fix EntityBreedEvent copying + // CraftBukkit start + EntityEnterLoveModeEvent entityEnterLoveModeEvent = CraftEventFactory.callEntityEnterLoveModeEvent(player, this, 600); + if (entityEnterLoveModeEvent.isCancelled()) { ++ this.breedItem = null; // Paper - Fix EntityBreedEvent copying; clear if cancelled + return; + } + this.inLove = entityEnterLoveModeEvent.getTicksInLove(); +@@ -185,7 +194,7 @@ public abstract class Animal extends AgeableMob { + if (player != null) { + this.loveCause = player.getUUID(); + } +- this.breedItem = player.getInventory().getSelected(); // CraftBukkit ++ // Paper - Fix EntityBreedEvent copying; set breed item in better place + + this.level().broadcastEntityEvent(this, (byte) 18); + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/Panda.java b/src/main/java/net/minecraft/world/entity/animal/Panda.java +index 0440fd2d1bb3f87641ad88de6d5ae64617536a6d..db60b91c2b26ca8cdb66e05deab7742ffe212767 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Panda.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Panda.java +@@ -668,8 +668,9 @@ public class Panda extends Animal { + this.usePlayerItem(player, hand, itemstack); + this.ageUp((int) ((float) (-this.getAge() / 20) * 0.1F), true); + } else if (!this.level().isClientSide && this.getAge() == 0 && this.canFallInLove()) { ++ final ItemStack breedCopy = itemstack.copy(); // Paper - Fix EntityBreedEvent copying + this.usePlayerItem(player, hand, itemstack); +- this.setInLove(player); ++ this.setInLove(player, breedCopy); // Paper - Fix EntityBreedEvent copying + } else { + if (this.level().isClientSide || this.isSitting() || this.isInWater()) { + return InteractionResult.PASS; +diff --git a/src/main/java/net/minecraft/world/entity/animal/camel/Camel.java b/src/main/java/net/minecraft/world/entity/animal/camel/Camel.java +index 33531cbdd9b2a5607092d2364ce5655a63c2361c..9229948c90e278cbdacd0f82689144c8dc50956f 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/camel/Camel.java ++++ b/src/main/java/net/minecraft/world/entity/animal/camel/Camel.java +@@ -389,7 +389,7 @@ public class Camel extends AbstractHorse implements PlayerRideableJumping, Saddl + boolean flag1 = this.isTamed() && this.getAge() == 0 && this.canFallInLove(); + + if (flag1) { +- this.setInLove(player); ++ this.setInLove(player, item.copy()); // Paper - Fix EntityBreedEvent copying + } + + boolean flag2 = this.isBaby(); +diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java b/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java +index 8cae4e154ca07493aba9acadd4ff7150d7ae4f67..9357cf0179d19fbdfe76413e909a99b924c85780 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java ++++ b/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java +@@ -571,7 +571,7 @@ public abstract class AbstractHorse extends Animal implements ContainerListener, + b0 = 5; + if (!this.level().isClientSide && this.isTamed() && this.getAge() == 0 && !this.isInLove()) { + flag = true; +- this.setInLove(player); ++ this.setInLove(player, item.copy()); // Paper - Fix EntityBreedEvent copying + } + } else if (item.is(Items.GOLDEN_APPLE) || item.is(Items.ENCHANTED_GOLDEN_APPLE)) { + f = 10.0F; +@@ -579,7 +579,7 @@ public abstract class AbstractHorse extends Animal implements ContainerListener, + b0 = 10; + if (!this.level().isClientSide && this.isTamed() && this.getAge() == 0 && !this.isInLove()) { + flag = true; +- this.setInLove(player); ++ this.setInLove(player, item.copy()); // Paper - Fix EntityBreedEvent copying + } + } + +diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java b/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java +index d4eceb535561e11f5f7ab891e81aac285f3cd828..1dd4290287725898ace29e46b439b55df8fdd1af 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java ++++ b/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java +@@ -183,7 +183,7 @@ public class Llama extends AbstractChestedHorse implements VariantHolder +Date: Thu, 9 Nov 2023 20:34:44 -0500 +Subject: [PATCH] Add UUID attribute modifier API + + +diff --git a/src/main/java/io/papermc/paper/attribute/UnmodifiableAttributeInstance.java b/src/main/java/io/papermc/paper/attribute/UnmodifiableAttributeInstance.java +index 52439f4b959c74027eb191a3af960c70beb978e8..a2c057d92ea34368c7efc538b6e5b15ef342c54e 100644 +--- a/src/main/java/io/papermc/paper/attribute/UnmodifiableAttributeInstance.java ++++ b/src/main/java/io/papermc/paper/attribute/UnmodifiableAttributeInstance.java +@@ -5,6 +5,7 @@ import org.bukkit.attribute.Attribute; + import org.bukkit.attribute.AttributeModifier; + import org.bukkit.craftbukkit.attribute.CraftAttributeInstance; + ++import java.util.UUID; + import java.util.Collection; + + public class UnmodifiableAttributeInstance extends CraftAttributeInstance { +@@ -18,6 +19,11 @@ public class UnmodifiableAttributeInstance extends CraftAttributeInstance { + throw new UnsupportedOperationException("Cannot modify default attributes"); + } + ++ @Override ++ public void removeModifier(UUID uuid) { ++ throw new UnsupportedOperationException("Cannot modify default attributes"); ++ } ++ + @Override + public void addModifier(AttributeModifier modifier) { + throw new UnsupportedOperationException("Cannot modify default attributes"); +diff --git a/src/main/java/org/bukkit/craftbukkit/attribute/CraftAttributeInstance.java b/src/main/java/org/bukkit/craftbukkit/attribute/CraftAttributeInstance.java +index c0fb6da6bc3c5b4f37601d898fa421d430417d97..bd0f7dceab1a74dbb7486e126e31152579f987de 100644 +--- a/src/main/java/org/bukkit/craftbukkit/attribute/CraftAttributeInstance.java ++++ b/src/main/java/org/bukkit/craftbukkit/attribute/CraftAttributeInstance.java +@@ -44,6 +44,21 @@ public class CraftAttributeInstance implements AttributeInstance { + return result; + } + ++ // Paper start ++ @Override ++ public AttributeModifier getModifier(java.util.UUID uuid) { ++ Preconditions.checkArgument(uuid != null, "UUID cannot be null"); ++ net.minecraft.world.entity.ai.attributes.AttributeModifier modifier = this.handle.getModifier(uuid); ++ return modifier == null ? null : CraftAttributeInstance.convert(modifier); ++ } ++ ++ @Override ++ public void removeModifier(java.util.UUID uuid) { ++ Preconditions.checkArgument(uuid != null, "UUID cannot be null"); ++ this.handle.removeModifier(uuid); ++ } ++ // Paper end ++ + @Override + public void addModifier(AttributeModifier modifier) { + Preconditions.checkArgument(modifier != null, "modifier"); diff --git a/patches/server/0935-Fix-several-issues-with-EntityBreedEvent.patch b/patches/server/0935-Fix-several-issues-with-EntityBreedEvent.patch deleted file mode 100644 index 96a98989cc..0000000000 --- a/patches/server/0935-Fix-several-issues-with-EntityBreedEvent.patch +++ /dev/null @@ -1,118 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Thu, 15 Dec 2022 00:14:44 -0800 -Subject: [PATCH] Fix several issues with EntityBreedEvent - -Upstream did not account for different hands when storing -the breed item for later use in the event. Also they only -stored a reference to the stack, not a copy so if the stack -changed after love mode was started, the breed item in the event -also changed. Also in several places, the breed item was stored after -it was decreased by one to consume the item. - -diff --git a/src/main/java/net/minecraft/world/entity/animal/Animal.java b/src/main/java/net/minecraft/world/entity/animal/Animal.java -index 3bd13c9b8c7573726ae7defdc3c0f32876d1cf1f..5193cf1d3c922d750a11e492b7636215e23ad0d6 100644 ---- a/src/main/java/net/minecraft/world/entity/animal/Animal.java -+++ b/src/main/java/net/minecraft/world/entity/animal/Animal.java -@@ -147,8 +147,9 @@ public abstract class Animal extends AgeableMob { - int i = this.getAge(); - - if (!this.level().isClientSide && i == 0 && this.canFallInLove()) { -+ final ItemStack breedCopy = itemstack.copy(); // Paper - Fix EntityBreedEvent copying - this.usePlayerItem(player, hand, itemstack); -- this.setInLove(player); -+ this.setInLove(player, breedCopy); // Paper - Fix EntityBreedEvent copying - return InteractionResult.SUCCESS; - } - -@@ -174,10 +175,18 @@ public abstract class Animal extends AgeableMob { - return this.inLove <= 0; - } - -+ @Deprecated @io.papermc.paper.annotation.DoNotUse // Paper - Fix EntityBreedEvent copying - public void setInLove(@Nullable Player player) { -+ // Paper start - Fix EntityBreedEvent copying -+ this.setInLove(player, null); -+ } -+ public void setInLove(@Nullable Player player, @Nullable ItemStack breedItemCopy) { -+ if (breedItemCopy != null) this.breedItem = breedItemCopy; -+ // Paper end - Fix EntityBreedEvent copying - // CraftBukkit start - EntityEnterLoveModeEvent entityEnterLoveModeEvent = CraftEventFactory.callEntityEnterLoveModeEvent(player, this, 600); - if (entityEnterLoveModeEvent.isCancelled()) { -+ this.breedItem = null; // Paper - Fix EntityBreedEvent copying; clear if cancelled - return; - } - this.inLove = entityEnterLoveModeEvent.getTicksInLove(); -@@ -185,7 +194,7 @@ public abstract class Animal extends AgeableMob { - if (player != null) { - this.loveCause = player.getUUID(); - } -- this.breedItem = player.getInventory().getSelected(); // CraftBukkit -+ // Paper - Fix EntityBreedEvent copying; set breed item in better place - - this.level().broadcastEntityEvent(this, (byte) 18); - } -diff --git a/src/main/java/net/minecraft/world/entity/animal/Panda.java b/src/main/java/net/minecraft/world/entity/animal/Panda.java -index 0440fd2d1bb3f87641ad88de6d5ae64617536a6d..db60b91c2b26ca8cdb66e05deab7742ffe212767 100644 ---- a/src/main/java/net/minecraft/world/entity/animal/Panda.java -+++ b/src/main/java/net/minecraft/world/entity/animal/Panda.java -@@ -668,8 +668,9 @@ public class Panda extends Animal { - this.usePlayerItem(player, hand, itemstack); - this.ageUp((int) ((float) (-this.getAge() / 20) * 0.1F), true); - } else if (!this.level().isClientSide && this.getAge() == 0 && this.canFallInLove()) { -+ final ItemStack breedCopy = itemstack.copy(); // Paper - Fix EntityBreedEvent copying - this.usePlayerItem(player, hand, itemstack); -- this.setInLove(player); -+ this.setInLove(player, breedCopy); // Paper - Fix EntityBreedEvent copying - } else { - if (this.level().isClientSide || this.isSitting() || this.isInWater()) { - return InteractionResult.PASS; -diff --git a/src/main/java/net/minecraft/world/entity/animal/camel/Camel.java b/src/main/java/net/minecraft/world/entity/animal/camel/Camel.java -index 33531cbdd9b2a5607092d2364ce5655a63c2361c..9229948c90e278cbdacd0f82689144c8dc50956f 100644 ---- a/src/main/java/net/minecraft/world/entity/animal/camel/Camel.java -+++ b/src/main/java/net/minecraft/world/entity/animal/camel/Camel.java -@@ -389,7 +389,7 @@ public class Camel extends AbstractHorse implements PlayerRideableJumping, Saddl - boolean flag1 = this.isTamed() && this.getAge() == 0 && this.canFallInLove(); - - if (flag1) { -- this.setInLove(player); -+ this.setInLove(player, item.copy()); // Paper - Fix EntityBreedEvent copying - } - - boolean flag2 = this.isBaby(); -diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java b/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java -index 8cae4e154ca07493aba9acadd4ff7150d7ae4f67..9357cf0179d19fbdfe76413e909a99b924c85780 100644 ---- a/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java -+++ b/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java -@@ -571,7 +571,7 @@ public abstract class AbstractHorse extends Animal implements ContainerListener, - b0 = 5; - if (!this.level().isClientSide && this.isTamed() && this.getAge() == 0 && !this.isInLove()) { - flag = true; -- this.setInLove(player); -+ this.setInLove(player, item.copy()); // Paper - Fix EntityBreedEvent copying - } - } else if (item.is(Items.GOLDEN_APPLE) || item.is(Items.ENCHANTED_GOLDEN_APPLE)) { - f = 10.0F; -@@ -579,7 +579,7 @@ public abstract class AbstractHorse extends Animal implements ContainerListener, - b0 = 10; - if (!this.level().isClientSide && this.isTamed() && this.getAge() == 0 && !this.isInLove()) { - flag = true; -- this.setInLove(player); -+ this.setInLove(player, item.copy()); // Paper - Fix EntityBreedEvent copying - } - } - -diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java b/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java -index d4eceb535561e11f5f7ab891e81aac285f3cd828..1dd4290287725898ace29e46b439b55df8fdd1af 100644 ---- a/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java -+++ b/src/main/java/net/minecraft/world/entity/animal/horse/Llama.java -@@ -183,7 +183,7 @@ public class Llama extends AbstractChestedHorse implements VariantHolder -Date: Thu, 9 Nov 2023 20:34:44 -0500 -Subject: [PATCH] Add UUID attribute modifier API - - -diff --git a/src/main/java/io/papermc/paper/attribute/UnmodifiableAttributeInstance.java b/src/main/java/io/papermc/paper/attribute/UnmodifiableAttributeInstance.java -index 52439f4b959c74027eb191a3af960c70beb978e8..a2c057d92ea34368c7efc538b6e5b15ef342c54e 100644 ---- a/src/main/java/io/papermc/paper/attribute/UnmodifiableAttributeInstance.java -+++ b/src/main/java/io/papermc/paper/attribute/UnmodifiableAttributeInstance.java -@@ -5,6 +5,7 @@ import org.bukkit.attribute.Attribute; - import org.bukkit.attribute.AttributeModifier; - import org.bukkit.craftbukkit.attribute.CraftAttributeInstance; - -+import java.util.UUID; - import java.util.Collection; - - public class UnmodifiableAttributeInstance extends CraftAttributeInstance { -@@ -18,6 +19,11 @@ public class UnmodifiableAttributeInstance extends CraftAttributeInstance { - throw new UnsupportedOperationException("Cannot modify default attributes"); - } - -+ @Override -+ public void removeModifier(UUID uuid) { -+ throw new UnsupportedOperationException("Cannot modify default attributes"); -+ } -+ - @Override - public void addModifier(AttributeModifier modifier) { - throw new UnsupportedOperationException("Cannot modify default attributes"); -diff --git a/src/main/java/org/bukkit/craftbukkit/attribute/CraftAttributeInstance.java b/src/main/java/org/bukkit/craftbukkit/attribute/CraftAttributeInstance.java -index c0fb6da6bc3c5b4f37601d898fa421d430417d97..bd0f7dceab1a74dbb7486e126e31152579f987de 100644 ---- a/src/main/java/org/bukkit/craftbukkit/attribute/CraftAttributeInstance.java -+++ b/src/main/java/org/bukkit/craftbukkit/attribute/CraftAttributeInstance.java -@@ -44,6 +44,21 @@ public class CraftAttributeInstance implements AttributeInstance { - return result; - } - -+ // Paper start -+ @Override -+ public AttributeModifier getModifier(java.util.UUID uuid) { -+ Preconditions.checkArgument(uuid != null, "UUID cannot be null"); -+ net.minecraft.world.entity.ai.attributes.AttributeModifier modifier = this.handle.getModifier(uuid); -+ return modifier == null ? null : CraftAttributeInstance.convert(modifier); -+ } -+ -+ @Override -+ public void removeModifier(java.util.UUID uuid) { -+ Preconditions.checkArgument(uuid != null, "UUID cannot be null"); -+ this.handle.removeModifier(uuid); -+ } -+ // Paper end -+ - @Override - public void addModifier(AttributeModifier modifier) { - Preconditions.checkArgument(modifier != null, "modifier"); diff --git a/patches/server/0936-Fix-missing-event-call-for-entity-teleport-API.patch b/patches/server/0936-Fix-missing-event-call-for-entity-teleport-API.patch new file mode 100644 index 0000000000..ac77e45fa2 --- /dev/null +++ b/patches/server/0936-Fix-missing-event-call-for-entity-teleport-API.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: booky10 +Date: Sun, 12 Nov 2023 05:09:47 +0100 +Subject: [PATCH] Fix missing event call for entity teleport API + + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +index d52b7c5739b8ea51cb91c45ddcad6237b9ce5a5e..3ab04c4bdbe26ff7f6f54eb9cdd58376c592fa05 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +@@ -256,6 +256,17 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + return false; + } + ++ // Paper start - fix teleport event not being called ++ org.bukkit.event.entity.EntityTeleportEvent event = new org.bukkit.event.entity.EntityTeleportEvent( ++ this, this.getLocation(), location); ++ // cancelling the event is handled differently for players and entities, ++ // entities just stop teleporting, players will still teleport to the "from" location of the event ++ if (!event.callEvent() || event.getTo() == null) { ++ return false; ++ } ++ location = event.getTo(); ++ // Paper end ++ + // If this entity is riding another entity, we must dismount before teleporting. + if (dismount) this.entity.stopRiding(); // Paper - Teleport passenger API + diff --git a/patches/server/0937-Fix-missing-event-call-for-entity-teleport-API.patch b/patches/server/0937-Fix-missing-event-call-for-entity-teleport-API.patch deleted file mode 100644 index ac77e45fa2..0000000000 --- a/patches/server/0937-Fix-missing-event-call-for-entity-teleport-API.patch +++ /dev/null @@ -1,28 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: booky10 -Date: Sun, 12 Nov 2023 05:09:47 +0100 -Subject: [PATCH] Fix missing event call for entity teleport API - - -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -index d52b7c5739b8ea51cb91c45ddcad6237b9ce5a5e..3ab04c4bdbe26ff7f6f54eb9cdd58376c592fa05 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -@@ -256,6 +256,17 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { - return false; - } - -+ // Paper start - fix teleport event not being called -+ org.bukkit.event.entity.EntityTeleportEvent event = new org.bukkit.event.entity.EntityTeleportEvent( -+ this, this.getLocation(), location); -+ // cancelling the event is handled differently for players and entities, -+ // entities just stop teleporting, players will still teleport to the "from" location of the event -+ if (!event.callEvent() || event.getTo() == null) { -+ return false; -+ } -+ location = event.getTo(); -+ // Paper end -+ - // If this entity is riding another entity, we must dismount before teleporting. - if (dismount) this.entity.stopRiding(); // Paper - Teleport passenger API - diff --git a/patches/server/0937-Lazily-create-LootContext-for-criterions.patch b/patches/server/0937-Lazily-create-LootContext-for-criterions.patch new file mode 100644 index 0000000000..d1033bf7e7 --- /dev/null +++ b/patches/server/0937-Lazily-create-LootContext-for-criterions.patch @@ -0,0 +1,30 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrPowerGamerBR +Date: Tue, 21 Nov 2023 12:16:39 -0300 +Subject: [PATCH] Lazily create LootContext for criterions + +For each player on each tick, enter block triggers are invoked, and these create loot contexts that are promptly thrown away since the trigger doesn't pass the predicate + +To avoid this, we now lazily create the LootContext if the criterion passes the predicate AND if any of the listener triggers require a loot context instance + +diff --git a/src/main/java/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java b/src/main/java/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java +index f43053ba082f9772b6ec02828fa2d6f387c32d26..35772110e9318df46a2729dbc0b5879b290011b7 100644 +--- a/src/main/java/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java ++++ b/src/main/java/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java +@@ -42,14 +42,14 @@ public abstract class SimpleCriterionTrigger> set = (Set) playerAdvancements.criterionData.get(this); // Paper - fix AdvancementDataPlayer leak + if (set != null && !set.isEmpty()) { +- LootContext lootContext = EntityPredicate.createContext(player, player); ++ LootContext lootContext = null; // EntityPredicate.createContext(player, player); // Paper - Perf: lazily create LootContext for criterions + List> list = null; + + for (CriterionTrigger.Listener listener : set) { + T simpleInstance = listener.trigger(); + if (predicate.test(simpleInstance)) { + Optional optional = simpleInstance.player(); +- if (optional.isEmpty() || optional.get().matches(lootContext)) { ++ if (optional.isEmpty() || optional.get().matches(lootContext = (lootContext == null ? EntityPredicate.createContext(player, player) : lootContext))) { // Paper - Perf: lazily create LootContext for criterions + if (list == null) { + list = Lists.newArrayList(); + } diff --git a/patches/server/0938-Don-t-fire-sync-events-during-worldgen.patch b/patches/server/0938-Don-t-fire-sync-events-during-worldgen.patch new file mode 100644 index 0000000000..492d95271a --- /dev/null +++ b/patches/server/0938-Don-t-fire-sync-events-during-worldgen.patch @@ -0,0 +1,208 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Thu, 23 Nov 2023 10:33:25 -0800 +Subject: [PATCH] Don't fire sync events during worldgen + +Fixes EntityPotionEffectEvent +Fixes EntityPoseChangeEvent + +Asynchronous chunk generation provides an opportunity for things +to happen async that previously fired synchronous-only events. This +patch is for mitigating those issues by various methods. + +Also fixes correctly marking/clearing the entity generation flag. +This patch sets the generation flag anytime an entity is created +via StructureTemplate before loading from NBT to catch uses of +the flag during the loading logic. This patch clears the generation +flag from an entity when added to a ServerLevel for the situation +where generation happened directly to a ServerLevel and the +entity still has the flag set. + +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 601ddb8a2ed1559a3c439c1aca49f772ffdd092a..6e7f98fd7b43b46171c3cd58ed91b53960bd0d8a 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -1232,6 +1232,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + // CraftBukkit start + private boolean addEntity(Entity entity, CreatureSpawnEvent.SpawnReason spawnReason) { + org.spigotmc.AsyncCatcher.catchOp("entity add"); // Spigot ++ entity.generation = false; // Paper - Don't fire sync event during generation; Reset flag if it was added during a ServerLevel generation process + // Paper start - extra debug info + if (entity.valid) { + MinecraftServer.LOGGER.error("Attempted Double World add on {}", entity, new Throwable()); +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index acf2cc8afaf4dee193ecf3bfc4a39b8c39ffc376..ee38f76e46c20e9fadfb5d4d602b7b82a8a22e51 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -650,7 +650,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + if (pose == this.getPose()) { + return; + } +- this.level.getCraftServer().getPluginManager().callEvent(new EntityPoseChangeEvent(this.getBukkitEntity(), Pose.values()[pose.ordinal()])); ++ // Paper start - Don't fire sync event during generation ++ if (!this.generation) { ++ this.level.getCraftServer().getPluginManager().callEvent(new EntityPoseChangeEvent(this.getBukkitEntity(), Pose.values()[pose.ordinal()])); ++ } ++ // Paper end - Don't fire sync event during generation + // CraftBukkit end + this.entityData.set(Entity.DATA_POSE, pose); + } +diff --git a/src/main/java/net/minecraft/world/entity/EntityType.java b/src/main/java/net/minecraft/world/entity/EntityType.java +index d769dc472d3f12bf1e30ddd767a76659b734caac..8c7cc9c5af0b8d8bef9b6e2d3d3e723cd76f3212 100644 +--- a/src/main/java/net/minecraft/world/entity/EntityType.java ++++ b/src/main/java/net/minecraft/world/entity/EntityType.java +@@ -592,9 +592,15 @@ public class EntityType implements FeatureElement, EntityTypeT + } + + public static Optional create(CompoundTag nbt, Level world) { ++ // Paper start - Don't fire sync event during generation ++ return create(nbt, world, false); ++ } ++ public static Optional create(CompoundTag nbt, Level world, boolean generation) { ++ // Paper end - Don't fire sync event during generation + return Util.ifElse(EntityType.by(nbt).map((entitytypes) -> { + return entitytypes.create(world); + }), (entity) -> { ++ if (generation) entity.generation = true; // Paper - Don't fire sync event during generation + entity.load(nbt); + }, () -> { + EntityType.LOGGER.warn("Skipping Entity with id {}", nbt.getString("id")); +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index a8b168f56c74feeaca55335731b1d485289736cb..281d2f3df347e911eee459e06b917a0e075078e9 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -1163,6 +1163,11 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + public boolean addEffect(MobEffectInstance mobeffect, @Nullable Entity entity, EntityPotionEffectEvent.Cause cause) { ++ // Paper start - Don't fire sync event during generation ++ return this.addEffect(mobeffect, entity, cause, true); ++ } ++ public boolean addEffect(MobEffectInstance mobeffect, @Nullable Entity entity, EntityPotionEffectEvent.Cause cause, boolean fireEvent) { ++ // Paper end - Don't fire sync event during generation + // org.spigotmc.AsyncCatcher.catchOp("effect add"); // Spigot // Paper - move to API + if (this.isTickingEffects) { + this.effectsToProcess.add(new ProcessableEffect(mobeffect, cause)); +@@ -1182,10 +1187,13 @@ public abstract class LivingEntity extends Entity implements Attackable { + override = new MobEffectInstance(mobeffect1).update(mobeffect); + } + ++ if (fireEvent) { // Paper - Don't fire sync event during generation + EntityPotionEffectEvent event = CraftEventFactory.callEntityPotionEffectChangeEvent(this, mobeffect1, mobeffect, cause, override); ++ override = event.isOverride(); // Paper - Don't fire sync event during generation + if (event.isCancelled()) { + return false; + } ++ } // Paper - Don't fire sync event during generation + // CraftBukkit end + + if (mobeffect1 == null) { +@@ -1194,7 +1202,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + flag = true; + mobeffect.onEffectAdded(this); + // CraftBukkit start +- } else if (event.isOverride()) { ++ } else if (override) { // Paper - Don't fire sync event during generation + mobeffect1.update(mobeffect); + this.onEffectUpdated(mobeffect1, true, entity); + // CraftBukkit end +diff --git a/src/main/java/net/minecraft/world/entity/monster/Spider.java b/src/main/java/net/minecraft/world/entity/monster/Spider.java +index 6006480d9f6d60bb7b5628eabe6740013066cde4..f0127f1b55999aa4a841341ad02cbcde45702b50 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Spider.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Spider.java +@@ -172,7 +172,7 @@ public class Spider extends Monster { + Holder holder = entityspider_groupdataspider.effect; + + if (holder != null) { +- this.addEffect(new MobEffectInstance(holder, -1), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.SPIDER_SPAWN); // CraftBukkit ++ this.addEffect(new MobEffectInstance(holder, -1), null, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.SPIDER_SPAWN, world instanceof net.minecraft.server.level.ServerLevel); // CraftBukkit + } + } + +diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate.java b/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate.java +index b5365dce7882c98b5be4f5df877165eee80933dd..9b4e2f96299bb12af7090ef70ac158c825d49d19 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate.java +@@ -525,7 +525,7 @@ public class StructureTemplate { + private static Optional createEntityIgnoreException(ServerLevelAccessor world, CompoundTag nbt) { + // CraftBukkit start + // try { +- return EntityType.create(nbt, world.getLevel()); ++ return EntityType.create(nbt, world.getLevel(), true); // Paper - Don't fire sync event during generation + // } catch (Exception exception) { + // return Optional.empty(); + // } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java +index 0794d92c42b0db6b367505ae28f09f1fd39fa312..cd7f1309cf01a5f01a28aded03a36fe15adb1756 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java +@@ -92,15 +92,17 @@ public abstract class DelegatedGeneratorAccess implements WorldGenLevel { + return this.handle.getLevel(); + } + +- @Override +- public void addFreshEntityWithPassengers(Entity arg0, CreatureSpawnEvent.SpawnReason arg1) { +- this.handle.addFreshEntityWithPassengers(arg0, arg1); +- } +- +- @Override +- public void addFreshEntityWithPassengers(Entity entity) { +- this.handle.addFreshEntityWithPassengers(entity); +- } ++ // Paper start - Don't fire sync event during generation; don't override these methods so all entities are run through addFreshEntity ++ // @Override ++ // public void addFreshEntityWithPassengers(Entity arg0, CreatureSpawnEvent.SpawnReason arg1) { ++ // this.handle.addFreshEntityWithPassengers(arg0, arg1); ++ // } ++ // ++ // @Override ++ // public void addFreshEntityWithPassengers(Entity entity) { ++ // this.handle.addFreshEntityWithPassengers(entity); ++ // } ++ // Paper end - Don't fire sync event during generation; don't override these methods + + @Override + public ServerLevel getMinecraftWorld() { +diff --git a/src/main/java/org/bukkit/craftbukkit/util/TransformerGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/TransformerGeneratorAccess.java +index 35ecf6f824aca56a20280dd683123df1d0c7d66e..1d1fdcf10498c421f106158254e052da6d68d8a5 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/TransformerGeneratorAccess.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/TransformerGeneratorAccess.java +@@ -39,21 +39,23 @@ public class TransformerGeneratorAccess extends DelegatedGeneratorAccess { + return super.addFreshEntity(arg0, arg1); + } + +- @Override +- public void addFreshEntityWithPassengers(Entity entity) { +- if (this.structureTransformer != null && !this.structureTransformer.transformEntity(entity)) { +- return; +- } +- super.addFreshEntityWithPassengers(entity); +- } +- +- @Override +- public void addFreshEntityWithPassengers(Entity arg0, SpawnReason arg1) { +- if (this.structureTransformer != null && !this.structureTransformer.transformEntity(arg0)) { +- return; +- } +- super.addFreshEntityWithPassengers(arg0, arg1); +- } ++ // Paper start - Don't fire sync event during generation; don't override these methods so all entities are run through addFreshEntity ++ // @Override ++ // public void addFreshEntityWithPassengers(Entity entity) { ++ // if (this.structureTransformer != null && !this.structureTransformer.transformEntity(entity)) { ++ // return; ++ // } ++ // super.addFreshEntityWithPassengers(entity); ++ // } ++ // ++ // @Override ++ // public void addFreshEntityWithPassengers(Entity arg0, SpawnReason arg1) { ++ // if (this.structureTransformer != null && !this.structureTransformer.transformEntity(arg0)) { ++ // return; ++ // } ++ // super.addFreshEntityWithPassengers(arg0, arg1); ++ // } ++ // Paper end - Don't fire sync event during generation; don't override these methods + + public boolean setCraftBlock(BlockPos position, CraftBlockState craftBlockState, int i, int j) { + if (this.structureTransformer != null) { diff --git a/patches/server/0938-Lazily-create-LootContext-for-criterions.patch b/patches/server/0938-Lazily-create-LootContext-for-criterions.patch deleted file mode 100644 index d1033bf7e7..0000000000 --- a/patches/server/0938-Lazily-create-LootContext-for-criterions.patch +++ /dev/null @@ -1,30 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: MrPowerGamerBR -Date: Tue, 21 Nov 2023 12:16:39 -0300 -Subject: [PATCH] Lazily create LootContext for criterions - -For each player on each tick, enter block triggers are invoked, and these create loot contexts that are promptly thrown away since the trigger doesn't pass the predicate - -To avoid this, we now lazily create the LootContext if the criterion passes the predicate AND if any of the listener triggers require a loot context instance - -diff --git a/src/main/java/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java b/src/main/java/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java -index f43053ba082f9772b6ec02828fa2d6f387c32d26..35772110e9318df46a2729dbc0b5879b290011b7 100644 ---- a/src/main/java/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java -+++ b/src/main/java/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java -@@ -42,14 +42,14 @@ public abstract class SimpleCriterionTrigger> set = (Set) playerAdvancements.criterionData.get(this); // Paper - fix AdvancementDataPlayer leak - if (set != null && !set.isEmpty()) { -- LootContext lootContext = EntityPredicate.createContext(player, player); -+ LootContext lootContext = null; // EntityPredicate.createContext(player, player); // Paper - Perf: lazily create LootContext for criterions - List> list = null; - - for (CriterionTrigger.Listener listener : set) { - T simpleInstance = listener.trigger(); - if (predicate.test(simpleInstance)) { - Optional optional = simpleInstance.player(); -- if (optional.isEmpty() || optional.get().matches(lootContext)) { -+ if (optional.isEmpty() || optional.get().matches(lootContext = (lootContext == null ? EntityPredicate.createContext(player, player) : lootContext))) { // Paper - Perf: lazily create LootContext for criterions - if (list == null) { - list = Lists.newArrayList(); - } diff --git a/patches/server/0939-Add-Structure-check-API.patch b/patches/server/0939-Add-Structure-check-API.patch new file mode 100644 index 0000000000..b1501eb55b --- /dev/null +++ b/patches/server/0939-Add-Structure-check-API.patch @@ -0,0 +1,31 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Mon, 27 Mar 2023 10:20:00 -0700 +Subject: [PATCH] Add Structure check API + + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 039efd0b8eb141de044668a5633f26eee238c2b0..4a62482b7c73728de8a94542e8978e836540b328 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -237,6 +237,20 @@ public class CraftWorld extends CraftRegionAccessor implements World { + }; + } + // Paper end ++ // Paper start - structure check API ++ @Override ++ public boolean hasStructureAt(final io.papermc.paper.math.Position position, final Structure structure) { ++ net.minecraft.world.level.levelgen.structure.Structure vanillaStructure = this.world.registryAccess() ++ .registryOrThrow(net.minecraft.core.registries.Registries.STRUCTURE) ++ .getHolder(CraftNamespacedKey.toMinecraft(structure.getStructureType().getKey())) ++ .orElseThrow() ++ .value(); ++ return this.world.structureManager().getStructureWithPieceAt( ++ io.papermc.paper.util.MCUtil.toBlockPos(position), ++ vanillaStructure ++ ).isValid(); ++ } ++ // Paper end + + private static final Random rand = new Random(); + diff --git a/patches/server/0939-Don-t-fire-sync-events-during-worldgen.patch b/patches/server/0939-Don-t-fire-sync-events-during-worldgen.patch deleted file mode 100644 index 492d95271a..0000000000 --- a/patches/server/0939-Don-t-fire-sync-events-during-worldgen.patch +++ /dev/null @@ -1,208 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Thu, 23 Nov 2023 10:33:25 -0800 -Subject: [PATCH] Don't fire sync events during worldgen - -Fixes EntityPotionEffectEvent -Fixes EntityPoseChangeEvent - -Asynchronous chunk generation provides an opportunity for things -to happen async that previously fired synchronous-only events. This -patch is for mitigating those issues by various methods. - -Also fixes correctly marking/clearing the entity generation flag. -This patch sets the generation flag anytime an entity is created -via StructureTemplate before loading from NBT to catch uses of -the flag during the loading logic. This patch clears the generation -flag from an entity when added to a ServerLevel for the situation -where generation happened directly to a ServerLevel and the -entity still has the flag set. - -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 601ddb8a2ed1559a3c439c1aca49f772ffdd092a..6e7f98fd7b43b46171c3cd58ed91b53960bd0d8a 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -1232,6 +1232,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - // CraftBukkit start - private boolean addEntity(Entity entity, CreatureSpawnEvent.SpawnReason spawnReason) { - org.spigotmc.AsyncCatcher.catchOp("entity add"); // Spigot -+ entity.generation = false; // Paper - Don't fire sync event during generation; Reset flag if it was added during a ServerLevel generation process - // Paper start - extra debug info - if (entity.valid) { - MinecraftServer.LOGGER.error("Attempted Double World add on {}", entity, new Throwable()); -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index acf2cc8afaf4dee193ecf3bfc4a39b8c39ffc376..ee38f76e46c20e9fadfb5d4d602b7b82a8a22e51 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -650,7 +650,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - if (pose == this.getPose()) { - return; - } -- this.level.getCraftServer().getPluginManager().callEvent(new EntityPoseChangeEvent(this.getBukkitEntity(), Pose.values()[pose.ordinal()])); -+ // Paper start - Don't fire sync event during generation -+ if (!this.generation) { -+ this.level.getCraftServer().getPluginManager().callEvent(new EntityPoseChangeEvent(this.getBukkitEntity(), Pose.values()[pose.ordinal()])); -+ } -+ // Paper end - Don't fire sync event during generation - // CraftBukkit end - this.entityData.set(Entity.DATA_POSE, pose); - } -diff --git a/src/main/java/net/minecraft/world/entity/EntityType.java b/src/main/java/net/minecraft/world/entity/EntityType.java -index d769dc472d3f12bf1e30ddd767a76659b734caac..8c7cc9c5af0b8d8bef9b6e2d3d3e723cd76f3212 100644 ---- a/src/main/java/net/minecraft/world/entity/EntityType.java -+++ b/src/main/java/net/minecraft/world/entity/EntityType.java -@@ -592,9 +592,15 @@ public class EntityType implements FeatureElement, EntityTypeT - } - - public static Optional create(CompoundTag nbt, Level world) { -+ // Paper start - Don't fire sync event during generation -+ return create(nbt, world, false); -+ } -+ public static Optional create(CompoundTag nbt, Level world, boolean generation) { -+ // Paper end - Don't fire sync event during generation - return Util.ifElse(EntityType.by(nbt).map((entitytypes) -> { - return entitytypes.create(world); - }), (entity) -> { -+ if (generation) entity.generation = true; // Paper - Don't fire sync event during generation - entity.load(nbt); - }, () -> { - EntityType.LOGGER.warn("Skipping Entity with id {}", nbt.getString("id")); -diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index a8b168f56c74feeaca55335731b1d485289736cb..281d2f3df347e911eee459e06b917a0e075078e9 100644 ---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -1163,6 +1163,11 @@ public abstract class LivingEntity extends Entity implements Attackable { - } - - public boolean addEffect(MobEffectInstance mobeffect, @Nullable Entity entity, EntityPotionEffectEvent.Cause cause) { -+ // Paper start - Don't fire sync event during generation -+ return this.addEffect(mobeffect, entity, cause, true); -+ } -+ public boolean addEffect(MobEffectInstance mobeffect, @Nullable Entity entity, EntityPotionEffectEvent.Cause cause, boolean fireEvent) { -+ // Paper end - Don't fire sync event during generation - // org.spigotmc.AsyncCatcher.catchOp("effect add"); // Spigot // Paper - move to API - if (this.isTickingEffects) { - this.effectsToProcess.add(new ProcessableEffect(mobeffect, cause)); -@@ -1182,10 +1187,13 @@ public abstract class LivingEntity extends Entity implements Attackable { - override = new MobEffectInstance(mobeffect1).update(mobeffect); - } - -+ if (fireEvent) { // Paper - Don't fire sync event during generation - EntityPotionEffectEvent event = CraftEventFactory.callEntityPotionEffectChangeEvent(this, mobeffect1, mobeffect, cause, override); -+ override = event.isOverride(); // Paper - Don't fire sync event during generation - if (event.isCancelled()) { - return false; - } -+ } // Paper - Don't fire sync event during generation - // CraftBukkit end - - if (mobeffect1 == null) { -@@ -1194,7 +1202,7 @@ public abstract class LivingEntity extends Entity implements Attackable { - flag = true; - mobeffect.onEffectAdded(this); - // CraftBukkit start -- } else if (event.isOverride()) { -+ } else if (override) { // Paper - Don't fire sync event during generation - mobeffect1.update(mobeffect); - this.onEffectUpdated(mobeffect1, true, entity); - // CraftBukkit end -diff --git a/src/main/java/net/minecraft/world/entity/monster/Spider.java b/src/main/java/net/minecraft/world/entity/monster/Spider.java -index 6006480d9f6d60bb7b5628eabe6740013066cde4..f0127f1b55999aa4a841341ad02cbcde45702b50 100644 ---- a/src/main/java/net/minecraft/world/entity/monster/Spider.java -+++ b/src/main/java/net/minecraft/world/entity/monster/Spider.java -@@ -172,7 +172,7 @@ public class Spider extends Monster { - Holder holder = entityspider_groupdataspider.effect; - - if (holder != null) { -- this.addEffect(new MobEffectInstance(holder, -1), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.SPIDER_SPAWN); // CraftBukkit -+ this.addEffect(new MobEffectInstance(holder, -1), null, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.SPIDER_SPAWN, world instanceof net.minecraft.server.level.ServerLevel); // CraftBukkit - } - } - -diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate.java b/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate.java -index b5365dce7882c98b5be4f5df877165eee80933dd..9b4e2f96299bb12af7090ef70ac158c825d49d19 100644 ---- a/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate.java -+++ b/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate.java -@@ -525,7 +525,7 @@ public class StructureTemplate { - private static Optional createEntityIgnoreException(ServerLevelAccessor world, CompoundTag nbt) { - // CraftBukkit start - // try { -- return EntityType.create(nbt, world.getLevel()); -+ return EntityType.create(nbt, world.getLevel(), true); // Paper - Don't fire sync event during generation - // } catch (Exception exception) { - // return Optional.empty(); - // } -diff --git a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java -index 0794d92c42b0db6b367505ae28f09f1fd39fa312..cd7f1309cf01a5f01a28aded03a36fe15adb1756 100644 ---- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java -+++ b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java -@@ -92,15 +92,17 @@ public abstract class DelegatedGeneratorAccess implements WorldGenLevel { - return this.handle.getLevel(); - } - -- @Override -- public void addFreshEntityWithPassengers(Entity arg0, CreatureSpawnEvent.SpawnReason arg1) { -- this.handle.addFreshEntityWithPassengers(arg0, arg1); -- } -- -- @Override -- public void addFreshEntityWithPassengers(Entity entity) { -- this.handle.addFreshEntityWithPassengers(entity); -- } -+ // Paper start - Don't fire sync event during generation; don't override these methods so all entities are run through addFreshEntity -+ // @Override -+ // public void addFreshEntityWithPassengers(Entity arg0, CreatureSpawnEvent.SpawnReason arg1) { -+ // this.handle.addFreshEntityWithPassengers(arg0, arg1); -+ // } -+ // -+ // @Override -+ // public void addFreshEntityWithPassengers(Entity entity) { -+ // this.handle.addFreshEntityWithPassengers(entity); -+ // } -+ // Paper end - Don't fire sync event during generation; don't override these methods - - @Override - public ServerLevel getMinecraftWorld() { -diff --git a/src/main/java/org/bukkit/craftbukkit/util/TransformerGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/TransformerGeneratorAccess.java -index 35ecf6f824aca56a20280dd683123df1d0c7d66e..1d1fdcf10498c421f106158254e052da6d68d8a5 100644 ---- a/src/main/java/org/bukkit/craftbukkit/util/TransformerGeneratorAccess.java -+++ b/src/main/java/org/bukkit/craftbukkit/util/TransformerGeneratorAccess.java -@@ -39,21 +39,23 @@ public class TransformerGeneratorAccess extends DelegatedGeneratorAccess { - return super.addFreshEntity(arg0, arg1); - } - -- @Override -- public void addFreshEntityWithPassengers(Entity entity) { -- if (this.structureTransformer != null && !this.structureTransformer.transformEntity(entity)) { -- return; -- } -- super.addFreshEntityWithPassengers(entity); -- } -- -- @Override -- public void addFreshEntityWithPassengers(Entity arg0, SpawnReason arg1) { -- if (this.structureTransformer != null && !this.structureTransformer.transformEntity(arg0)) { -- return; -- } -- super.addFreshEntityWithPassengers(arg0, arg1); -- } -+ // Paper start - Don't fire sync event during generation; don't override these methods so all entities are run through addFreshEntity -+ // @Override -+ // public void addFreshEntityWithPassengers(Entity entity) { -+ // if (this.structureTransformer != null && !this.structureTransformer.transformEntity(entity)) { -+ // return; -+ // } -+ // super.addFreshEntityWithPassengers(entity); -+ // } -+ // -+ // @Override -+ // public void addFreshEntityWithPassengers(Entity arg0, SpawnReason arg1) { -+ // if (this.structureTransformer != null && !this.structureTransformer.transformEntity(arg0)) { -+ // return; -+ // } -+ // super.addFreshEntityWithPassengers(arg0, arg1); -+ // } -+ // Paper end - Don't fire sync event during generation; don't override these methods - - public boolean setCraftBlock(BlockPos position, CraftBlockState craftBlockState, int i, int j) { - if (this.structureTransformer != null) { diff --git a/patches/server/0940-Add-Structure-check-API.patch b/patches/server/0940-Add-Structure-check-API.patch deleted file mode 100644 index b1501eb55b..0000000000 --- a/patches/server/0940-Add-Structure-check-API.patch +++ /dev/null @@ -1,31 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Mon, 27 Mar 2023 10:20:00 -0700 -Subject: [PATCH] Add Structure check API - - -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index 039efd0b8eb141de044668a5633f26eee238c2b0..4a62482b7c73728de8a94542e8978e836540b328 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -@@ -237,6 +237,20 @@ public class CraftWorld extends CraftRegionAccessor implements World { - }; - } - // Paper end -+ // Paper start - structure check API -+ @Override -+ public boolean hasStructureAt(final io.papermc.paper.math.Position position, final Structure structure) { -+ net.minecraft.world.level.levelgen.structure.Structure vanillaStructure = this.world.registryAccess() -+ .registryOrThrow(net.minecraft.core.registries.Registries.STRUCTURE) -+ .getHolder(CraftNamespacedKey.toMinecraft(structure.getStructureType().getKey())) -+ .orElseThrow() -+ .value(); -+ return this.world.structureManager().getStructureWithPieceAt( -+ io.papermc.paper.util.MCUtil.toBlockPos(position), -+ vanillaStructure -+ ).isValid(); -+ } -+ // Paper end - - private static final Random rand = new Random(); - diff --git a/patches/server/0940-Fix-CraftMetaItem-getAttributeModifier-duplication-c.patch b/patches/server/0940-Fix-CraftMetaItem-getAttributeModifier-duplication-c.patch new file mode 100644 index 0000000000..8568cfc5bf --- /dev/null +++ b/patches/server/0940-Fix-CraftMetaItem-getAttributeModifier-duplication-c.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: hyper1423 +Date: Sun, 3 Dec 2023 07:38:09 +0900 +Subject: [PATCH] Fix CraftMetaItem#getAttributeModifier duplication check + + +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java +index a3ae3203e04dfb791b8aa537a303f4a4bd6cbab6..549d06b83b195c0a0207d18850b98d1e6e611460 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java +@@ -1255,7 +1255,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { + Preconditions.checkNotNull(modifier, "AttributeModifier cannot be null"); + this.checkAttributeList(); + for (Map.Entry entry : this.attributeModifiers.entries()) { +- Preconditions.checkArgument(!entry.getValue().getUniqueId().equals(modifier.getUniqueId()), "Cannot register AttributeModifier. Modifier is already applied! %s", modifier); ++ Preconditions.checkArgument(!(entry.getValue().getUniqueId().equals(modifier.getUniqueId()) && entry.getKey() == attribute), "Cannot register AttributeModifier. Modifier is already applied! %s", modifier); // Paper + } + return this.attributeModifiers.put(attribute, modifier); + } diff --git a/patches/server/0941-Fix-CraftMetaItem-getAttributeModifier-duplication-c.patch b/patches/server/0941-Fix-CraftMetaItem-getAttributeModifier-duplication-c.patch deleted file mode 100644 index 8568cfc5bf..0000000000 --- a/patches/server/0941-Fix-CraftMetaItem-getAttributeModifier-duplication-c.patch +++ /dev/null @@ -1,19 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: hyper1423 -Date: Sun, 3 Dec 2023 07:38:09 +0900 -Subject: [PATCH] Fix CraftMetaItem#getAttributeModifier duplication check - - -diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java -index a3ae3203e04dfb791b8aa537a303f4a4bd6cbab6..549d06b83b195c0a0207d18850b98d1e6e611460 100644 ---- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java -+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java -@@ -1255,7 +1255,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { - Preconditions.checkNotNull(modifier, "AttributeModifier cannot be null"); - this.checkAttributeList(); - for (Map.Entry entry : this.attributeModifiers.entries()) { -- Preconditions.checkArgument(!entry.getValue().getUniqueId().equals(modifier.getUniqueId()), "Cannot register AttributeModifier. Modifier is already applied! %s", modifier); -+ Preconditions.checkArgument(!(entry.getValue().getUniqueId().equals(modifier.getUniqueId()) && entry.getKey() == attribute), "Cannot register AttributeModifier. Modifier is already applied! %s", modifier); // Paper - } - return this.attributeModifiers.put(attribute, modifier); - } diff --git a/patches/server/0941-Restore-vanilla-entity-drops-behavior.patch b/patches/server/0941-Restore-vanilla-entity-drops-behavior.patch new file mode 100644 index 0000000000..edb6769a27 --- /dev/null +++ b/patches/server/0941-Restore-vanilla-entity-drops-behavior.patch @@ -0,0 +1,243 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Tue, 22 Mar 2022 09:34:41 -0700 +Subject: [PATCH] Restore vanilla entity drops behavior + +Instead of just tracking the itemstacks, this tracks with it, the +action to take with that itemstack to apply the correct logic +on dropping the item instead of generalizing it for all dropped +items like CB does. + +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index b2bf086506ba7eda6ba6912ebf51a9da154cd414..bee8cc19d7912d2ab1b731c47cf7e31d21d61866 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -958,22 +958,20 @@ public class ServerPlayer extends Player { + if (this.isRemoved()) { + return; + } +- java.util.List loot = new java.util.ArrayList(this.getInventory().getContainerSize()); ++ List loot = new java.util.ArrayList<>(this.getInventory().getContainerSize()); // Paper - Restore vanilla drops behavior + boolean keepInventory = this.level().getGameRules().getBoolean(GameRules.RULE_KEEPINVENTORY) || this.isSpectator(); + + if (!keepInventory) { + for (ItemStack item : this.getInventory().getContents()) { + if (!item.isEmpty() && !EnchantmentHelper.hasVanishingCurse(item)) { +- loot.add(CraftItemStack.asCraftMirror(item)); ++ loot.add(new DefaultDrop(item, stack -> this.drop(stack, true, false, false))); // Paper - Restore vanilla drops behavior; drop function taken from Inventory#dropAll (don't fire drop event) + } + } + } + if (this.shouldDropLoot() && this.level().getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) { // Paper - fix player loottables running when mob loot gamerule is false + // SPIGOT-5071: manually add player loot tables (SPIGOT-5195 - ignores keepInventory rule) + this.dropFromLootTable(damageSource, this.lastHurtByPlayerTime > 0); +- for (org.bukkit.inventory.ItemStack item : this.drops) { +- loot.add(item); +- } ++ loot.addAll(this.drops); // Paper + this.drops.clear(); // SPIGOT-5188: make sure to clear + } // Paper - fix player loottables running when mob loot gamerule is false + +@@ -2471,8 +2469,8 @@ public class ServerPlayer extends Player { + } + + @Override +- public ItemEntity drop(ItemStack stack, boolean throwRandomly, boolean retainOwnership) { +- ItemEntity entityitem = super.drop(stack, throwRandomly, retainOwnership); ++ public ItemEntity drop(ItemStack stack, boolean throwRandomly, boolean retainOwnership, boolean callDropEvent) { // Paper - Restore vanilla drops behavior; override method with most params ++ ItemEntity entityitem = super.drop(stack, throwRandomly, retainOwnership, callDropEvent); // Paper - Restore vanilla drops behavior; override method with most params + + if (entityitem == null) { + return null; +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index ee38f76e46c20e9fadfb5d4d602b7b82a8a22e51..4e96b6c05bf8b8235e91bdd26e5615c5299fd9aa 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -2524,6 +2524,25 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + + @Nullable + public ItemEntity spawnAtLocation(ItemStack stack, float yOffset) { ++ // Paper start - Restore vanilla drops behavior ++ return this.spawnAtLocation(stack, yOffset, null); ++ } ++ public record DefaultDrop(Item item, org.bukkit.inventory.ItemStack stack, @Nullable java.util.function.Consumer dropConsumer) { ++ public DefaultDrop(final ItemStack stack, final java.util.function.Consumer dropConsumer) { ++ this(stack.getItem(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack), dropConsumer); ++ } ++ ++ public void runConsumer(final org.bukkit.World fallbackWorld, final Location fallbackLoc) { ++ if (this.dropConsumer == null || org.bukkit.craftbukkit.inventory.CraftItemType.bukkitToMinecraft(this.stack.getType()) != this.item) { ++ fallbackWorld.dropItem(fallbackLoc, this.stack); ++ } else { ++ this.dropConsumer.accept(org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(this.stack)); ++ } ++ } ++ } ++ @Nullable ++ public ItemEntity spawnAtLocation(ItemStack stack, float yOffset, @Nullable java.util.function.Consumer delayedAddConsumer) { ++ // Paper end - Restore vanilla drops behavior + if (stack.isEmpty()) { + return null; + } else if (this.level().isClientSide) { +@@ -2531,14 +2550,21 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } else { + // CraftBukkit start - Capture drops for death event + if (this instanceof net.minecraft.world.entity.LivingEntity && !((net.minecraft.world.entity.LivingEntity) this).forceDrops) { +- ((net.minecraft.world.entity.LivingEntity) this).drops.add(org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack)); // Paper - mirror so we can destroy it later ++ // Paper start - Restore vanilla drops behavior ++ ((net.minecraft.world.entity.LivingEntity) this).drops.add(new net.minecraft.world.entity.Entity.DefaultDrop(stack, itemStack -> { ++ ItemEntity itemEntity = new ItemEntity(this.level, this.getX(), this.getY() + (double) yOffset, this.getZ(), itemStack); // stack is copied before consumer ++ itemEntity.setDefaultPickUpDelay(); ++ this.level.addFreshEntity(itemEntity); ++ if (delayedAddConsumer != null) delayedAddConsumer.accept(itemEntity); ++ })); ++ // Paper end - Restore vanilla drops behavior + return null; + } + // CraftBukkit end + ItemEntity entityitem = new ItemEntity(this.level(), this.getX(), this.getY() + (double) yOffset, this.getZ(), stack.copy()); // Paper - copy so we can destroy original + stack.setCount(0); // Paper - destroy this item - if this ever leaks due to game bugs, ensure it doesn't dupe + +- entityitem.setDefaultPickUpDelay(); ++ entityitem.setDefaultPickUpDelay(); // Paper - diff on change (in dropConsumer) + // Paper start - Call EntityDropItemEvent + return this.spawnAtLocation(entityitem); + } +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index 281d2f3df347e911eee459e06b917a0e075078e9..f948f4051607611e7105790c3b6bacc5d3ec39db 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -266,7 +266,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + // CraftBukkit start + public int expToDrop; + public boolean forceDrops; +- public ArrayList drops = new ArrayList(); ++ public ArrayList drops = new ArrayList<>(); // Paper - Restore vanilla drops behavior + public final org.bukkit.craftbukkit.attribute.CraftAttributeMap craftAttributes; + public boolean collides = true; + public Set collidableExemptions = new HashSet<>(); +diff --git a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java +index aa22829c3d41118664a872540fdc8f716120c407..c23d4ee0a16d1ae7168b2496d97189a14256bdcc 100644 +--- a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java ++++ b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java +@@ -537,10 +537,10 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob + @Override + protected void dropCustomDeathLoot(DamageSource source, int lootingMultiplier, boolean allowDrops) { + super.dropCustomDeathLoot(source, lootingMultiplier, allowDrops); +- ItemEntity entityitem = this.spawnAtLocation((ItemLike) Items.NETHER_STAR); ++ ItemEntity entityitem = this.spawnAtLocation(new net.minecraft.world.item.ItemStack(Items.NETHER_STAR), 0, ItemEntity::setExtendedLifetime); // Paper - Restore vanilla drops behavior; spawnAtLocation returns null so modify the item entity with a consumer + + if (entityitem != null) { +- entityitem.setExtendedLifetime(); ++ entityitem.setExtendedLifetime(); // Paper - diff on change + } + + } +diff --git a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java +index b1b0b0554e0ad035463fd53aa440d9c079b7cfb4..2ed6845f16fab175e2e9e96e76391e63ab4a43e2 100644 +--- a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java ++++ b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java +@@ -614,7 +614,7 @@ public class ArmorStand extends LivingEntity { + ItemStack itemstack = new ItemStack(Items.ARMOR_STAND); + + itemstack.set(DataComponents.CUSTOM_NAME, this.getCustomName()); +- this.drops.add(org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(itemstack)); // CraftBukkit - add to drops ++ this.drops.add(new DefaultDrop(itemstack, stack -> Block.popResource(this.level(), this.blockPosition(), stack))); // CraftBukkit - add to drops // Paper - Restore vanilla drops behavior + return this.brokenByAnything(damageSource); // Paper + } + +@@ -628,7 +628,7 @@ public class ArmorStand extends LivingEntity { + for (i = 0; i < this.handItems.size(); ++i) { + itemstack = (ItemStack) this.handItems.get(i); + if (!itemstack.isEmpty()) { +- this.drops.add(org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack)); // CraftBukkit - add to drops // Paper - mirror so we can destroy it later - though this call site was safe ++ this.drops.add(new DefaultDrop(itemstack, stack -> Block.popResource(this.level(), this.blockPosition().above(), stack))); // CraftBukkit - add to drops // Paper - Restore vanilla drops behavior; mirror so we can destroy it later - though this call site was safe & spawn drops correctly + this.handItems.set(i, ItemStack.EMPTY); + } + } +@@ -636,7 +636,7 @@ public class ArmorStand extends LivingEntity { + for (i = 0; i < this.armorItems.size(); ++i) { + itemstack = (ItemStack) this.armorItems.get(i); + if (!itemstack.isEmpty()) { +- this.drops.add(org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack)); // CraftBukkit - add to drops // Paper - mirror so we can destroy it later - though this call site was safe ++ this.drops.add(new DefaultDrop(itemstack, stack -> Block.popResource(this.level(), this.blockPosition().above(), stack))); // CraftBukkit - add to drops // Paper - Restore vanilla drops behavior; mirror so we can destroy it later - though this call site was safe & spawn drops correctly + this.armorItems.set(i, ItemStack.EMPTY); + } + } +diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +index eefdb3da069d9615db7c663bcf8a72b78a0e6817..822bfebc779646dde655239ac217bad011a9eee4 100644 +--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java ++++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +@@ -964,17 +964,23 @@ public class CraftEventFactory { + } + + public static EntityDeathEvent callEntityDeathEvent(net.minecraft.world.entity.LivingEntity victim) { +- return CraftEventFactory.callEntityDeathEvent(victim, new ArrayList(0)); ++ return CraftEventFactory.callEntityDeathEvent(victim, new ArrayList<>(0)); // Paper - Restore vanilla drops behavior + } + +- public static EntityDeathEvent callEntityDeathEvent(net.minecraft.world.entity.LivingEntity victim, List drops) { ++ public static EntityDeathEvent callEntityDeathEvent(net.minecraft.world.entity.LivingEntity victim, List drops) { // Paper - Restore vanilla drops behavior + // Paper start + return CraftEventFactory.callEntityDeathEvent(victim, drops, com.google.common.util.concurrent.Runnables.doNothing()); + } +- public static EntityDeathEvent callEntityDeathEvent(net.minecraft.world.entity.LivingEntity victim, List drops, Runnable lootCheck) { ++ ++ private static final java.util.function.Function FROM_FUNCTION = stack -> { ++ if (stack == null) return null; ++ return new Entity.DefaultDrop(CraftItemType.bukkitToMinecraft(stack.getType()), stack, null); ++ }; ++ ++ public static EntityDeathEvent callEntityDeathEvent(net.minecraft.world.entity.LivingEntity victim, List drops, Runnable lootCheck) { // Paper + // Paper end + CraftLivingEntity entity = (CraftLivingEntity) victim.getBukkitEntity(); +- EntityDeathEvent event = new EntityDeathEvent(entity, drops, victim.getExpReward()); ++ EntityDeathEvent event = new EntityDeathEvent(entity, new io.papermc.paper.util.TransformingRandomAccessList<>(drops, Entity.DefaultDrop::stack, FROM_FUNCTION), victim.getExpReward()); // Paper - Restore vanilla drops behavior + populateFields(victim, event); // Paper - make cancellable + CraftWorld world = (CraftWorld) entity.getWorld(); + Bukkit.getServer().getPluginManager().callEvent(event); +@@ -988,19 +994,23 @@ public class CraftEventFactory { + victim.expToDrop = event.getDroppedExp(); + lootCheck.run(); // Paper - advancement triggers before destroying items + +- for (org.bukkit.inventory.ItemStack stack : event.getDrops()) { ++ // Paper start - Restore vanilla drops behavior ++ for (Entity.DefaultDrop drop : drops) { ++ if (drop == null) continue; ++ final org.bukkit.inventory.ItemStack stack = drop.stack(); ++ // Paper end - Restore vanilla drops behavior + if (stack == null || stack.getType() == Material.AIR || stack.getAmount() == 0) continue; + +- world.dropItem(entity.getLocation(), stack); // Paper - note: dropItem already clones due to this being bukkit -> NMS ++ drop.runConsumer(world, entity.getLocation()); // Paper - Restore vanilla drops behavior + if (stack instanceof CraftItemStack) stack.setAmount(0); // Paper - destroy this item - if this ever leaks due to game bugs, ensure it doesn't dupe, but don't nuke bukkit stacks of manually added items + } + + return event; + } + +- public static PlayerDeathEvent callPlayerDeathEvent(ServerPlayer victim, List drops, net.kyori.adventure.text.Component deathMessage, boolean keepInventory) { // Paper - Adventure ++ public static PlayerDeathEvent callPlayerDeathEvent(ServerPlayer victim, List drops, net.kyori.adventure.text.Component deathMessage, boolean keepInventory) { // Paper - Adventure & Restore vanilla drops behavior + CraftPlayer entity = victim.getBukkitEntity(); +- PlayerDeathEvent event = new PlayerDeathEvent(entity, drops, victim.getExpReward(), 0, deathMessage); ++ PlayerDeathEvent event = new PlayerDeathEvent(entity, new io.papermc.paper.util.TransformingRandomAccessList<>(drops, Entity.DefaultDrop::stack, FROM_FUNCTION), victim.getExpReward(), 0, deathMessage); // Paper - Restore vanilla drops behavior + event.setKeepInventory(keepInventory); + event.setKeepLevel(victim.keepLevel); // SPIGOT-2222: pre-set keepLevel + populateFields(victim, event); // Paper - make cancellable +@@ -1019,10 +1029,14 @@ public class CraftEventFactory { + victim.expToDrop = event.getDroppedExp(); + victim.newExp = event.getNewExp(); + +- for (org.bukkit.inventory.ItemStack stack : event.getDrops()) { ++ // Paper start - Restore vanilla drops behavior ++ for (Entity.DefaultDrop drop : drops) { ++ if (drop == null) continue; ++ final org.bukkit.inventory.ItemStack stack = drop.stack(); ++ // Paper end - Restore vanilla drops behavior + if (stack == null || stack.getType() == Material.AIR) continue; + +- world.dropItem(entity.getLocation(), stack); ++ drop.runConsumer(world, entity.getLocation()); // Paper - Restore vanilla drops behavior + } + + return event; diff --git a/patches/server/0942-Dont-resend-blocks-on-interactions.patch b/patches/server/0942-Dont-resend-blocks-on-interactions.patch new file mode 100644 index 0000000000..384e9d45a5 --- /dev/null +++ b/patches/server/0942-Dont-resend-blocks-on-interactions.patch @@ -0,0 +1,171 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> +Date: Tue, 27 Jun 2023 21:09:11 -0400 +Subject: [PATCH] Dont resend blocks on interactions + +In general, the client now has an acknowledgment system which will prevent block changes made by the client to be reverted correctly. + +It should be noted that this system does not yet support block entities, so those still need to resynced when needed. + +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +index cef3a64b5e6c4d34ed079078420078a7adfcd574..3dff328c5e4456aae291a8dec82766681f49af06 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +@@ -202,7 +202,7 @@ public class ServerPlayerGameMode { + PlayerInteractEvent event = CraftEventFactory.callPlayerInteractEvent(this.player, Action.LEFT_CLICK_BLOCK, pos, direction, this.player.getInventory().getSelected(), InteractionHand.MAIN_HAND); + if (event.isCancelled()) { + // Let the client know the block still exists +- this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); ++ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); // Paper - Don't resync blocks + // Update any tile entity data for this block + capturedBlockEntity = true; // Paper - Send block entities after destroy prediction + return; +@@ -217,7 +217,7 @@ public class ServerPlayerGameMode { + // Spigot start - handle debug stick left click for non-creative + if (this.player.getMainHandItem().is(net.minecraft.world.item.Items.DEBUG_STICK) + && ((net.minecraft.world.item.DebugStickItem) net.minecraft.world.item.Items.DEBUG_STICK).handleInteraction(this.player, this.level.getBlockState(pos), this.level, pos, false, this.player.getMainHandItem())) { +- this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); ++ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); // Paper - Don't resync block + return; + } + // Spigot end +@@ -235,15 +235,17 @@ public class ServerPlayerGameMode { + // CraftBukkit start - Swings at air do *NOT* exist. + if (event.useInteractedBlock() == Event.Result.DENY) { + // If we denied a door from opening, we need to send a correcting update to the client, as it already opened the door. +- BlockState data = this.level.getBlockState(pos); +- if (data.getBlock() instanceof DoorBlock) { +- // For some reason *BOTH* the bottom/top part have to be marked updated. +- boolean bottom = data.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER; +- this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); +- this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, bottom ? pos.above() : pos.below())); +- } else if (data.getBlock() instanceof TrapDoorBlock) { +- this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); +- } ++ // Paper start - Don't resync blocks ++ //BlockState data = this.level.getBlockState(pos); ++ //if (data.getBlock() instanceof DoorBlock) { ++ // // For some reason *BOTH* the bottom/top part have to be marked updated. ++ // boolean bottom = data.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER; ++ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); ++ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, bottom ? pos.above() : pos.below())); ++ //} else if (data.getBlock() instanceof TrapDoorBlock) { ++ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); ++ //} ++ // Paper end - Don't resync blocks + } else if (!iblockdata.isAir()) { + iblockdata.attack(this.level, pos, this.player); + f = iblockdata.getDestroyProgress(this.player, this.player.level(), pos); +@@ -252,7 +254,7 @@ public class ServerPlayerGameMode { + if (event.useItemInHand() == Event.Result.DENY) { + // If we 'insta destroyed' then the client needs to be informed. + if (f > 1.0f) { +- this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); ++ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); // Paper - Don't resync blocks + } + return; + } +@@ -260,7 +262,7 @@ public class ServerPlayerGameMode { + + if (blockEvent.isCancelled()) { + // Let the client know the block still exists +- this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); ++ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); // Paper - Don't resync block + return; + } + +@@ -351,7 +353,7 @@ public class ServerPlayerGameMode { + + // Tell client the block is gone immediately then process events + // Don't tell the client if its a creative sword break because its not broken! +- if (this.level.getBlockEntity(pos) == null && !isSwordNoBreak) { ++ if (false && this.level.getBlockEntity(pos) == null && !isSwordNoBreak) { // Paper - Don't resync block + ClientboundBlockUpdatePacket packet = new ClientboundBlockUpdatePacket(pos, Blocks.AIR.defaultBlockState()); + this.player.connection.send(packet); + } +@@ -377,13 +379,15 @@ public class ServerPlayerGameMode { + if (isSwordNoBreak) { + return false; + } ++ // Paper start - Don't resync blocks + // Let the client know the block still exists +- this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); ++ //this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); + + // Brute force all possible updates +- for (Direction dir : Direction.values()) { +- this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos.relative(dir))); +- } ++ //for (Direction dir : Direction.values()) { ++ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos.relative(dir))); ++ //} ++ // Paper end - Don't resync blocks + + // Update any tile entity data for this block + if (!captureSentBlockEntities) { // Paper - Send block entities after destroy prediction +@@ -532,16 +536,18 @@ public class ServerPlayerGameMode { + if (event.useInteractedBlock() == Event.Result.DENY) { + // If we denied a door from opening, we need to send a correcting update to the client, as it already opened the door. + if (iblockdata.getBlock() instanceof DoorBlock) { +- boolean bottom = iblockdata.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER; +- player.connection.send(new ClientboundBlockUpdatePacket(world, bottom ? blockposition.above() : blockposition.below())); ++ // Paper start - Don't resync blocks ++ // boolean bottom = iblockdata.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER; ++ // player.connection.send(new ClientboundBlockUpdatePacket(world, bottom ? blockposition.above() : blockposition.below())); ++ // Paper end - Don't resync blocks + } else if (iblockdata.getBlock() instanceof CakeBlock) { + player.getBukkitEntity().sendHealthUpdate(); // SPIGOT-1341 - reset health for cake + } else if (this.interactItemStack.getItem() instanceof DoubleHighBlockItem) { + // send a correcting update to the client, as it already placed the upper half of the bisected item +- player.connection.send(new ClientboundBlockUpdatePacket(world, blockposition.relative(hitResult.getDirection()).above())); ++ //player.connection.send(new ClientboundBlockUpdatePacket(world, blockposition.relative(hitResult.getDirection()).above())); // Paper - Don't resync blocks + + // send a correcting update to the client for the block above as well, this because of replaceable blocks (such as grass, sea grass etc) +- player.connection.send(new ClientboundBlockUpdatePacket(world, blockposition.above())); ++ //player.connection.send(new ClientboundBlockUpdatePacket(world, blockposition.above())); // Paper - Don't resync blocks + // Paper start - extend Player Interact cancellation // TODO: consider merging this into the extracted method + } else if (iblockdata.is(Blocks.STRUCTURE_BLOCK) || iblockdata.getBlock() instanceof net.minecraft.world.level.block.CommandBlock) { + player.connection.send(new net.minecraft.network.protocol.game.ClientboundContainerClosePacket(this.player.containerMenu.containerId)); +diff --git a/src/main/java/net/minecraft/world/item/BucketItem.java b/src/main/java/net/minecraft/world/item/BucketItem.java +index 6d494b80ae002aea00afa44adf83dad1ae5bbbc1..49557d6f22c5725c663a231deab019d4f6fe95fa 100644 +--- a/src/main/java/net/minecraft/world/item/BucketItem.java ++++ b/src/main/java/net/minecraft/world/item/BucketItem.java +@@ -78,7 +78,7 @@ public class BucketItem extends Item implements DispensibleContainerItem { + PlayerBucketFillEvent event = CraftEventFactory.callPlayerBucketFillEvent((ServerLevel) world, user, blockposition, blockposition, movingobjectpositionblock.getDirection(), itemstack, dummyFluid.getItem(), hand); + + if (event.isCancelled()) { +- ((ServerPlayer) user).connection.send(new ClientboundBlockUpdatePacket(world, blockposition)); // SPIGOT-5163 (see PlayerInteractManager) ++ // ((ServerPlayer) user).connection.send(new ClientboundBlockUpdatePacket(world, blockposition)); // SPIGOT-5163 (see PlayerInteractManager) // Paper - Don't resend blocks + ((ServerPlayer) user).getBukkitEntity().updateInventory(); // SPIGOT-4541 + return InteractionResultHolder.fail(itemstack); + } +@@ -185,7 +185,7 @@ public class BucketItem extends Item implements DispensibleContainerItem { + if (flag2 && entityhuman != null) { + PlayerBucketEmptyEvent event = CraftEventFactory.callPlayerBucketEmptyEvent((ServerLevel) world, entityhuman, blockposition, clicked, enumdirection, itemstack, enumhand); + if (event.isCancelled()) { +- ((ServerPlayer) entityhuman).connection.send(new ClientboundBlockUpdatePacket(world, blockposition)); // SPIGOT-4238: needed when looking through entity ++ // ((ServerPlayer) entityhuman).connection.send(new ClientboundBlockUpdatePacket(world, blockposition)); // SPIGOT-4238: needed when looking through entity // Paper - Don't resend blocks + ((ServerPlayer) entityhuman).getBukkitEntity().updateInventory(); // SPIGOT-4541 + return false; + } +diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java +index fb85a7da9da8063c2b781377f62e7db34c467859..2d2b436cae5693051df14e5777d385d38d5f4dd6 100644 +--- a/src/main/java/net/minecraft/world/item/ItemStack.java ++++ b/src/main/java/net/minecraft/world/item/ItemStack.java +@@ -480,10 +480,12 @@ public final class ItemStack implements DataComponentHolder { + world.preventPoiUpdated = false; + + // Brute force all possible updates +- BlockPos placedPos = ((CraftBlock) placeEvent.getBlock()).getPosition(); +- for (Direction dir : Direction.values()) { +- ((ServerPlayer) entityhuman).connection.send(new ClientboundBlockUpdatePacket(world, placedPos.relative(dir))); +- } ++ // Paper start - Don't resync blocks ++ // BlockPos placedPos = ((CraftBlock) placeEvent.getBlock()).getPosition(); ++ // for (Direction dir : Direction.values()) { ++ // ((ServerPlayer) entityhuman).connection.send(new ClientboundBlockUpdatePacket(world, placedPos.relative(dir))); ++ // } ++ // Paper end - Don't resync blocks + SignItem.openSign = null; // SPIGOT-6758 - Reset on early return + } else { + // Change the stack to its new contents if it hasn't been tampered with. diff --git a/patches/server/0942-Restore-vanilla-entity-drops-behavior.patch b/patches/server/0942-Restore-vanilla-entity-drops-behavior.patch deleted file mode 100644 index edb6769a27..0000000000 --- a/patches/server/0942-Restore-vanilla-entity-drops-behavior.patch +++ /dev/null @@ -1,243 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Tue, 22 Mar 2022 09:34:41 -0700 -Subject: [PATCH] Restore vanilla entity drops behavior - -Instead of just tracking the itemstacks, this tracks with it, the -action to take with that itemstack to apply the correct logic -on dropping the item instead of generalizing it for all dropped -items like CB does. - -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index b2bf086506ba7eda6ba6912ebf51a9da154cd414..bee8cc19d7912d2ab1b731c47cf7e31d21d61866 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -958,22 +958,20 @@ public class ServerPlayer extends Player { - if (this.isRemoved()) { - return; - } -- java.util.List loot = new java.util.ArrayList(this.getInventory().getContainerSize()); -+ List loot = new java.util.ArrayList<>(this.getInventory().getContainerSize()); // Paper - Restore vanilla drops behavior - boolean keepInventory = this.level().getGameRules().getBoolean(GameRules.RULE_KEEPINVENTORY) || this.isSpectator(); - - if (!keepInventory) { - for (ItemStack item : this.getInventory().getContents()) { - if (!item.isEmpty() && !EnchantmentHelper.hasVanishingCurse(item)) { -- loot.add(CraftItemStack.asCraftMirror(item)); -+ loot.add(new DefaultDrop(item, stack -> this.drop(stack, true, false, false))); // Paper - Restore vanilla drops behavior; drop function taken from Inventory#dropAll (don't fire drop event) - } - } - } - if (this.shouldDropLoot() && this.level().getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) { // Paper - fix player loottables running when mob loot gamerule is false - // SPIGOT-5071: manually add player loot tables (SPIGOT-5195 - ignores keepInventory rule) - this.dropFromLootTable(damageSource, this.lastHurtByPlayerTime > 0); -- for (org.bukkit.inventory.ItemStack item : this.drops) { -- loot.add(item); -- } -+ loot.addAll(this.drops); // Paper - this.drops.clear(); // SPIGOT-5188: make sure to clear - } // Paper - fix player loottables running when mob loot gamerule is false - -@@ -2471,8 +2469,8 @@ public class ServerPlayer extends Player { - } - - @Override -- public ItemEntity drop(ItemStack stack, boolean throwRandomly, boolean retainOwnership) { -- ItemEntity entityitem = super.drop(stack, throwRandomly, retainOwnership); -+ public ItemEntity drop(ItemStack stack, boolean throwRandomly, boolean retainOwnership, boolean callDropEvent) { // Paper - Restore vanilla drops behavior; override method with most params -+ ItemEntity entityitem = super.drop(stack, throwRandomly, retainOwnership, callDropEvent); // Paper - Restore vanilla drops behavior; override method with most params - - if (entityitem == null) { - return null; -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index ee38f76e46c20e9fadfb5d4d602b7b82a8a22e51..4e96b6c05bf8b8235e91bdd26e5615c5299fd9aa 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -2524,6 +2524,25 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - - @Nullable - public ItemEntity spawnAtLocation(ItemStack stack, float yOffset) { -+ // Paper start - Restore vanilla drops behavior -+ return this.spawnAtLocation(stack, yOffset, null); -+ } -+ public record DefaultDrop(Item item, org.bukkit.inventory.ItemStack stack, @Nullable java.util.function.Consumer dropConsumer) { -+ public DefaultDrop(final ItemStack stack, final java.util.function.Consumer dropConsumer) { -+ this(stack.getItem(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack), dropConsumer); -+ } -+ -+ public void runConsumer(final org.bukkit.World fallbackWorld, final Location fallbackLoc) { -+ if (this.dropConsumer == null || org.bukkit.craftbukkit.inventory.CraftItemType.bukkitToMinecraft(this.stack.getType()) != this.item) { -+ fallbackWorld.dropItem(fallbackLoc, this.stack); -+ } else { -+ this.dropConsumer.accept(org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(this.stack)); -+ } -+ } -+ } -+ @Nullable -+ public ItemEntity spawnAtLocation(ItemStack stack, float yOffset, @Nullable java.util.function.Consumer delayedAddConsumer) { -+ // Paper end - Restore vanilla drops behavior - if (stack.isEmpty()) { - return null; - } else if (this.level().isClientSide) { -@@ -2531,14 +2550,21 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } else { - // CraftBukkit start - Capture drops for death event - if (this instanceof net.minecraft.world.entity.LivingEntity && !((net.minecraft.world.entity.LivingEntity) this).forceDrops) { -- ((net.minecraft.world.entity.LivingEntity) this).drops.add(org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack)); // Paper - mirror so we can destroy it later -+ // Paper start - Restore vanilla drops behavior -+ ((net.minecraft.world.entity.LivingEntity) this).drops.add(new net.minecraft.world.entity.Entity.DefaultDrop(stack, itemStack -> { -+ ItemEntity itemEntity = new ItemEntity(this.level, this.getX(), this.getY() + (double) yOffset, this.getZ(), itemStack); // stack is copied before consumer -+ itemEntity.setDefaultPickUpDelay(); -+ this.level.addFreshEntity(itemEntity); -+ if (delayedAddConsumer != null) delayedAddConsumer.accept(itemEntity); -+ })); -+ // Paper end - Restore vanilla drops behavior - return null; - } - // CraftBukkit end - ItemEntity entityitem = new ItemEntity(this.level(), this.getX(), this.getY() + (double) yOffset, this.getZ(), stack.copy()); // Paper - copy so we can destroy original - stack.setCount(0); // Paper - destroy this item - if this ever leaks due to game bugs, ensure it doesn't dupe - -- entityitem.setDefaultPickUpDelay(); -+ entityitem.setDefaultPickUpDelay(); // Paper - diff on change (in dropConsumer) - // Paper start - Call EntityDropItemEvent - return this.spawnAtLocation(entityitem); - } -diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index 281d2f3df347e911eee459e06b917a0e075078e9..f948f4051607611e7105790c3b6bacc5d3ec39db 100644 ---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -266,7 +266,7 @@ public abstract class LivingEntity extends Entity implements Attackable { - // CraftBukkit start - public int expToDrop; - public boolean forceDrops; -- public ArrayList drops = new ArrayList(); -+ public ArrayList drops = new ArrayList<>(); // Paper - Restore vanilla drops behavior - public final org.bukkit.craftbukkit.attribute.CraftAttributeMap craftAttributes; - public boolean collides = true; - public Set collidableExemptions = new HashSet<>(); -diff --git a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java -index aa22829c3d41118664a872540fdc8f716120c407..c23d4ee0a16d1ae7168b2496d97189a14256bdcc 100644 ---- a/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java -+++ b/src/main/java/net/minecraft/world/entity/boss/wither/WitherBoss.java -@@ -537,10 +537,10 @@ public class WitherBoss extends Monster implements PowerableMob, RangedAttackMob - @Override - protected void dropCustomDeathLoot(DamageSource source, int lootingMultiplier, boolean allowDrops) { - super.dropCustomDeathLoot(source, lootingMultiplier, allowDrops); -- ItemEntity entityitem = this.spawnAtLocation((ItemLike) Items.NETHER_STAR); -+ ItemEntity entityitem = this.spawnAtLocation(new net.minecraft.world.item.ItemStack(Items.NETHER_STAR), 0, ItemEntity::setExtendedLifetime); // Paper - Restore vanilla drops behavior; spawnAtLocation returns null so modify the item entity with a consumer - - if (entityitem != null) { -- entityitem.setExtendedLifetime(); -+ entityitem.setExtendedLifetime(); // Paper - diff on change - } - - } -diff --git a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java -index b1b0b0554e0ad035463fd53aa440d9c079b7cfb4..2ed6845f16fab175e2e9e96e76391e63ab4a43e2 100644 ---- a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java -+++ b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java -@@ -614,7 +614,7 @@ public class ArmorStand extends LivingEntity { - ItemStack itemstack = new ItemStack(Items.ARMOR_STAND); - - itemstack.set(DataComponents.CUSTOM_NAME, this.getCustomName()); -- this.drops.add(org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(itemstack)); // CraftBukkit - add to drops -+ this.drops.add(new DefaultDrop(itemstack, stack -> Block.popResource(this.level(), this.blockPosition(), stack))); // CraftBukkit - add to drops // Paper - Restore vanilla drops behavior - return this.brokenByAnything(damageSource); // Paper - } - -@@ -628,7 +628,7 @@ public class ArmorStand extends LivingEntity { - for (i = 0; i < this.handItems.size(); ++i) { - itemstack = (ItemStack) this.handItems.get(i); - if (!itemstack.isEmpty()) { -- this.drops.add(org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack)); // CraftBukkit - add to drops // Paper - mirror so we can destroy it later - though this call site was safe -+ this.drops.add(new DefaultDrop(itemstack, stack -> Block.popResource(this.level(), this.blockPosition().above(), stack))); // CraftBukkit - add to drops // Paper - Restore vanilla drops behavior; mirror so we can destroy it later - though this call site was safe & spawn drops correctly - this.handItems.set(i, ItemStack.EMPTY); - } - } -@@ -636,7 +636,7 @@ public class ArmorStand extends LivingEntity { - for (i = 0; i < this.armorItems.size(); ++i) { - itemstack = (ItemStack) this.armorItems.get(i); - if (!itemstack.isEmpty()) { -- this.drops.add(org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack)); // CraftBukkit - add to drops // Paper - mirror so we can destroy it later - though this call site was safe -+ this.drops.add(new DefaultDrop(itemstack, stack -> Block.popResource(this.level(), this.blockPosition().above(), stack))); // CraftBukkit - add to drops // Paper - Restore vanilla drops behavior; mirror so we can destroy it later - though this call site was safe & spawn drops correctly - this.armorItems.set(i, ItemStack.EMPTY); - } - } -diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java -index eefdb3da069d9615db7c663bcf8a72b78a0e6817..822bfebc779646dde655239ac217bad011a9eee4 100644 ---- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java -+++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java -@@ -964,17 +964,23 @@ public class CraftEventFactory { - } - - public static EntityDeathEvent callEntityDeathEvent(net.minecraft.world.entity.LivingEntity victim) { -- return CraftEventFactory.callEntityDeathEvent(victim, new ArrayList(0)); -+ return CraftEventFactory.callEntityDeathEvent(victim, new ArrayList<>(0)); // Paper - Restore vanilla drops behavior - } - -- public static EntityDeathEvent callEntityDeathEvent(net.minecraft.world.entity.LivingEntity victim, List drops) { -+ public static EntityDeathEvent callEntityDeathEvent(net.minecraft.world.entity.LivingEntity victim, List drops) { // Paper - Restore vanilla drops behavior - // Paper start - return CraftEventFactory.callEntityDeathEvent(victim, drops, com.google.common.util.concurrent.Runnables.doNothing()); - } -- public static EntityDeathEvent callEntityDeathEvent(net.minecraft.world.entity.LivingEntity victim, List drops, Runnable lootCheck) { -+ -+ private static final java.util.function.Function FROM_FUNCTION = stack -> { -+ if (stack == null) return null; -+ return new Entity.DefaultDrop(CraftItemType.bukkitToMinecraft(stack.getType()), stack, null); -+ }; -+ -+ public static EntityDeathEvent callEntityDeathEvent(net.minecraft.world.entity.LivingEntity victim, List drops, Runnable lootCheck) { // Paper - // Paper end - CraftLivingEntity entity = (CraftLivingEntity) victim.getBukkitEntity(); -- EntityDeathEvent event = new EntityDeathEvent(entity, drops, victim.getExpReward()); -+ EntityDeathEvent event = new EntityDeathEvent(entity, new io.papermc.paper.util.TransformingRandomAccessList<>(drops, Entity.DefaultDrop::stack, FROM_FUNCTION), victim.getExpReward()); // Paper - Restore vanilla drops behavior - populateFields(victim, event); // Paper - make cancellable - CraftWorld world = (CraftWorld) entity.getWorld(); - Bukkit.getServer().getPluginManager().callEvent(event); -@@ -988,19 +994,23 @@ public class CraftEventFactory { - victim.expToDrop = event.getDroppedExp(); - lootCheck.run(); // Paper - advancement triggers before destroying items - -- for (org.bukkit.inventory.ItemStack stack : event.getDrops()) { -+ // Paper start - Restore vanilla drops behavior -+ for (Entity.DefaultDrop drop : drops) { -+ if (drop == null) continue; -+ final org.bukkit.inventory.ItemStack stack = drop.stack(); -+ // Paper end - Restore vanilla drops behavior - if (stack == null || stack.getType() == Material.AIR || stack.getAmount() == 0) continue; - -- world.dropItem(entity.getLocation(), stack); // Paper - note: dropItem already clones due to this being bukkit -> NMS -+ drop.runConsumer(world, entity.getLocation()); // Paper - Restore vanilla drops behavior - if (stack instanceof CraftItemStack) stack.setAmount(0); // Paper - destroy this item - if this ever leaks due to game bugs, ensure it doesn't dupe, but don't nuke bukkit stacks of manually added items - } - - return event; - } - -- public static PlayerDeathEvent callPlayerDeathEvent(ServerPlayer victim, List drops, net.kyori.adventure.text.Component deathMessage, boolean keepInventory) { // Paper - Adventure -+ public static PlayerDeathEvent callPlayerDeathEvent(ServerPlayer victim, List drops, net.kyori.adventure.text.Component deathMessage, boolean keepInventory) { // Paper - Adventure & Restore vanilla drops behavior - CraftPlayer entity = victim.getBukkitEntity(); -- PlayerDeathEvent event = new PlayerDeathEvent(entity, drops, victim.getExpReward(), 0, deathMessage); -+ PlayerDeathEvent event = new PlayerDeathEvent(entity, new io.papermc.paper.util.TransformingRandomAccessList<>(drops, Entity.DefaultDrop::stack, FROM_FUNCTION), victim.getExpReward(), 0, deathMessage); // Paper - Restore vanilla drops behavior - event.setKeepInventory(keepInventory); - event.setKeepLevel(victim.keepLevel); // SPIGOT-2222: pre-set keepLevel - populateFields(victim, event); // Paper - make cancellable -@@ -1019,10 +1029,14 @@ public class CraftEventFactory { - victim.expToDrop = event.getDroppedExp(); - victim.newExp = event.getNewExp(); - -- for (org.bukkit.inventory.ItemStack stack : event.getDrops()) { -+ // Paper start - Restore vanilla drops behavior -+ for (Entity.DefaultDrop drop : drops) { -+ if (drop == null) continue; -+ final org.bukkit.inventory.ItemStack stack = drop.stack(); -+ // Paper end - Restore vanilla drops behavior - if (stack == null || stack.getType() == Material.AIR) continue; - -- world.dropItem(entity.getLocation(), stack); -+ drop.runConsumer(world, entity.getLocation()); // Paper - Restore vanilla drops behavior - } - - return event; diff --git a/patches/server/0943-Dont-resend-blocks-on-interactions.patch b/patches/server/0943-Dont-resend-blocks-on-interactions.patch deleted file mode 100644 index 384e9d45a5..0000000000 --- a/patches/server/0943-Dont-resend-blocks-on-interactions.patch +++ /dev/null @@ -1,171 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> -Date: Tue, 27 Jun 2023 21:09:11 -0400 -Subject: [PATCH] Dont resend blocks on interactions - -In general, the client now has an acknowledgment system which will prevent block changes made by the client to be reverted correctly. - -It should be noted that this system does not yet support block entities, so those still need to resynced when needed. - -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -index cef3a64b5e6c4d34ed079078420078a7adfcd574..3dff328c5e4456aae291a8dec82766681f49af06 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -@@ -202,7 +202,7 @@ public class ServerPlayerGameMode { - PlayerInteractEvent event = CraftEventFactory.callPlayerInteractEvent(this.player, Action.LEFT_CLICK_BLOCK, pos, direction, this.player.getInventory().getSelected(), InteractionHand.MAIN_HAND); - if (event.isCancelled()) { - // Let the client know the block still exists -- this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); -+ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); // Paper - Don't resync blocks - // Update any tile entity data for this block - capturedBlockEntity = true; // Paper - Send block entities after destroy prediction - return; -@@ -217,7 +217,7 @@ public class ServerPlayerGameMode { - // Spigot start - handle debug stick left click for non-creative - if (this.player.getMainHandItem().is(net.minecraft.world.item.Items.DEBUG_STICK) - && ((net.minecraft.world.item.DebugStickItem) net.minecraft.world.item.Items.DEBUG_STICK).handleInteraction(this.player, this.level.getBlockState(pos), this.level, pos, false, this.player.getMainHandItem())) { -- this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); -+ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); // Paper - Don't resync block - return; - } - // Spigot end -@@ -235,15 +235,17 @@ public class ServerPlayerGameMode { - // CraftBukkit start - Swings at air do *NOT* exist. - if (event.useInteractedBlock() == Event.Result.DENY) { - // If we denied a door from opening, we need to send a correcting update to the client, as it already opened the door. -- BlockState data = this.level.getBlockState(pos); -- if (data.getBlock() instanceof DoorBlock) { -- // For some reason *BOTH* the bottom/top part have to be marked updated. -- boolean bottom = data.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER; -- this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); -- this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, bottom ? pos.above() : pos.below())); -- } else if (data.getBlock() instanceof TrapDoorBlock) { -- this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); -- } -+ // Paper start - Don't resync blocks -+ //BlockState data = this.level.getBlockState(pos); -+ //if (data.getBlock() instanceof DoorBlock) { -+ // // For some reason *BOTH* the bottom/top part have to be marked updated. -+ // boolean bottom = data.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER; -+ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); -+ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, bottom ? pos.above() : pos.below())); -+ //} else if (data.getBlock() instanceof TrapDoorBlock) { -+ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); -+ //} -+ // Paper end - Don't resync blocks - } else if (!iblockdata.isAir()) { - iblockdata.attack(this.level, pos, this.player); - f = iblockdata.getDestroyProgress(this.player, this.player.level(), pos); -@@ -252,7 +254,7 @@ public class ServerPlayerGameMode { - if (event.useItemInHand() == Event.Result.DENY) { - // If we 'insta destroyed' then the client needs to be informed. - if (f > 1.0f) { -- this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); -+ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); // Paper - Don't resync blocks - } - return; - } -@@ -260,7 +262,7 @@ public class ServerPlayerGameMode { - - if (blockEvent.isCancelled()) { - // Let the client know the block still exists -- this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); -+ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); // Paper - Don't resync block - return; - } - -@@ -351,7 +353,7 @@ public class ServerPlayerGameMode { - - // Tell client the block is gone immediately then process events - // Don't tell the client if its a creative sword break because its not broken! -- if (this.level.getBlockEntity(pos) == null && !isSwordNoBreak) { -+ if (false && this.level.getBlockEntity(pos) == null && !isSwordNoBreak) { // Paper - Don't resync block - ClientboundBlockUpdatePacket packet = new ClientboundBlockUpdatePacket(pos, Blocks.AIR.defaultBlockState()); - this.player.connection.send(packet); - } -@@ -377,13 +379,15 @@ public class ServerPlayerGameMode { - if (isSwordNoBreak) { - return false; - } -+ // Paper start - Don't resync blocks - // Let the client know the block still exists -- this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); -+ //this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); - - // Brute force all possible updates -- for (Direction dir : Direction.values()) { -- this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos.relative(dir))); -- } -+ //for (Direction dir : Direction.values()) { -+ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos.relative(dir))); -+ //} -+ // Paper end - Don't resync blocks - - // Update any tile entity data for this block - if (!captureSentBlockEntities) { // Paper - Send block entities after destroy prediction -@@ -532,16 +536,18 @@ public class ServerPlayerGameMode { - if (event.useInteractedBlock() == Event.Result.DENY) { - // If we denied a door from opening, we need to send a correcting update to the client, as it already opened the door. - if (iblockdata.getBlock() instanceof DoorBlock) { -- boolean bottom = iblockdata.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER; -- player.connection.send(new ClientboundBlockUpdatePacket(world, bottom ? blockposition.above() : blockposition.below())); -+ // Paper start - Don't resync blocks -+ // boolean bottom = iblockdata.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER; -+ // player.connection.send(new ClientboundBlockUpdatePacket(world, bottom ? blockposition.above() : blockposition.below())); -+ // Paper end - Don't resync blocks - } else if (iblockdata.getBlock() instanceof CakeBlock) { - player.getBukkitEntity().sendHealthUpdate(); // SPIGOT-1341 - reset health for cake - } else if (this.interactItemStack.getItem() instanceof DoubleHighBlockItem) { - // send a correcting update to the client, as it already placed the upper half of the bisected item -- player.connection.send(new ClientboundBlockUpdatePacket(world, blockposition.relative(hitResult.getDirection()).above())); -+ //player.connection.send(new ClientboundBlockUpdatePacket(world, blockposition.relative(hitResult.getDirection()).above())); // Paper - Don't resync blocks - - // send a correcting update to the client for the block above as well, this because of replaceable blocks (such as grass, sea grass etc) -- player.connection.send(new ClientboundBlockUpdatePacket(world, blockposition.above())); -+ //player.connection.send(new ClientboundBlockUpdatePacket(world, blockposition.above())); // Paper - Don't resync blocks - // Paper start - extend Player Interact cancellation // TODO: consider merging this into the extracted method - } else if (iblockdata.is(Blocks.STRUCTURE_BLOCK) || iblockdata.getBlock() instanceof net.minecraft.world.level.block.CommandBlock) { - player.connection.send(new net.minecraft.network.protocol.game.ClientboundContainerClosePacket(this.player.containerMenu.containerId)); -diff --git a/src/main/java/net/minecraft/world/item/BucketItem.java b/src/main/java/net/minecraft/world/item/BucketItem.java -index 6d494b80ae002aea00afa44adf83dad1ae5bbbc1..49557d6f22c5725c663a231deab019d4f6fe95fa 100644 ---- a/src/main/java/net/minecraft/world/item/BucketItem.java -+++ b/src/main/java/net/minecraft/world/item/BucketItem.java -@@ -78,7 +78,7 @@ public class BucketItem extends Item implements DispensibleContainerItem { - PlayerBucketFillEvent event = CraftEventFactory.callPlayerBucketFillEvent((ServerLevel) world, user, blockposition, blockposition, movingobjectpositionblock.getDirection(), itemstack, dummyFluid.getItem(), hand); - - if (event.isCancelled()) { -- ((ServerPlayer) user).connection.send(new ClientboundBlockUpdatePacket(world, blockposition)); // SPIGOT-5163 (see PlayerInteractManager) -+ // ((ServerPlayer) user).connection.send(new ClientboundBlockUpdatePacket(world, blockposition)); // SPIGOT-5163 (see PlayerInteractManager) // Paper - Don't resend blocks - ((ServerPlayer) user).getBukkitEntity().updateInventory(); // SPIGOT-4541 - return InteractionResultHolder.fail(itemstack); - } -@@ -185,7 +185,7 @@ public class BucketItem extends Item implements DispensibleContainerItem { - if (flag2 && entityhuman != null) { - PlayerBucketEmptyEvent event = CraftEventFactory.callPlayerBucketEmptyEvent((ServerLevel) world, entityhuman, blockposition, clicked, enumdirection, itemstack, enumhand); - if (event.isCancelled()) { -- ((ServerPlayer) entityhuman).connection.send(new ClientboundBlockUpdatePacket(world, blockposition)); // SPIGOT-4238: needed when looking through entity -+ // ((ServerPlayer) entityhuman).connection.send(new ClientboundBlockUpdatePacket(world, blockposition)); // SPIGOT-4238: needed when looking through entity // Paper - Don't resend blocks - ((ServerPlayer) entityhuman).getBukkitEntity().updateInventory(); // SPIGOT-4541 - return false; - } -diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java -index fb85a7da9da8063c2b781377f62e7db34c467859..2d2b436cae5693051df14e5777d385d38d5f4dd6 100644 ---- a/src/main/java/net/minecraft/world/item/ItemStack.java -+++ b/src/main/java/net/minecraft/world/item/ItemStack.java -@@ -480,10 +480,12 @@ public final class ItemStack implements DataComponentHolder { - world.preventPoiUpdated = false; - - // Brute force all possible updates -- BlockPos placedPos = ((CraftBlock) placeEvent.getBlock()).getPosition(); -- for (Direction dir : Direction.values()) { -- ((ServerPlayer) entityhuman).connection.send(new ClientboundBlockUpdatePacket(world, placedPos.relative(dir))); -- } -+ // Paper start - Don't resync blocks -+ // BlockPos placedPos = ((CraftBlock) placeEvent.getBlock()).getPosition(); -+ // for (Direction dir : Direction.values()) { -+ // ((ServerPlayer) entityhuman).connection.send(new ClientboundBlockUpdatePacket(world, placedPos.relative(dir))); -+ // } -+ // Paper end - Don't resync blocks - SignItem.openSign = null; // SPIGOT-6758 - Reset on early return - } else { - // Change the stack to its new contents if it hasn't been tampered with. diff --git a/patches/server/0943-add-more-scoreboard-API.patch b/patches/server/0943-add-more-scoreboard-API.patch new file mode 100644 index 0000000000..3365b53a36 --- /dev/null +++ b/patches/server/0943-add-more-scoreboard-API.patch @@ -0,0 +1,79 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Sat, 16 Dec 2023 14:46:01 -0800 +Subject: [PATCH] add more scoreboard API + + +diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java +index b36e5574c10e6d70a399e2ac0704fd4f43dbb444..2d3abf2a1da487ead74d698cc5ea4eb729c35c8d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java ++++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java +@@ -185,6 +185,19 @@ final class CraftObjective extends CraftScoreboardComponent implements Objective + final CraftObjective other = (CraftObjective) obj; + return !(this.objective != other.objective && (this.objective == null || !this.objective.equals(other.objective))); + } ++ // Paper start - add more score API ++ @Override ++ public boolean willAutoUpdateDisplay() { ++ this.checkState(); ++ return this.objective.displayAutoUpdate(); ++ } ++ ++ @Override ++ public void setAutoUpdateDisplay(final boolean autoUpdateDisplay) { ++ this.checkState(); ++ this.objective.setDisplayAutoUpdate(autoUpdateDisplay); ++ } ++ // Paper end - add more score API + + + } +diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScore.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScore.java +index ceb1a39c02c3cfa7632a0fdca414c7046888fcb1..74d9c407e971804bed420370f7b684d8658eb5aa 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScore.java ++++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScore.java +@@ -74,4 +74,44 @@ final class CraftScore implements Score { + board.resetSinglePlayerScore(entry, this.objective.getHandle()); + } + // Paper end ++ ++ // Paper start - add more score API ++ @Override ++ public boolean isTriggerable() { ++ if (this.objective.getTrackedCriteria() != org.bukkit.scoreboard.Criteria.TRIGGER) { ++ return false; ++ } ++ final Scoreboard board = this.objective.checkState().board; ++ final ReadOnlyScoreInfo scoreInfo = board.getPlayerScoreInfo(this.entry, this.objective.getHandle()); ++ return scoreInfo != null && !scoreInfo.isLocked(); ++ } ++ ++ @Override ++ public void setTriggerable(final boolean triggerable) { ++ com.google.common.base.Preconditions.checkArgument(this.objective.getTrackedCriteria() == org.bukkit.scoreboard.Criteria.TRIGGER, "the criteria isn't 'trigger'"); ++ final Scoreboard board = this.objective.checkState().board; ++ if (triggerable) { ++ board.getOrCreatePlayerScore(this.entry, this.objective.getHandle()).unlock(); ++ } else { ++ board.getOrCreatePlayerScore(this.entry, this.objective.getHandle()).lock(); ++ } ++ } ++ ++ @Override ++ public net.kyori.adventure.text.Component customName() { ++ final Scoreboard board = this.objective.checkState().board; ++ final ReadOnlyScoreInfo scoreInfo = board.getPlayerScoreInfo(this.entry, this.objective.getHandle()); ++ if (scoreInfo == null) { ++ return null; // If score doesn't exist, don't create one ++ } ++ final net.minecraft.network.chat.Component display = board.getOrCreatePlayerScore(this.entry, this.objective.getHandle()).display(); ++ return display == null ? null : io.papermc.paper.adventure.PaperAdventure.asAdventure(display); ++ } ++ ++ @Override ++ public void customName(final net.kyori.adventure.text.Component customName) { ++ final Scoreboard board = this.objective.checkState().board; ++ board.getOrCreatePlayerScore(this.entry, this.objective.getHandle()).display(io.papermc.paper.adventure.PaperAdventure.asVanilla(customName)); ++ } ++ // Paper end - add more score API + } diff --git a/patches/server/0944-Improve-Registry.patch b/patches/server/0944-Improve-Registry.patch new file mode 100644 index 0000000000..0d7c6b94e6 --- /dev/null +++ b/patches/server/0944-Improve-Registry.patch @@ -0,0 +1,102 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Wed, 20 Dec 2023 02:03:05 -0800 +Subject: [PATCH] Improve Registry + + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java b/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java +index c7d377dbb53cdc1f823a839e3a113136efc16349..73472890e30180dc3cb6aa1bd9c5815087334682 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java +@@ -150,6 +150,7 @@ public class CraftRegistry implements Registry { + + private final Class bukkitClass; + private final Map cache = new HashMap<>(); ++ private final Map byValue = new java.util.IdentityHashMap<>(); // Paper - improve Registry + private final net.minecraft.core.Registry minecraftRegistry; + private final BiFunction minecraftToBukkit; + private boolean init; +@@ -194,6 +195,7 @@ public class CraftRegistry implements Registry { + } + + this.cache.put(namespacedKey, bukkit); ++ this.byValue.put(bukkit, namespacedKey); // Paper - improve Registry + + return bukkit; + } +@@ -216,4 +218,11 @@ public class CraftRegistry implements Registry { + + return this.minecraftToBukkit.apply(namespacedKey, minecraft); + } ++ ++ // Paper start - improve Registry ++ @Override ++ public NamespacedKey getKey(final B value) { ++ return this.byValue.get(value); ++ } ++ // Paper end - improve Registry + } +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimMaterial.java b/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimMaterial.java +index cd3e35867075e65f46051fb88d8a2460a8bb4b53..76627683f256a034a147765db693a9fd2ab9613f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimMaterial.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimMaterial.java +@@ -54,6 +54,7 @@ public class CraftTrimMaterial implements TrimMaterial, Handleable this + " doesn't have a key"); // Paper + return this.key; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimPattern.java b/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimPattern.java +index 364f8d7a7106259401154d91b1b79869d014a469..f336bf98574e4fdeabc3b210629834393ec11a74 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimPattern.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimPattern.java +@@ -54,6 +54,7 @@ public class CraftTrimPattern implements TrimPattern, Handleable this + " doesn't have a key"); // Paper + return this.key; + } + +diff --git a/src/test/java/org/bukkit/registry/PerRegistryTest.java b/src/test/java/org/bukkit/registry/PerRegistryTest.java +index 1c4966520b6401e6571aa44d5934dfa280bc80e3..010de6fbb75eb5d51639695d260f916072fdb22d 100644 +--- a/src/test/java/org/bukkit/registry/PerRegistryTest.java ++++ b/src/test/java/org/bukkit/registry/PerRegistryTest.java +@@ -49,19 +49,22 @@ public class PerRegistryTest extends AbstractTestingBase { + + @ParameterizedTest + @MethodSource("data") +- public void testGet(Registry registry) { ++ public void testGet(Registry registry) { // Paper - improve Registry + registry.forEach(element -> { ++ NamespacedKey key = registry.getKey(element); // Paper - improve Registry ++ assertNotNull(key); // Paper - improve Registry + // Values in the registry should be referentially equal to what is returned with #get() + // This ensures that new instances are not created each time #get() is invoked +- assertSame(element, registry.get(element.getKey())); ++ assertSame(element, registry.get(key)); // Paper - improve Registry + }); + } + + @ParameterizedTest + @MethodSource("data") +- public void testMatch(Registry registry) { ++ public void testMatch(Registry registry) { // Paper - improve Registry + registry.forEach(element -> { +- NamespacedKey key = element.getKey(); ++ NamespacedKey key = registry.getKey(element); // Paper - improve Registry ++ assertNotNull(key); // Paper - improve Registry + + this.assertSameMatchWithKeyMessage(registry, element, key.toString()); // namespace:key + this.assertSameMatchWithKeyMessage(registry, element, key.getKey()); // key +@@ -72,7 +75,7 @@ public class PerRegistryTest extends AbstractTestingBase { + }); + } + +- private void assertSameMatchWithKeyMessage(Registry registry, Keyed element, String key) { ++ private void assertSameMatchWithKeyMessage(Registry registry, T element, String key) { // Paper - improve Registry + assertSame(element, registry.match(key), key); + } + diff --git a/patches/server/0944-add-more-scoreboard-API.patch b/patches/server/0944-add-more-scoreboard-API.patch deleted file mode 100644 index 3365b53a36..0000000000 --- a/patches/server/0944-add-more-scoreboard-API.patch +++ /dev/null @@ -1,79 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Sat, 16 Dec 2023 14:46:01 -0800 -Subject: [PATCH] add more scoreboard API - - -diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java -index b36e5574c10e6d70a399e2ac0704fd4f43dbb444..2d3abf2a1da487ead74d698cc5ea4eb729c35c8d 100644 ---- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java -+++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java -@@ -185,6 +185,19 @@ final class CraftObjective extends CraftScoreboardComponent implements Objective - final CraftObjective other = (CraftObjective) obj; - return !(this.objective != other.objective && (this.objective == null || !this.objective.equals(other.objective))); - } -+ // Paper start - add more score API -+ @Override -+ public boolean willAutoUpdateDisplay() { -+ this.checkState(); -+ return this.objective.displayAutoUpdate(); -+ } -+ -+ @Override -+ public void setAutoUpdateDisplay(final boolean autoUpdateDisplay) { -+ this.checkState(); -+ this.objective.setDisplayAutoUpdate(autoUpdateDisplay); -+ } -+ // Paper end - add more score API - - - } -diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScore.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScore.java -index ceb1a39c02c3cfa7632a0fdca414c7046888fcb1..74d9c407e971804bed420370f7b684d8658eb5aa 100644 ---- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScore.java -+++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScore.java -@@ -74,4 +74,44 @@ final class CraftScore implements Score { - board.resetSinglePlayerScore(entry, this.objective.getHandle()); - } - // Paper end -+ -+ // Paper start - add more score API -+ @Override -+ public boolean isTriggerable() { -+ if (this.objective.getTrackedCriteria() != org.bukkit.scoreboard.Criteria.TRIGGER) { -+ return false; -+ } -+ final Scoreboard board = this.objective.checkState().board; -+ final ReadOnlyScoreInfo scoreInfo = board.getPlayerScoreInfo(this.entry, this.objective.getHandle()); -+ return scoreInfo != null && !scoreInfo.isLocked(); -+ } -+ -+ @Override -+ public void setTriggerable(final boolean triggerable) { -+ com.google.common.base.Preconditions.checkArgument(this.objective.getTrackedCriteria() == org.bukkit.scoreboard.Criteria.TRIGGER, "the criteria isn't 'trigger'"); -+ final Scoreboard board = this.objective.checkState().board; -+ if (triggerable) { -+ board.getOrCreatePlayerScore(this.entry, this.objective.getHandle()).unlock(); -+ } else { -+ board.getOrCreatePlayerScore(this.entry, this.objective.getHandle()).lock(); -+ } -+ } -+ -+ @Override -+ public net.kyori.adventure.text.Component customName() { -+ final Scoreboard board = this.objective.checkState().board; -+ final ReadOnlyScoreInfo scoreInfo = board.getPlayerScoreInfo(this.entry, this.objective.getHandle()); -+ if (scoreInfo == null) { -+ return null; // If score doesn't exist, don't create one -+ } -+ final net.minecraft.network.chat.Component display = board.getOrCreatePlayerScore(this.entry, this.objective.getHandle()).display(); -+ return display == null ? null : io.papermc.paper.adventure.PaperAdventure.asAdventure(display); -+ } -+ -+ @Override -+ public void customName(final net.kyori.adventure.text.Component customName) { -+ final Scoreboard board = this.objective.checkState().board; -+ board.getOrCreatePlayerScore(this.entry, this.objective.getHandle()).display(io.papermc.paper.adventure.PaperAdventure.asVanilla(customName)); -+ } -+ // Paper end - add more score API - } diff --git a/patches/server/0945-Fix-NPE-on-null-loc-for-EntityTeleportEvent.patch b/patches/server/0945-Fix-NPE-on-null-loc-for-EntityTeleportEvent.patch new file mode 100644 index 0000000000..b6f285b9cc --- /dev/null +++ b/patches/server/0945-Fix-NPE-on-null-loc-for-EntityTeleportEvent.patch @@ -0,0 +1,66 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Sat, 9 Dec 2023 19:15:59 -0800 +Subject: [PATCH] Fix NPE on null loc for EntityTeleportEvent + +EntityTeleportEvent#setTo is marked as nullable and so is the +getTo method. This fixes the handling of a null "to" location +by treating it the same as the event being cancelled. This is +already existing behavior for the EntityPortalEvent (which +extends EntityTeleportEvent). + +diff --git a/src/main/java/net/minecraft/server/commands/TeleportCommand.java b/src/main/java/net/minecraft/server/commands/TeleportCommand.java +index a306b30af19277386a2f3e560b4902a8b5796f2a..54851f6cc0d5fddb32a9a1e84a4f5ae41af18758 100644 +--- a/src/main/java/net/minecraft/server/commands/TeleportCommand.java ++++ b/src/main/java/net/minecraft/server/commands/TeleportCommand.java +@@ -169,9 +169,10 @@ public class TeleportCommand { + Location to = new Location(world.getWorld(), x, y, z, f2, f3); + EntityTeleportEvent event = new EntityTeleportEvent(target.getBukkitEntity(), target.getBukkitEntity().getLocation(), to); + world.getCraftServer().getPluginManager().callEvent(event); +- if (event.isCancelled()) { ++ if (event.isCancelled() || event.getTo() == null) { // Paper + return; + } ++ to = event.getTo(); // Paper - actually track new location + + x = to.getX(); + y = to.getY(); +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index f948f4051607611e7105790c3b6bacc5d3ec39db..68c312dc39ef6fdb88e201ba096c9bd99aebddd7 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -4183,7 +4183,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + if (!(this instanceof ServerPlayer)) { + EntityTeleportEvent teleport = new EntityTeleportEvent(this.getBukkitEntity(), new Location(this.level().getWorld(), d3, d4, d5), new Location(this.level().getWorld(), d0, d6, d2)); + this.level().getCraftServer().getPluginManager().callEvent(teleport); +- if (!teleport.isCancelled()) { ++ if (!teleport.isCancelled() && teleport.getTo() != null) { // Paper + Location to = teleport.getTo(); + this.teleportTo(to.getX(), to.getY(), to.getZ()); + } else { +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java +index 8fa9282acd87132516329083f774345df3310cf2..edd29c2f4d0151d512618115a8fb4b7423171491 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java +@@ -130,7 +130,7 @@ public class FollowOwnerGoal extends Goal { + } else { + // CraftBukkit start + EntityTeleportEvent event = CraftEventFactory.callEntityTeleportEvent(this.tamable, (double) x + 0.5D, (double) y, (double) z + 0.5D); +- if (event.isCancelled()) { ++ if (event.isCancelled() || event.getTo() == null) { // Paper + return false; + } + Location to = event.getTo(); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Shulker.java b/src/main/java/net/minecraft/world/entity/monster/Shulker.java +index 509c8fae366e6aeca324b4d8e39bd3182d6d9b9b..c1f73e10d8473251425300cedbed2b82da6a79ce 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Shulker.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Shulker.java +@@ -409,7 +409,7 @@ public class Shulker extends AbstractGolem implements VariantHolder -Date: Wed, 20 Dec 2023 02:03:05 -0800 -Subject: [PATCH] Improve Registry - - -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java b/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java -index c7d377dbb53cdc1f823a839e3a113136efc16349..73472890e30180dc3cb6aa1bd9c5815087334682 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java -@@ -150,6 +150,7 @@ public class CraftRegistry implements Registry { - - private final Class bukkitClass; - private final Map cache = new HashMap<>(); -+ private final Map byValue = new java.util.IdentityHashMap<>(); // Paper - improve Registry - private final net.minecraft.core.Registry minecraftRegistry; - private final BiFunction minecraftToBukkit; - private boolean init; -@@ -194,6 +195,7 @@ public class CraftRegistry implements Registry { - } - - this.cache.put(namespacedKey, bukkit); -+ this.byValue.put(bukkit, namespacedKey); // Paper - improve Registry - - return bukkit; - } -@@ -216,4 +218,11 @@ public class CraftRegistry implements Registry { - - return this.minecraftToBukkit.apply(namespacedKey, minecraft); - } -+ -+ // Paper start - improve Registry -+ @Override -+ public NamespacedKey getKey(final B value) { -+ return this.byValue.get(value); -+ } -+ // Paper end - improve Registry - } -diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimMaterial.java b/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimMaterial.java -index cd3e35867075e65f46051fb88d8a2460a8bb4b53..76627683f256a034a147765db693a9fd2ab9613f 100644 ---- a/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimMaterial.java -+++ b/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimMaterial.java -@@ -54,6 +54,7 @@ public class CraftTrimMaterial implements TrimMaterial, Handleable this + " doesn't have a key"); // Paper - return this.key; - } - -diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimPattern.java b/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimPattern.java -index 364f8d7a7106259401154d91b1b79869d014a469..f336bf98574e4fdeabc3b210629834393ec11a74 100644 ---- a/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimPattern.java -+++ b/src/main/java/org/bukkit/craftbukkit/inventory/trim/CraftTrimPattern.java -@@ -54,6 +54,7 @@ public class CraftTrimPattern implements TrimPattern, Handleable this + " doesn't have a key"); // Paper - return this.key; - } - -diff --git a/src/test/java/org/bukkit/registry/PerRegistryTest.java b/src/test/java/org/bukkit/registry/PerRegistryTest.java -index 1c4966520b6401e6571aa44d5934dfa280bc80e3..010de6fbb75eb5d51639695d260f916072fdb22d 100644 ---- a/src/test/java/org/bukkit/registry/PerRegistryTest.java -+++ b/src/test/java/org/bukkit/registry/PerRegistryTest.java -@@ -49,19 +49,22 @@ public class PerRegistryTest extends AbstractTestingBase { - - @ParameterizedTest - @MethodSource("data") -- public void testGet(Registry registry) { -+ public void testGet(Registry registry) { // Paper - improve Registry - registry.forEach(element -> { -+ NamespacedKey key = registry.getKey(element); // Paper - improve Registry -+ assertNotNull(key); // Paper - improve Registry - // Values in the registry should be referentially equal to what is returned with #get() - // This ensures that new instances are not created each time #get() is invoked -- assertSame(element, registry.get(element.getKey())); -+ assertSame(element, registry.get(key)); // Paper - improve Registry - }); - } - - @ParameterizedTest - @MethodSource("data") -- public void testMatch(Registry registry) { -+ public void testMatch(Registry registry) { // Paper - improve Registry - registry.forEach(element -> { -- NamespacedKey key = element.getKey(); -+ NamespacedKey key = registry.getKey(element); // Paper - improve Registry -+ assertNotNull(key); // Paper - improve Registry - - this.assertSameMatchWithKeyMessage(registry, element, key.toString()); // namespace:key - this.assertSameMatchWithKeyMessage(registry, element, key.getKey()); // key -@@ -72,7 +75,7 @@ public class PerRegistryTest extends AbstractTestingBase { - }); - } - -- private void assertSameMatchWithKeyMessage(Registry registry, Keyed element, String key) { -+ private void assertSameMatchWithKeyMessage(Registry registry, T element, String key) { // Paper - improve Registry - assertSame(element, registry.match(key), key); - } - diff --git a/patches/server/0946-Add-experience-points-API.patch b/patches/server/0946-Add-experience-points-API.patch new file mode 100644 index 0000000000..cdd4f08f67 --- /dev/null +++ b/patches/server/0946-Add-experience-points-API.patch @@ -0,0 +1,73 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Lukas Planz +Date: Tue, 5 Sep 2023 20:34:20 +0200 +Subject: [PATCH] Add experience points API + + +diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java +index 230ffd3c4541ef8979605d733b6463e39cd00206..446556028028b25c35dbf3d099bf14a59e32a266 100644 +--- a/src/main/java/net/minecraft/world/entity/player/Player.java ++++ b/src/main/java/net/minecraft/world/entity/player/Player.java +@@ -1855,7 +1855,7 @@ public abstract class Player extends LivingEntity { + } + + public int getXpNeededForNextLevel() { +- return this.experienceLevel >= 30 ? 112 + (this.experienceLevel - 30) * 9 : (this.experienceLevel >= 15 ? 37 + (this.experienceLevel - 15) * 5 : 7 + this.experienceLevel * 2); ++ return this.experienceLevel >= 30 ? 112 + (this.experienceLevel - 30) * 9 : (this.experienceLevel >= 15 ? 37 + (this.experienceLevel - 15) * 5 : 7 + this.experienceLevel * 2); // Paper - diff on change; calculateTotalExperiencePoints + } + // Paper start - send while respecting visibility + private static void sendSoundEffect(Player fromEntity, double x, double y, double z, SoundEvent soundEffect, SoundSource soundCategory, float volume, float pitch) { +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 483221803ec1f1c78da1f83ccf42b041cb782d7a..e1af41a8d938420694a5342f08b811615dc46003 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -1902,6 +1902,49 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + Preconditions.checkArgument(exp >= 0, "Total experience points must not be negative (%s)", exp); + this.getHandle().totalExperience = exp; + } ++ // Paper start ++ @Override ++ public int calculateTotalExperiencePoints() { ++ return calculateTotalExperiencePoints(this.getLevel()) + Math.round(this.getExperiencePointsNeededForNextLevel() * getExp()); ++ } ++ ++ @Override ++ public void setExperienceLevelAndProgress(final int totalExperience) { ++ Preconditions.checkArgument(totalExperience >= 0, "Total experience points must not be negative (%s)", totalExperience); ++ int level = calculateLevelsForExperiencePoints(totalExperience); ++ int remainingPoints = totalExperience - calculateTotalExperiencePoints(level); ++ ++ this.getHandle().experienceLevel = level; ++ this.getHandle().experienceProgress = (float) remainingPoints / this.getExperiencePointsNeededForNextLevel(); ++ this.getHandle().lastSentExp = -1; ++ } ++ ++ @Override ++ public int getExperiencePointsNeededForNextLevel() { ++ return this.getHandle().getXpNeededForNextLevel(); ++ } ++ ++ // See https://minecraft.wiki/w/Experience#Leveling_up for reference ++ private int calculateTotalExperiencePoints(int level) { ++ if (level <= 16) { ++ return (int) (Math.pow(level, 2) + 6 * level); ++ } else if (level <= 31) { ++ return (int) (2.5 * Math.pow(level, 2) - 40.5 * level + 360.0); ++ } else { ++ return (int) (4.5 * Math.pow(level, 2) - 162.5 * level + 2220.0); ++ } ++ } ++ ++ private int calculateLevelsForExperiencePoints(int points) { ++ if (points <= 352) { // Level 0-16 ++ return (int) Math.floor(Math.sqrt(points + 9) - 3); ++ } else if (points <= 1507) { // Level 17-31 ++ return (int) Math.floor(8.1 + Math.sqrt(0.4 * (points - (7839.0 / 40.0)))); ++ } else { // 32+ ++ return (int) Math.floor((325.0 / 18.0) + Math.sqrt((2.0 / 9.0) * (points - (54215.0 / 72.0)))); ++ } ++ } ++ // Paper end + + @Override + public void sendExperienceChange(float progress) { diff --git a/patches/server/0946-Fix-NPE-on-null-loc-for-EntityTeleportEvent.patch b/patches/server/0946-Fix-NPE-on-null-loc-for-EntityTeleportEvent.patch deleted file mode 100644 index b6f285b9cc..0000000000 --- a/patches/server/0946-Fix-NPE-on-null-loc-for-EntityTeleportEvent.patch +++ /dev/null @@ -1,66 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Sat, 9 Dec 2023 19:15:59 -0800 -Subject: [PATCH] Fix NPE on null loc for EntityTeleportEvent - -EntityTeleportEvent#setTo is marked as nullable and so is the -getTo method. This fixes the handling of a null "to" location -by treating it the same as the event being cancelled. This is -already existing behavior for the EntityPortalEvent (which -extends EntityTeleportEvent). - -diff --git a/src/main/java/net/minecraft/server/commands/TeleportCommand.java b/src/main/java/net/minecraft/server/commands/TeleportCommand.java -index a306b30af19277386a2f3e560b4902a8b5796f2a..54851f6cc0d5fddb32a9a1e84a4f5ae41af18758 100644 ---- a/src/main/java/net/minecraft/server/commands/TeleportCommand.java -+++ b/src/main/java/net/minecraft/server/commands/TeleportCommand.java -@@ -169,9 +169,10 @@ public class TeleportCommand { - Location to = new Location(world.getWorld(), x, y, z, f2, f3); - EntityTeleportEvent event = new EntityTeleportEvent(target.getBukkitEntity(), target.getBukkitEntity().getLocation(), to); - world.getCraftServer().getPluginManager().callEvent(event); -- if (event.isCancelled()) { -+ if (event.isCancelled() || event.getTo() == null) { // Paper - return; - } -+ to = event.getTo(); // Paper - actually track new location - - x = to.getX(); - y = to.getY(); -diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index f948f4051607611e7105790c3b6bacc5d3ec39db..68c312dc39ef6fdb88e201ba096c9bd99aebddd7 100644 ---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -4183,7 +4183,7 @@ public abstract class LivingEntity extends Entity implements Attackable { - if (!(this instanceof ServerPlayer)) { - EntityTeleportEvent teleport = new EntityTeleportEvent(this.getBukkitEntity(), new Location(this.level().getWorld(), d3, d4, d5), new Location(this.level().getWorld(), d0, d6, d2)); - this.level().getCraftServer().getPluginManager().callEvent(teleport); -- if (!teleport.isCancelled()) { -+ if (!teleport.isCancelled() && teleport.getTo() != null) { // Paper - Location to = teleport.getTo(); - this.teleportTo(to.getX(), to.getY(), to.getZ()); - } else { -diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java -index 8fa9282acd87132516329083f774345df3310cf2..edd29c2f4d0151d512618115a8fb4b7423171491 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java -+++ b/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java -@@ -130,7 +130,7 @@ public class FollowOwnerGoal extends Goal { - } else { - // CraftBukkit start - EntityTeleportEvent event = CraftEventFactory.callEntityTeleportEvent(this.tamable, (double) x + 0.5D, (double) y, (double) z + 0.5D); -- if (event.isCancelled()) { -+ if (event.isCancelled() || event.getTo() == null) { // Paper - return false; - } - Location to = event.getTo(); -diff --git a/src/main/java/net/minecraft/world/entity/monster/Shulker.java b/src/main/java/net/minecraft/world/entity/monster/Shulker.java -index 509c8fae366e6aeca324b4d8e39bd3182d6d9b9b..c1f73e10d8473251425300cedbed2b82da6a79ce 100644 ---- a/src/main/java/net/minecraft/world/entity/monster/Shulker.java -+++ b/src/main/java/net/minecraft/world/entity/monster/Shulker.java -@@ -409,7 +409,7 @@ public class Shulker extends AbstractGolem implements VariantHolder +Date: Tue, 18 May 2021 12:32:02 -0700 +Subject: [PATCH] Add drops to shear events + + +diff --git a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +index fbed5d57db0c0e79996f85571b9af0071fa953c7..a024c697a65bbab27408da1d6a75e531d9719b47 100644 +--- a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +@@ -104,11 +104,14 @@ public class ShearsDispenseItemBehavior extends OptionalDispenseItemBehavior { + if (entityliving instanceof Shearable ishearable) { + if (ishearable.readyForShearing()) { + // CraftBukkit start +- if (CraftEventFactory.callBlockShearEntityEvent(entityliving, bukkitBlock, craftItem).isCancelled()) { ++ // Paper start - Add drops to shear events ++ org.bukkit.event.block.BlockShearEntityEvent event = CraftEventFactory.callBlockShearEntityEvent(entityliving, bukkitBlock, craftItem, ishearable.generateDefaultDrops()); ++ if (event.isCancelled()) { ++ // Paper end - Add drops to shear events + continue; + } + // CraftBukkit end +- ishearable.shear(SoundSource.BLOCKS); ++ ishearable.shear(SoundSource.BLOCKS, CraftItemStack.asNMSCopy(event.getDrops())); // Paper - Add drops to shear events + worldserver.gameEvent((Entity) null, (Holder) GameEvent.SHEAR, blockposition); + return true; + } +diff --git a/src/main/java/net/minecraft/world/entity/Shearable.java b/src/main/java/net/minecraft/world/entity/Shearable.java +index 5e8cc5cfac8888628c6d513148f41be09ca65a2c..2ee48ac3b665db2b02bcb1a30ec972d43a3725b0 100644 +--- a/src/main/java/net/minecraft/world/entity/Shearable.java ++++ b/src/main/java/net/minecraft/world/entity/Shearable.java +@@ -3,7 +3,13 @@ package net.minecraft.world.entity; + import net.minecraft.sounds.SoundSource; + + public interface Shearable { ++ default void shear(SoundSource soundCategory, java.util.List drops) { this.shear(soundCategory); } // Paper - Add drops to shear events + void shear(SoundSource shearedSoundCategory); + + boolean readyForShearing(); ++ // Paper start - custom shear drops; ensure all implementing entities override this ++ default java.util.List generateDefaultDrops() { ++ return java.util.Collections.emptyList(); ++ } ++ // Paper end - custom shear drops + } +diff --git a/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java b/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java +index aa125e3043b120935aaa015803f065f99eb8d050..0c21959f57ae88fcd0a4d6dc911c1ce347c96528 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java ++++ b/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java +@@ -123,11 +123,18 @@ public class MushroomCow extends Cow implements Shearable, VariantHolder drops = this.generateDefaultDrops(); ++ org.bukkit.event.player.PlayerShearEntityEvent event = CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemstack, hand, drops); ++ if (event != null) { ++ if (event.isCancelled()) { ++ return InteractionResult.PASS; ++ } ++ drops = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getDrops()); + } ++ // Paper end - custom shear drops + // CraftBukkit end +- this.shear(SoundSource.PLAYERS); ++ this.shear(SoundSource.PLAYERS, drops); // Paper - custom shear drops + this.gameEvent(GameEvent.SHEAR, player); + if (!this.level().isClientSide) { + itemstack.hurtAndBreak(1, player, getSlotForHand(hand)); +@@ -164,6 +171,22 @@ public class MushroomCow extends Cow implements Shearable, VariantHolder generateDefaultDrops() { ++ java.util.List dropEntities = new java.util.ArrayList<>(5); ++ for (int i = 0; i < 5; ++i) { ++ dropEntities.add(new ItemStack(this.getVariant().getBlockState().getBlock())); ++ } ++ return dropEntities; ++ } ++ ++ @Override ++ public void shear(SoundSource shearedSoundCategory, java.util.List drops) { // If drops is null, need to generate drops ++ // Paper end - custom shear drops + this.level().playSound((Player) null, (Entity) this, SoundEvents.MOOSHROOM_SHEAR, shearedSoundCategory, 1.0F, 1.0F); + if (!this.level().isClientSide()) { + Cow entitycow = (Cow) EntityType.COW.create(this.level()); +@@ -193,17 +216,12 @@ public class MushroomCow extends Cow implements Shearable, VariantHolder drops = this.generateDefaultDrops(); ++ org.bukkit.event.player.PlayerShearEntityEvent event = CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemstack, hand, drops); ++ if (event != null) { ++ if (event.isCancelled()) { ++ return InteractionResult.PASS; ++ } ++ drops = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getDrops()); + } ++ // Paper end - custom shear drops + // CraftBukkit end +- this.shear(SoundSource.PLAYERS); ++ this.shear(SoundSource.PLAYERS, drops); // Paper + this.gameEvent(GameEvent.SHEAR, player); + itemstack.hurtAndBreak(1, player, getSlotForHand(hand)); + return InteractionResult.SUCCESS; +@@ -276,13 +283,30 @@ public class Sheep extends Animal implements Shearable { + + @Override + public void shear(SoundSource shearedSoundCategory) { ++ // Paper start - custom shear drops ++ this.shear(shearedSoundCategory, this.generateDefaultDrops()); ++ } ++ ++ @Override ++ public java.util.List generateDefaultDrops() { ++ int count = 1 + this.random.nextInt(3); ++ java.util.List dropEntities = new java.util.ArrayList<>(count); ++ for (int j = 0; j < count; ++j) { ++ dropEntities.add(new ItemStack(Sheep.ITEM_BY_DYE.get(this.getColor()))); ++ } ++ return dropEntities; ++ } ++ ++ @Override ++ public void shear(SoundSource shearedSoundCategory, java.util.List drops) { ++ // Paper end - custom shear drops + this.level().playSound((Player) null, (Entity) this, SoundEvents.SHEEP_SHEAR, shearedSoundCategory, 1.0F, 1.0F); + this.setSheared(true); + int i = 1 + this.random.nextInt(3); + +- for (int j = 0; j < i; ++j) { ++ for (final ItemStack drop : drops) { // Paper - custom shear drops (moved drop generation to separate method) + this.forceDrops = true; // CraftBukkit +- ItemEntity entityitem = this.spawnAtLocation((ItemLike) Sheep.ITEM_BY_DYE.get(this.getColor()), 1); ++ ItemEntity entityitem = this.spawnAtLocation(drop, 1); // Paper - custom shear drops + this.forceDrops = false; // CraftBukkit + + if (entityitem != null) { +diff --git a/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java b/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java +index 2de1a2f666da9db1832907e1651dbff948e37252..5c2ed3c39c8eb850f3be1e2ea5b5a7ea266e16d1 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java ++++ b/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java +@@ -146,11 +146,18 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM + + if (itemstack.is(Items.SHEARS) && this.readyForShearing()) { + // CraftBukkit start +- if (!CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemstack, hand)) { +- return InteractionResult.PASS; ++ // Paper start - custom shear drops ++ java.util.List drops = this.generateDefaultDrops(); ++ org.bukkit.event.player.PlayerShearEntityEvent event = CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemstack, hand, drops); ++ if (event != null) { ++ if (event.isCancelled()) { ++ return InteractionResult.PASS; ++ } ++ drops = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getDrops()); + } ++ // Paper end - custom shear drops + // CraftBukkit end +- this.shear(SoundSource.PLAYERS); ++ this.shear(SoundSource.PLAYERS, drops); // Paper + this.gameEvent(GameEvent.SHEAR, player); + if (!this.level().isClientSide) { + itemstack.hurtAndBreak(1, player, getSlotForHand(hand)); +@@ -164,12 +171,28 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM + + @Override + public void shear(SoundSource shearedSoundCategory) { ++ // Paper start - custom shear drops ++ this.shear(shearedSoundCategory, this.generateDefaultDrops()); ++ } ++ ++ @Override ++ public java.util.List generateDefaultDrops() { ++ return java.util.Collections.singletonList(new ItemStack(Items.CARVED_PUMPKIN)); ++ } ++ ++ @Override ++ public void shear(SoundSource shearedSoundCategory, java.util.List drops) { ++ // Paper end - custom shear drops + this.level().playSound((Player) null, (Entity) this, SoundEvents.SNOW_GOLEM_SHEAR, shearedSoundCategory, 1.0F, 1.0F); + if (!this.level().isClientSide()) { + this.setPumpkin(false); +- this.forceDrops = true; // CraftBukkit +- this.spawnAtLocation(new ItemStack(Items.CARVED_PUMPKIN), this.getEyeHeight()); +- this.forceDrops = false; // CraftBukkit ++ // Paper start - custom shear drops (moved drop generation to separate method) ++ for (final ItemStack drop : drops) { ++ this.forceDrops = true; ++ this.spawnAtLocation(drop, this.getEyeHeight()); ++ this.forceDrops = false; ++ } ++ // Paper end - custom shear drops + } + + } +diff --git a/src/main/java/net/minecraft/world/entity/monster/Bogged.java b/src/main/java/net/minecraft/world/entity/monster/Bogged.java +index 9c0bfddd479eef688d2509ad700371c3c860ff1a..754eb747179d9318bc5a3883e5622cc400c4e06c 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Bogged.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Bogged.java +@@ -135,6 +135,11 @@ public class Bogged extends AbstractSkeleton implements Shearable { + } + + private void spawnShearedMushrooms() { ++ // Paper start - shear drops API ++ this.generateShearedMushrooms(this::spawnAtLocation); ++ } ++ private void generateShearedMushrooms(java.util.function.Consumer stackConsumer) { ++ // Paper end - shear drops API + if (this.level() instanceof ServerLevel serverLevel && serverLevel.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) { + LootTable lootTable = serverLevel.getServer().reloadableRegistries().getLootTable(BuiltInLootTables.BOGGED_SHEAR); + LootParams lootParams = new LootParams.Builder(serverLevel) +@@ -143,11 +148,20 @@ public class Bogged extends AbstractSkeleton implements Shearable { + .create(LootContextParamSets.SHEARING); + + for (ItemStack itemStack : lootTable.getRandomItems(lootParams)) { +- this.spawnAtLocation(itemStack); ++ stackConsumer.accept(itemStack); // Paper + } + } + } + ++ // Paper start - shear drops API ++ @Override ++ public java.util.List generateDefaultDrops() { ++ final java.util.List drops = new java.util.ArrayList<>(); ++ this.generateShearedMushrooms(drops::add); ++ return drops; ++ } ++ // Paper end - shear drops API ++ + @Override + public boolean readyForShearing() { + return !this.isSheared() && this.isAlive(); +diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +index 822bfebc779646dde655239ac217bad011a9eee4..444287fea080182df533198f5c659533de86c5dc 100644 +--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java ++++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +@@ -1669,20 +1669,20 @@ public class CraftEventFactory { + player.level().getCraftServer().getPluginManager().callEvent(event); + } + +- public static BlockShearEntityEvent callBlockShearEntityEvent(Entity animal, org.bukkit.block.Block dispenser, CraftItemStack is) { +- BlockShearEntityEvent bse = new BlockShearEntityEvent(dispenser, animal.getBukkitEntity(), is); ++ public static BlockShearEntityEvent callBlockShearEntityEvent(Entity animal, org.bukkit.block.Block dispenser, CraftItemStack is, List drops) { // Paper - custom shear drops ++ BlockShearEntityEvent bse = new BlockShearEntityEvent(dispenser, animal.getBukkitEntity(), is, Lists.transform(drops, CraftItemStack::asCraftMirror)); // Paper - custom shear drops + Bukkit.getPluginManager().callEvent(bse); + return bse; + } + +- public static boolean handlePlayerShearEntityEvent(net.minecraft.world.entity.player.Player player, Entity sheared, ItemStack shears, InteractionHand hand) { ++ public static PlayerShearEntityEvent handlePlayerShearEntityEvent(net.minecraft.world.entity.player.Player player, Entity sheared, ItemStack shears, InteractionHand hand, List drops) { // Paper - custom shear drops + if (!(player instanceof ServerPlayer)) { +- return true; ++ return null; // Paper - custom shear drops + } + +- PlayerShearEntityEvent event = new PlayerShearEntityEvent((Player) player.getBukkitEntity(), sheared.getBukkitEntity(), CraftItemStack.asCraftMirror(shears), (hand == InteractionHand.OFF_HAND ? EquipmentSlot.OFF_HAND : EquipmentSlot.HAND)); ++ PlayerShearEntityEvent event = new PlayerShearEntityEvent((Player) player.getBukkitEntity(), sheared.getBukkitEntity(), CraftItemStack.asCraftMirror(shears), (hand == InteractionHand.OFF_HAND ? EquipmentSlot.OFF_HAND : EquipmentSlot.HAND), Lists.transform(drops, CraftItemStack::asCraftMirror)); // Paper - custom shear drops + Bukkit.getPluginManager().callEvent(event); +- return !event.isCancelled(); ++ return event; // Paper - custom shear drops + } + + public static Cancellable handleStatisticsIncrease(net.minecraft.world.entity.player.Player entityHuman, net.minecraft.stats.Stat statistic, int current, int newValue) { +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java +index 0790f10122546bff1079b522b08d117426364a95..2ba771efa61e109804f3141e95f77613ac952ed1 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java +@@ -65,6 +65,16 @@ public final class CraftItemStack extends ItemStack { + return stack; + } + ++ // Paper start ++ public static java.util.List asNMSCopy(java.util.List originals) { ++ final java.util.List items = new java.util.ArrayList<>(originals.size()); ++ for (final ItemStack original : originals) { ++ items.add(asNMSCopy(original)); ++ } ++ return items; ++ } ++ // Paper end ++ + public static net.minecraft.world.item.ItemStack copyNMSStack(net.minecraft.world.item.ItemStack original, int amount) { + net.minecraft.world.item.ItemStack stack = original.copy(); + stack.setCount(amount); +diff --git a/src/test/java/io/papermc/paper/entity/ShearableDropsTest.java b/src/test/java/io/papermc/paper/entity/ShearableDropsTest.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e612515f7709dbe5d1fa5751337cdc34fce10a98 +--- /dev/null ++++ b/src/test/java/io/papermc/paper/entity/ShearableDropsTest.java +@@ -0,0 +1,34 @@ ++package io.papermc.paper.entity; ++ ++import io.github.classgraph.ClassGraph; ++import io.github.classgraph.ClassInfo; ++import io.github.classgraph.MethodInfoList; ++import io.github.classgraph.ScanResult; ++import java.util.ArrayList; ++import net.minecraft.world.entity.Shearable; ++import org.bukkit.support.AbstractTestingBase; ++import org.junit.jupiter.params.ParameterizedTest; ++import org.junit.jupiter.params.provider.MethodSource; ++ ++import static org.junit.jupiter.api.Assertions.assertEquals; ++ ++class ShearableDropsTest extends AbstractTestingBase { ++ ++ static Iterable parameters() { ++ try (ScanResult scanResult = new ClassGraph() ++ .enableClassInfo() ++ .enableMethodInfo() ++ .whitelistPackages("net.minecraft") ++ .scan() ++ ) { ++ return new ArrayList<>(scanResult.getClassesImplementing(Shearable.class.getName())); ++ } ++ } ++ ++ @ParameterizedTest ++ @MethodSource("parameters") ++ void checkShearableDropOverrides(final ClassInfo classInfo) { ++ final MethodInfoList generateDefaultDrops = classInfo.getDeclaredMethodInfo("generateDefaultDrops"); ++ assertEquals(1, generateDefaultDrops.size(), classInfo.getName() + " doesn't implement Shearable#generateDefaultDrops"); ++ } ++} diff --git a/patches/server/0947-Add-experience-points-API.patch b/patches/server/0947-Add-experience-points-API.patch deleted file mode 100644 index cdd4f08f67..0000000000 --- a/patches/server/0947-Add-experience-points-API.patch +++ /dev/null @@ -1,73 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Lukas Planz -Date: Tue, 5 Sep 2023 20:34:20 +0200 -Subject: [PATCH] Add experience points API - - -diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java -index 230ffd3c4541ef8979605d733b6463e39cd00206..446556028028b25c35dbf3d099bf14a59e32a266 100644 ---- a/src/main/java/net/minecraft/world/entity/player/Player.java -+++ b/src/main/java/net/minecraft/world/entity/player/Player.java -@@ -1855,7 +1855,7 @@ public abstract class Player extends LivingEntity { - } - - public int getXpNeededForNextLevel() { -- return this.experienceLevel >= 30 ? 112 + (this.experienceLevel - 30) * 9 : (this.experienceLevel >= 15 ? 37 + (this.experienceLevel - 15) * 5 : 7 + this.experienceLevel * 2); -+ return this.experienceLevel >= 30 ? 112 + (this.experienceLevel - 30) * 9 : (this.experienceLevel >= 15 ? 37 + (this.experienceLevel - 15) * 5 : 7 + this.experienceLevel * 2); // Paper - diff on change; calculateTotalExperiencePoints - } - // Paper start - send while respecting visibility - private static void sendSoundEffect(Player fromEntity, double x, double y, double z, SoundEvent soundEffect, SoundSource soundCategory, float volume, float pitch) { -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index 483221803ec1f1c78da1f83ccf42b041cb782d7a..e1af41a8d938420694a5342f08b811615dc46003 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -@@ -1902,6 +1902,49 @@ public class CraftPlayer extends CraftHumanEntity implements Player { - Preconditions.checkArgument(exp >= 0, "Total experience points must not be negative (%s)", exp); - this.getHandle().totalExperience = exp; - } -+ // Paper start -+ @Override -+ public int calculateTotalExperiencePoints() { -+ return calculateTotalExperiencePoints(this.getLevel()) + Math.round(this.getExperiencePointsNeededForNextLevel() * getExp()); -+ } -+ -+ @Override -+ public void setExperienceLevelAndProgress(final int totalExperience) { -+ Preconditions.checkArgument(totalExperience >= 0, "Total experience points must not be negative (%s)", totalExperience); -+ int level = calculateLevelsForExperiencePoints(totalExperience); -+ int remainingPoints = totalExperience - calculateTotalExperiencePoints(level); -+ -+ this.getHandle().experienceLevel = level; -+ this.getHandle().experienceProgress = (float) remainingPoints / this.getExperiencePointsNeededForNextLevel(); -+ this.getHandle().lastSentExp = -1; -+ } -+ -+ @Override -+ public int getExperiencePointsNeededForNextLevel() { -+ return this.getHandle().getXpNeededForNextLevel(); -+ } -+ -+ // See https://minecraft.wiki/w/Experience#Leveling_up for reference -+ private int calculateTotalExperiencePoints(int level) { -+ if (level <= 16) { -+ return (int) (Math.pow(level, 2) + 6 * level); -+ } else if (level <= 31) { -+ return (int) (2.5 * Math.pow(level, 2) - 40.5 * level + 360.0); -+ } else { -+ return (int) (4.5 * Math.pow(level, 2) - 162.5 * level + 2220.0); -+ } -+ } -+ -+ private int calculateLevelsForExperiencePoints(int points) { -+ if (points <= 352) { // Level 0-16 -+ return (int) Math.floor(Math.sqrt(points + 9) - 3); -+ } else if (points <= 1507) { // Level 17-31 -+ return (int) Math.floor(8.1 + Math.sqrt(0.4 * (points - (7839.0 / 40.0)))); -+ } else { // 32+ -+ return (int) Math.floor((325.0 / 18.0) + Math.sqrt((2.0 / 9.0) * (points - (54215.0 / 72.0)))); -+ } -+ } -+ // Paper end - - @Override - public void sendExperienceChange(float progress) { diff --git a/patches/server/0948-Add-PlayerShieldDisableEvent.patch b/patches/server/0948-Add-PlayerShieldDisableEvent.patch new file mode 100644 index 0000000000..a4316fd237 --- /dev/null +++ b/patches/server/0948-Add-PlayerShieldDisableEvent.patch @@ -0,0 +1,53 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Cryptite +Date: Mon, 1 May 2023 16:22:43 -0500 +Subject: [PATCH] Add PlayerShieldDisableEvent + +Called whenever a players shield is disabled. This is mainly caused by +attacking players or monsters that carry axes. + +The event, while similar to the PlayerItemCooldownEvent, offers other +behaviour and can hence not be implemented as a childtype of said event. +Specifically, cancelling the event prevents the game events from being +sent to the player. + +Plugins listening to just the PlayerItemCooldownEvent may not want said +sideeffects, meaning the disable event cannot share a handlerlist with +the cooldown event + +diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java +index 446556028028b25c35dbf3d099bf14a59e32a266..70d225dbf5649e6a3fa658291de734f54a6f73dc 100644 +--- a/src/main/java/net/minecraft/world/entity/player/Player.java ++++ b/src/main/java/net/minecraft/world/entity/player/Player.java +@@ -989,7 +989,7 @@ public abstract class Player extends LivingEntity { + protected void blockUsingShield(LivingEntity attacker) { + super.blockUsingShield(attacker); + if (attacker.canDisableShield()) { +- this.disableShield(); ++ this.disableShield(attacker); // Paper - Add PlayerShieldDisableEvent + } + + } +@@ -1473,8 +1473,21 @@ public abstract class Player extends LivingEntity { + this.attack(target); + } + ++ @io.papermc.paper.annotation.DoNotUse @Deprecated // Paper - Add PlayerShieldDisableEvent + public void disableShield() { +- this.getCooldowns().addCooldown(Items.SHIELD, 100); ++ // Paper start - Add PlayerShieldDisableEvent ++ this.disableShield(null); ++ } ++ public void disableShield(@Nullable LivingEntity attacker) { ++ final org.bukkit.entity.Entity finalAttacker = attacker != null ? attacker.getBukkitEntity() : null; ++ if (finalAttacker != null) { ++ final io.papermc.paper.event.player.PlayerShieldDisableEvent shieldDisableEvent = new io.papermc.paper.event.player.PlayerShieldDisableEvent((org.bukkit.entity.Player) getBukkitEntity(), finalAttacker, 100); ++ if (!shieldDisableEvent.callEvent()) return; ++ this.getCooldowns().addCooldown(Items.SHIELD, shieldDisableEvent.getCooldown()); ++ } else { ++ this.getCooldowns().addCooldown(Items.SHIELD, 100); ++ } ++ // Paper end - Add PlayerShieldDisableEvent + this.stopUsingItem(); + this.level().broadcastEntityEvent(this, (byte) 30); + } diff --git a/patches/server/0948-Add-drops-to-shear-events.patch b/patches/server/0948-Add-drops-to-shear-events.patch deleted file mode 100644 index 093e1d3325..0000000000 --- a/patches/server/0948-Add-drops-to-shear-events.patch +++ /dev/null @@ -1,326 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Tue, 18 May 2021 12:32:02 -0700 -Subject: [PATCH] Add drops to shear events - - -diff --git a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java -index fbed5d57db0c0e79996f85571b9af0071fa953c7..a024c697a65bbab27408da1d6a75e531d9719b47 100644 ---- a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java -+++ b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java -@@ -104,11 +104,14 @@ public class ShearsDispenseItemBehavior extends OptionalDispenseItemBehavior { - if (entityliving instanceof Shearable ishearable) { - if (ishearable.readyForShearing()) { - // CraftBukkit start -- if (CraftEventFactory.callBlockShearEntityEvent(entityliving, bukkitBlock, craftItem).isCancelled()) { -+ // Paper start - Add drops to shear events -+ org.bukkit.event.block.BlockShearEntityEvent event = CraftEventFactory.callBlockShearEntityEvent(entityliving, bukkitBlock, craftItem, ishearable.generateDefaultDrops()); -+ if (event.isCancelled()) { -+ // Paper end - Add drops to shear events - continue; - } - // CraftBukkit end -- ishearable.shear(SoundSource.BLOCKS); -+ ishearable.shear(SoundSource.BLOCKS, CraftItemStack.asNMSCopy(event.getDrops())); // Paper - Add drops to shear events - worldserver.gameEvent((Entity) null, (Holder) GameEvent.SHEAR, blockposition); - return true; - } -diff --git a/src/main/java/net/minecraft/world/entity/Shearable.java b/src/main/java/net/minecraft/world/entity/Shearable.java -index 5e8cc5cfac8888628c6d513148f41be09ca65a2c..2ee48ac3b665db2b02bcb1a30ec972d43a3725b0 100644 ---- a/src/main/java/net/minecraft/world/entity/Shearable.java -+++ b/src/main/java/net/minecraft/world/entity/Shearable.java -@@ -3,7 +3,13 @@ package net.minecraft.world.entity; - import net.minecraft.sounds.SoundSource; - - public interface Shearable { -+ default void shear(SoundSource soundCategory, java.util.List drops) { this.shear(soundCategory); } // Paper - Add drops to shear events - void shear(SoundSource shearedSoundCategory); - - boolean readyForShearing(); -+ // Paper start - custom shear drops; ensure all implementing entities override this -+ default java.util.List generateDefaultDrops() { -+ return java.util.Collections.emptyList(); -+ } -+ // Paper end - custom shear drops - } -diff --git a/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java b/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java -index aa125e3043b120935aaa015803f065f99eb8d050..0c21959f57ae88fcd0a4d6dc911c1ce347c96528 100644 ---- a/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java -+++ b/src/main/java/net/minecraft/world/entity/animal/MushroomCow.java -@@ -123,11 +123,18 @@ public class MushroomCow extends Cow implements Shearable, VariantHolder drops = this.generateDefaultDrops(); -+ org.bukkit.event.player.PlayerShearEntityEvent event = CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemstack, hand, drops); -+ if (event != null) { -+ if (event.isCancelled()) { -+ return InteractionResult.PASS; -+ } -+ drops = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getDrops()); - } -+ // Paper end - custom shear drops - // CraftBukkit end -- this.shear(SoundSource.PLAYERS); -+ this.shear(SoundSource.PLAYERS, drops); // Paper - custom shear drops - this.gameEvent(GameEvent.SHEAR, player); - if (!this.level().isClientSide) { - itemstack.hurtAndBreak(1, player, getSlotForHand(hand)); -@@ -164,6 +171,22 @@ public class MushroomCow extends Cow implements Shearable, VariantHolder generateDefaultDrops() { -+ java.util.List dropEntities = new java.util.ArrayList<>(5); -+ for (int i = 0; i < 5; ++i) { -+ dropEntities.add(new ItemStack(this.getVariant().getBlockState().getBlock())); -+ } -+ return dropEntities; -+ } -+ -+ @Override -+ public void shear(SoundSource shearedSoundCategory, java.util.List drops) { // If drops is null, need to generate drops -+ // Paper end - custom shear drops - this.level().playSound((Player) null, (Entity) this, SoundEvents.MOOSHROOM_SHEAR, shearedSoundCategory, 1.0F, 1.0F); - if (!this.level().isClientSide()) { - Cow entitycow = (Cow) EntityType.COW.create(this.level()); -@@ -193,17 +216,12 @@ public class MushroomCow extends Cow implements Shearable, VariantHolder drops = this.generateDefaultDrops(); -+ org.bukkit.event.player.PlayerShearEntityEvent event = CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemstack, hand, drops); -+ if (event != null) { -+ if (event.isCancelled()) { -+ return InteractionResult.PASS; -+ } -+ drops = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getDrops()); - } -+ // Paper end - custom shear drops - // CraftBukkit end -- this.shear(SoundSource.PLAYERS); -+ this.shear(SoundSource.PLAYERS, drops); // Paper - this.gameEvent(GameEvent.SHEAR, player); - itemstack.hurtAndBreak(1, player, getSlotForHand(hand)); - return InteractionResult.SUCCESS; -@@ -276,13 +283,30 @@ public class Sheep extends Animal implements Shearable { - - @Override - public void shear(SoundSource shearedSoundCategory) { -+ // Paper start - custom shear drops -+ this.shear(shearedSoundCategory, this.generateDefaultDrops()); -+ } -+ -+ @Override -+ public java.util.List generateDefaultDrops() { -+ int count = 1 + this.random.nextInt(3); -+ java.util.List dropEntities = new java.util.ArrayList<>(count); -+ for (int j = 0; j < count; ++j) { -+ dropEntities.add(new ItemStack(Sheep.ITEM_BY_DYE.get(this.getColor()))); -+ } -+ return dropEntities; -+ } -+ -+ @Override -+ public void shear(SoundSource shearedSoundCategory, java.util.List drops) { -+ // Paper end - custom shear drops - this.level().playSound((Player) null, (Entity) this, SoundEvents.SHEEP_SHEAR, shearedSoundCategory, 1.0F, 1.0F); - this.setSheared(true); - int i = 1 + this.random.nextInt(3); - -- for (int j = 0; j < i; ++j) { -+ for (final ItemStack drop : drops) { // Paper - custom shear drops (moved drop generation to separate method) - this.forceDrops = true; // CraftBukkit -- ItemEntity entityitem = this.spawnAtLocation((ItemLike) Sheep.ITEM_BY_DYE.get(this.getColor()), 1); -+ ItemEntity entityitem = this.spawnAtLocation(drop, 1); // Paper - custom shear drops - this.forceDrops = false; // CraftBukkit - - if (entityitem != null) { -diff --git a/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java b/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java -index 2de1a2f666da9db1832907e1651dbff948e37252..5c2ed3c39c8eb850f3be1e2ea5b5a7ea266e16d1 100644 ---- a/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java -+++ b/src/main/java/net/minecraft/world/entity/animal/SnowGolem.java -@@ -146,11 +146,18 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM - - if (itemstack.is(Items.SHEARS) && this.readyForShearing()) { - // CraftBukkit start -- if (!CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemstack, hand)) { -- return InteractionResult.PASS; -+ // Paper start - custom shear drops -+ java.util.List drops = this.generateDefaultDrops(); -+ org.bukkit.event.player.PlayerShearEntityEvent event = CraftEventFactory.handlePlayerShearEntityEvent(player, this, itemstack, hand, drops); -+ if (event != null) { -+ if (event.isCancelled()) { -+ return InteractionResult.PASS; -+ } -+ drops = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getDrops()); - } -+ // Paper end - custom shear drops - // CraftBukkit end -- this.shear(SoundSource.PLAYERS); -+ this.shear(SoundSource.PLAYERS, drops); // Paper - this.gameEvent(GameEvent.SHEAR, player); - if (!this.level().isClientSide) { - itemstack.hurtAndBreak(1, player, getSlotForHand(hand)); -@@ -164,12 +171,28 @@ public class SnowGolem extends AbstractGolem implements Shearable, RangedAttackM - - @Override - public void shear(SoundSource shearedSoundCategory) { -+ // Paper start - custom shear drops -+ this.shear(shearedSoundCategory, this.generateDefaultDrops()); -+ } -+ -+ @Override -+ public java.util.List generateDefaultDrops() { -+ return java.util.Collections.singletonList(new ItemStack(Items.CARVED_PUMPKIN)); -+ } -+ -+ @Override -+ public void shear(SoundSource shearedSoundCategory, java.util.List drops) { -+ // Paper end - custom shear drops - this.level().playSound((Player) null, (Entity) this, SoundEvents.SNOW_GOLEM_SHEAR, shearedSoundCategory, 1.0F, 1.0F); - if (!this.level().isClientSide()) { - this.setPumpkin(false); -- this.forceDrops = true; // CraftBukkit -- this.spawnAtLocation(new ItemStack(Items.CARVED_PUMPKIN), this.getEyeHeight()); -- this.forceDrops = false; // CraftBukkit -+ // Paper start - custom shear drops (moved drop generation to separate method) -+ for (final ItemStack drop : drops) { -+ this.forceDrops = true; -+ this.spawnAtLocation(drop, this.getEyeHeight()); -+ this.forceDrops = false; -+ } -+ // Paper end - custom shear drops - } - - } -diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java -index 822bfebc779646dde655239ac217bad011a9eee4..444287fea080182df533198f5c659533de86c5dc 100644 ---- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java -+++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java -@@ -1669,20 +1669,20 @@ public class CraftEventFactory { - player.level().getCraftServer().getPluginManager().callEvent(event); - } - -- public static BlockShearEntityEvent callBlockShearEntityEvent(Entity animal, org.bukkit.block.Block dispenser, CraftItemStack is) { -- BlockShearEntityEvent bse = new BlockShearEntityEvent(dispenser, animal.getBukkitEntity(), is); -+ public static BlockShearEntityEvent callBlockShearEntityEvent(Entity animal, org.bukkit.block.Block dispenser, CraftItemStack is, List drops) { // Paper - custom shear drops -+ BlockShearEntityEvent bse = new BlockShearEntityEvent(dispenser, animal.getBukkitEntity(), is, Lists.transform(drops, CraftItemStack::asCraftMirror)); // Paper - custom shear drops - Bukkit.getPluginManager().callEvent(bse); - return bse; - } - -- public static boolean handlePlayerShearEntityEvent(net.minecraft.world.entity.player.Player player, Entity sheared, ItemStack shears, InteractionHand hand) { -+ public static PlayerShearEntityEvent handlePlayerShearEntityEvent(net.minecraft.world.entity.player.Player player, Entity sheared, ItemStack shears, InteractionHand hand, List drops) { // Paper - custom shear drops - if (!(player instanceof ServerPlayer)) { -- return true; -+ return null; // Paper - custom shear drops - } - -- PlayerShearEntityEvent event = new PlayerShearEntityEvent((Player) player.getBukkitEntity(), sheared.getBukkitEntity(), CraftItemStack.asCraftMirror(shears), (hand == InteractionHand.OFF_HAND ? EquipmentSlot.OFF_HAND : EquipmentSlot.HAND)); -+ PlayerShearEntityEvent event = new PlayerShearEntityEvent((Player) player.getBukkitEntity(), sheared.getBukkitEntity(), CraftItemStack.asCraftMirror(shears), (hand == InteractionHand.OFF_HAND ? EquipmentSlot.OFF_HAND : EquipmentSlot.HAND), Lists.transform(drops, CraftItemStack::asCraftMirror)); // Paper - custom shear drops - Bukkit.getPluginManager().callEvent(event); -- return !event.isCancelled(); -+ return event; // Paper - custom shear drops - } - - public static Cancellable handleStatisticsIncrease(net.minecraft.world.entity.player.Player entityHuman, net.minecraft.stats.Stat statistic, int current, int newValue) { -diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java -index 0790f10122546bff1079b522b08d117426364a95..2ba771efa61e109804f3141e95f77613ac952ed1 100644 ---- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java -+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java -@@ -65,6 +65,16 @@ public final class CraftItemStack extends ItemStack { - return stack; - } - -+ // Paper start -+ public static java.util.List asNMSCopy(java.util.List originals) { -+ final java.util.List items = new java.util.ArrayList<>(originals.size()); -+ for (final ItemStack original : originals) { -+ items.add(asNMSCopy(original)); -+ } -+ return items; -+ } -+ // Paper end -+ - public static net.minecraft.world.item.ItemStack copyNMSStack(net.minecraft.world.item.ItemStack original, int amount) { - net.minecraft.world.item.ItemStack stack = original.copy(); - stack.setCount(amount); -diff --git a/src/test/java/io/papermc/paper/entity/ShearableDropsTest.java b/src/test/java/io/papermc/paper/entity/ShearableDropsTest.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e612515f7709dbe5d1fa5751337cdc34fce10a98 ---- /dev/null -+++ b/src/test/java/io/papermc/paper/entity/ShearableDropsTest.java -@@ -0,0 +1,34 @@ -+package io.papermc.paper.entity; -+ -+import io.github.classgraph.ClassGraph; -+import io.github.classgraph.ClassInfo; -+import io.github.classgraph.MethodInfoList; -+import io.github.classgraph.ScanResult; -+import java.util.ArrayList; -+import net.minecraft.world.entity.Shearable; -+import org.bukkit.support.AbstractTestingBase; -+import org.junit.jupiter.params.ParameterizedTest; -+import org.junit.jupiter.params.provider.MethodSource; -+ -+import static org.junit.jupiter.api.Assertions.assertEquals; -+ -+class ShearableDropsTest extends AbstractTestingBase { -+ -+ static Iterable parameters() { -+ try (ScanResult scanResult = new ClassGraph() -+ .enableClassInfo() -+ .enableMethodInfo() -+ .whitelistPackages("net.minecraft") -+ .scan() -+ ) { -+ return new ArrayList<>(scanResult.getClassesImplementing(Shearable.class.getName())); -+ } -+ } -+ -+ @ParameterizedTest -+ @MethodSource("parameters") -+ void checkShearableDropOverrides(final ClassInfo classInfo) { -+ final MethodInfoList generateDefaultDrops = classInfo.getDeclaredMethodInfo("generateDefaultDrops"); -+ assertEquals(1, generateDefaultDrops.size(), classInfo.getName() + " doesn't implement Shearable#generateDefaultDrops"); -+ } -+} diff --git a/patches/server/0949-Add-PlayerShieldDisableEvent.patch b/patches/server/0949-Add-PlayerShieldDisableEvent.patch deleted file mode 100644 index a4316fd237..0000000000 --- a/patches/server/0949-Add-PlayerShieldDisableEvent.patch +++ /dev/null @@ -1,53 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Cryptite -Date: Mon, 1 May 2023 16:22:43 -0500 -Subject: [PATCH] Add PlayerShieldDisableEvent - -Called whenever a players shield is disabled. This is mainly caused by -attacking players or monsters that carry axes. - -The event, while similar to the PlayerItemCooldownEvent, offers other -behaviour and can hence not be implemented as a childtype of said event. -Specifically, cancelling the event prevents the game events from being -sent to the player. - -Plugins listening to just the PlayerItemCooldownEvent may not want said -sideeffects, meaning the disable event cannot share a handlerlist with -the cooldown event - -diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java -index 446556028028b25c35dbf3d099bf14a59e32a266..70d225dbf5649e6a3fa658291de734f54a6f73dc 100644 ---- a/src/main/java/net/minecraft/world/entity/player/Player.java -+++ b/src/main/java/net/minecraft/world/entity/player/Player.java -@@ -989,7 +989,7 @@ public abstract class Player extends LivingEntity { - protected void blockUsingShield(LivingEntity attacker) { - super.blockUsingShield(attacker); - if (attacker.canDisableShield()) { -- this.disableShield(); -+ this.disableShield(attacker); // Paper - Add PlayerShieldDisableEvent - } - - } -@@ -1473,8 +1473,21 @@ public abstract class Player extends LivingEntity { - this.attack(target); - } - -+ @io.papermc.paper.annotation.DoNotUse @Deprecated // Paper - Add PlayerShieldDisableEvent - public void disableShield() { -- this.getCooldowns().addCooldown(Items.SHIELD, 100); -+ // Paper start - Add PlayerShieldDisableEvent -+ this.disableShield(null); -+ } -+ public void disableShield(@Nullable LivingEntity attacker) { -+ final org.bukkit.entity.Entity finalAttacker = attacker != null ? attacker.getBukkitEntity() : null; -+ if (finalAttacker != null) { -+ final io.papermc.paper.event.player.PlayerShieldDisableEvent shieldDisableEvent = new io.papermc.paper.event.player.PlayerShieldDisableEvent((org.bukkit.entity.Player) getBukkitEntity(), finalAttacker, 100); -+ if (!shieldDisableEvent.callEvent()) return; -+ this.getCooldowns().addCooldown(Items.SHIELD, shieldDisableEvent.getCooldown()); -+ } else { -+ this.getCooldowns().addCooldown(Items.SHIELD, 100); -+ } -+ // Paper end - Add PlayerShieldDisableEvent - this.stopUsingItem(); - this.level().broadcastEntityEvent(this, (byte) 30); - } diff --git a/patches/server/0949-Validate-ResourceLocation-in-NBT-reading.patch b/patches/server/0949-Validate-ResourceLocation-in-NBT-reading.patch new file mode 100644 index 0000000000..0e1c0b7e08 --- /dev/null +++ b/patches/server/0949-Validate-ResourceLocation-in-NBT-reading.patch @@ -0,0 +1,143 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Nassim Jahnke +Date: Thu, 4 Jan 2024 13:49:14 +0100 +Subject: [PATCH] Validate ResourceLocation in NBT reading + + +diff --git a/src/main/java/net/minecraft/nbt/NbtUtils.java b/src/main/java/net/minecraft/nbt/NbtUtils.java +index 1d7c20fe14882fdeddf315a8923669e3385652f5..f88dd37783b3c155c23b547c360b8d3c16e030c0 100644 +--- a/src/main/java/net/minecraft/nbt/NbtUtils.java ++++ b/src/main/java/net/minecraft/nbt/NbtUtils.java +@@ -149,8 +149,10 @@ public final class NbtUtils { + if (!nbt.contains("Name", 8)) { + return Blocks.AIR.defaultBlockState(); + } else { +- ResourceLocation resourceLocation = new ResourceLocation(nbt.getString("Name")); +- Optional> optional = blockLookup.get(ResourceKey.create(Registries.BLOCK, resourceLocation)); ++ // Paper start - Validate resource location ++ ResourceLocation resourceLocation = ResourceLocation.tryParse(nbt.getString("Name")); ++ Optional> optional = resourceLocation != null ? blockLookup.get(ResourceKey.create(Registries.BLOCK, resourceLocation)) : Optional.empty(); ++ // Paper end - Validate resource location + if (optional.isEmpty()) { + return Blocks.AIR.defaultBlockState(); + } else { +diff --git a/src/main/java/net/minecraft/resources/ResourceLocation.java b/src/main/java/net/minecraft/resources/ResourceLocation.java +index d93b623ce973b63d4f3a77bfe459f51af7cb3c1c..2c4553312f2f37f8613ac813708b4b95f9675e9f 100644 +--- a/src/main/java/net/minecraft/resources/ResourceLocation.java ++++ b/src/main/java/net/minecraft/resources/ResourceLocation.java +@@ -37,6 +37,13 @@ public class ResourceLocation implements Comparable { + private final String path; + + protected ResourceLocation(String namespace, String path, @Nullable ResourceLocation.Dummy extraData) { ++ // Paper start - Validate ResourceLocation ++ // Check for the max network string length (capped at Short.MAX_VALUE) as well as the max bytes of a StringTag (length written as an unsigned short) ++ final String resourceLocation = namespace + ":" + path; ++ if (resourceLocation.length() > Short.MAX_VALUE || io.netty.buffer.ByteBufUtil.utf8MaxBytes(resourceLocation) > 2 * Short.MAX_VALUE + 1) { ++ throw new ResourceLocationException("Resource location too long: " + resourceLocation); ++ } ++ // Paper end - Validate ResourceLocation + this.namespace = namespace; + this.path = path; + } +diff --git a/src/main/java/net/minecraft/world/entity/EntityType.java b/src/main/java/net/minecraft/world/entity/EntityType.java +index 8c7cc9c5af0b8d8bef9b6e2d3d3e723cd76f3212..56b51096ca4147363a843accf6ef2510f05e8f1a 100644 +--- a/src/main/java/net/minecraft/world/entity/EntityType.java ++++ b/src/main/java/net/minecraft/world/entity/EntityType.java +@@ -623,7 +623,7 @@ public class EntityType implements FeatureElement, EntityTypeT + } + + public static Optional> by(CompoundTag nbt) { +- return BuiltInRegistries.ENTITY_TYPE.getOptional(new ResourceLocation(nbt.getString("id"))); ++ return BuiltInRegistries.ENTITY_TYPE.getOptional(ResourceLocation.tryParse(nbt.getString("id"))); // Paper - Validate ResourceLocation + } + + @Nullable +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index 68c312dc39ef6fdb88e201ba096c9bd99aebddd7..0416af1342dad6a6789aa743aadf61b32b70f338 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -911,12 +911,13 @@ public abstract class LivingEntity extends Entity implements Attackable { + + if (nbt.contains("SleepingX", 99) && nbt.contains("SleepingY", 99) && nbt.contains("SleepingZ", 99)) { + BlockPos blockposition = new BlockPos(nbt.getInt("SleepingX"), nbt.getInt("SleepingY"), nbt.getInt("SleepingZ")); +- ++ if (this.position().distanceToSqr(blockposition.getX(), blockposition.getY(), blockposition.getZ()) < 16 * 16) { // Paper - The sleeping pos will always also set the actual pos, so a desync suggests something is wrong + this.setSleepingPos(blockposition); + this.entityData.set(LivingEntity.DATA_POSE, Pose.SLEEPING); + if (!this.firstTick) { + this.setPosToBed(blockposition); + } ++ } // Paper - The sleeping pos will always also set the actual pos, so a desync suggests something is wrong + } + + if (nbt.contains("Brain", 10)) { +diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java +index 7f781afb996dee380487f5be77c2149525ef0cca..0fd307e214f544284606ac8aa57f3f6bdca8bca6 100644 +--- a/src/main/java/net/minecraft/world/entity/Mob.java ++++ b/src/main/java/net/minecraft/world/entity/Mob.java +@@ -645,7 +645,7 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti + + this.setLeftHanded(nbt.getBoolean("LeftHanded")); + if (nbt.contains("DeathLootTable", 8)) { +- this.lootTable = ResourceKey.create(Registries.LOOT_TABLE, new ResourceLocation(nbt.getString("DeathLootTable"))); ++ this.lootTable = net.minecraft.Optionull.map(ResourceLocation.tryParse(nbt.getString("DeathLootTable")), rl -> ResourceKey.create(Registries.LOOT_TABLE, rl)); // Paper - Validate ResourceLocation + this.lootTableSeed = nbt.getLong("DeathLootTableSeed"); + } + +diff --git a/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java b/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java +index fc5913910f5614cd3e10cd9c1aa482a4b70ac260..31b8a8bf78d52b5f11b68e780ec09bf78e7bda84 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java +@@ -561,7 +561,7 @@ public abstract class AbstractArrow extends Projectile { + this.setCritArrow(nbt.getBoolean("crit")); + this.setPierceLevel(nbt.getByte("PierceLevel")); + if (nbt.contains("SoundEvent", 8)) { +- this.soundEvent = (SoundEvent) BuiltInRegistries.SOUND_EVENT.getOptional(new ResourceLocation(nbt.getString("SoundEvent"))).orElse(this.getDefaultHitGroundSoundEvent()); ++ this.soundEvent = (SoundEvent) BuiltInRegistries.SOUND_EVENT.getOptional(ResourceLocation.tryParse(nbt.getString("SoundEvent"))).orElse(this.getDefaultHitGroundSoundEvent()); // Paper - Validate resource location + } + + this.setShotFromCrossbow(nbt.getBoolean("ShotFromCrossbow")); +diff --git a/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java b/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java +index f33e5cf6d456e615050047e924d9b24268a2c51e..c660e9b1e9635011b32fa19cf64118d64408f158 100644 +--- a/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java ++++ b/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java +@@ -72,7 +72,7 @@ public interface ContainerEntity extends Container, MenuProvider { + default void readChestVehicleSaveData(CompoundTag nbt, HolderLookup.Provider registriesLookup) { + this.clearItemStacks(); + if (nbt.contains("LootTable", 8)) { +- this.setLootTable(ResourceKey.create(Registries.LOOT_TABLE, new ResourceLocation(nbt.getString("LootTable")))); ++ this.setLootTable(net.minecraft.Optionull.map(ResourceLocation.tryParse(nbt.getString("LootTable")), rl -> ResourceKey.create(Registries.LOOT_TABLE, rl))); // Paper - Validate ResourceLocation + this.setLootTableSeed(nbt.getLong("LootTableSeed")); + } + ContainerHelper.loadAllItems(nbt, this.getItemStacks(), registriesLookup); // Paper - always save the items, table may still remain +diff --git a/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java +index f40b2582d9087f9dbb5cab950304698f33fdd879..a99fe191c429bb528209dd0f31b509acf9cccbb5 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java +@@ -295,7 +295,12 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit + while (iterator.hasNext()) { + String s = (String) iterator.next(); + +- this.recipesUsed.put(new ResourceLocation(s), nbttagcompound1.getInt(s)); ++ // Paper start - Validate ResourceLocation ++ final ResourceLocation resourceLocation = ResourceLocation.tryParse(s); ++ if (resourceLocation != null) { ++ this.recipesUsed.put(resourceLocation, nbttagcompound1.getInt(s)); ++ } ++ // Paper end - Validate ResourceLocation + } + + // Paper start - cook speed multiplier API +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BrushableBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BrushableBlockEntity.java +index f78a9698afc87408fc46de2d3d00c923500885f4..dc02a3d84b397f634f77f4df9c06e245cc4dcb75 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BrushableBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BrushableBlockEntity.java +@@ -202,7 +202,7 @@ public class BrushableBlockEntity extends BlockEntity { + + private boolean tryLoadLootTable(CompoundTag nbt) { + if (nbt.contains("LootTable", 8)) { +- this.lootTable = ResourceKey.create(Registries.LOOT_TABLE, new ResourceLocation(nbt.getString("LootTable"))); ++ this.lootTable = net.minecraft.Optionull.map(ResourceLocation.tryParse(nbt.getString("LootTable")), rl -> ResourceKey.create(Registries.LOOT_TABLE, rl)); // Paper - Validate ResourceLocation + this.lootTableSeed = nbt.getLong("LootTableSeed"); + return true; + } else { diff --git a/patches/server/0950-Properly-handle-experience-dropping-on-block-break.patch b/patches/server/0950-Properly-handle-experience-dropping-on-block-break.patch new file mode 100644 index 0000000000..3ee724786e --- /dev/null +++ b/patches/server/0950-Properly-handle-experience-dropping-on-block-break.patch @@ -0,0 +1,94 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> +Date: Sat, 30 Dec 2023 15:00:06 -0500 +Subject: [PATCH] Properly handle experience dropping on block break + +This causes spawnAfterBreak to spawn xp by default, removing the need to manually add xp wherever this method is used. +For classes that use custom xp amounts, they can drop the resources with disabling + +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 2f018922fb12dc4c3c9c28c7b0597a2b97d1cb02..51c4877ffb77ece41e90d9c6ac06c04dc941e35c 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -618,7 +618,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + if (drop) { + BlockEntity tileentity = iblockdata.hasBlockEntity() ? this.getBlockEntity(pos) : null; + +- Block.dropResources(iblockdata, this, pos, tileentity, breakingEntity, ItemStack.EMPTY); ++ Block.dropResources(iblockdata, this, pos, tileentity, breakingEntity, ItemStack.EMPTY, false); // Paper - Properly handle xp dropping ++ iblockdata.getBlock().popExperience((ServerLevel) this, pos, xp, breakingEntity); // Paper - Properly handle xp dropping; custom amount + } + + boolean flag1 = this.setBlock(pos, fluid.createLegacyBlock(), 3, maxUpdateDepth); +diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java +index a757a45d423e1729c9374391df4186ae0522e1ac..7f0c0ca49e7575c18935b71e3180d112440289f7 100644 +--- a/src/main/java/net/minecraft/world/level/block/Block.java ++++ b/src/main/java/net/minecraft/world/level/block/Block.java +@@ -313,23 +313,31 @@ public class Block extends BlockBehaviour implements ItemLike { + for (ItemStack drop : Block.getDrops(state, serverLevel, pos, blockEntity)) { + items.add(org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(drop)); + } ++ Block block = state.getBlock(); // Paper - Properly handle xp dropping + io.papermc.paper.event.block.BlockBreakBlockEvent event = new io.papermc.paper.event.block.BlockBreakBlockEvent(org.bukkit.craftbukkit.block.CraftBlock.at(levelAccessor, pos), org.bukkit.craftbukkit.block.CraftBlock.at(levelAccessor, source), items); ++ event.setExpToDrop(block.getExpDrop(state, serverLevel, pos, net.minecraft.world.item.ItemStack.EMPTY, true)); // Paper - Properly handle xp dropping + event.callEvent(); + for (org.bukkit.inventory.ItemStack drop : event.getDrops()) { + popResource(serverLevel, pos, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(drop)); + } +- state.spawnAfterBreak(serverLevel, pos, ItemStack.EMPTY, true); ++ state.spawnAfterBreak(serverLevel, pos, ItemStack.EMPTY, false); // Paper - Properly handle xp dropping ++ block.popExperience(serverLevel, pos, event.getExpToDrop()); // Paper - Properly handle xp dropping + } + return true; + } + // Paper end - Add BlockBreakBlockEvent + + public static void dropResources(BlockState state, Level world, BlockPos pos, @Nullable BlockEntity blockEntity, @Nullable Entity entity, ItemStack tool) { ++ // Paper start - Properly handle xp dropping ++ dropResources(state, world, pos, blockEntity, entity, tool, true); ++ } ++ public static void dropResources(BlockState state, Level world, BlockPos pos, @Nullable BlockEntity blockEntity, @Nullable Entity entity, ItemStack tool, boolean dropExperience) { ++ // Paper end - Properly handle xp dropping + if (world instanceof ServerLevel) { + Block.getDrops(state, (ServerLevel) world, pos, blockEntity, entity, tool).forEach((itemstack1) -> { + Block.popResource(world, pos, itemstack1); + }); +- state.spawnAfterBreak((ServerLevel) world, pos, tool, true); ++ state.spawnAfterBreak((ServerLevel) world, pos, tool, dropExperience); // Paper - Properly handle xp dropping + } + + } +@@ -413,7 +421,7 @@ public class Block extends BlockBehaviour implements ItemLike { + player.awardStat(Stats.BLOCK_MINED.get(this)); + player.causeFoodExhaustion(0.005F, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.BLOCK_MINED); // CraftBukkit - EntityExhaustionEvent + if (includeDrops) { // Paper - fix drops not preventing stats/food exhaustion +- Block.dropResources(state, world, pos, blockEntity, player, tool); ++ Block.dropResources(state, world, pos, blockEntity, player, tool, dropExp); // Paper - Properly handle xp dropping + } // Paper - fix drops not preventing stats/food exhaustion + } + +diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +index c762a006b3f586b32209c20e85f6b8bf169dbd06..f87d9cb38caf3bf92fd32f2118f76799ede418db 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java ++++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +@@ -1118,6 +1118,7 @@ public abstract class BlockBehaviour implements FeatureElement { + + public void spawnAfterBreak(ServerLevel world, BlockPos pos, ItemStack tool, boolean dropExperience) { + this.getBlock().spawnAfterBreak(this.asState(), world, pos, tool, dropExperience); ++ if (dropExperience) {getBlock().popExperience(world, pos, this.getBlock().getExpDrop(asState(), world, pos, tool, true));} // Paper - Properly handle xp dropping + } + + public List getDrops(LootParams.Builder builder) { +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java +index 461a66c323a74db5a70981fafc5fa20f54f0f40d..ac11f18690434922179b61ffcc3036dea025b0cb 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java +@@ -509,7 +509,7 @@ public class CraftBlock implements Block { + + // Modelled off EntityHuman#hasBlock + if (block != Blocks.AIR && (item == null || !iblockdata.requiresCorrectToolForDrops() || nmsItem.isCorrectToolForDrops(iblockdata))) { +- net.minecraft.world.level.block.Block.dropResources(iblockdata, this.world.getMinecraftWorld(), this.position, this.world.getBlockEntity(this.position), null, nmsItem); ++ net.minecraft.world.level.block.Block.dropResources(iblockdata, this.world.getMinecraftWorld(), this.position, this.world.getBlockEntity(this.position), null, nmsItem, false); // Paper - Properly handle xp dropping + // Paper start - improve Block#breanNaturally + if (triggerEffect) { + if (iblockdata.getBlock() instanceof net.minecraft.world.level.block.BaseFireBlock) { diff --git a/patches/server/0950-Validate-ResourceLocation-in-NBT-reading.patch b/patches/server/0950-Validate-ResourceLocation-in-NBT-reading.patch deleted file mode 100644 index 0e1c0b7e08..0000000000 --- a/patches/server/0950-Validate-ResourceLocation-in-NBT-reading.patch +++ /dev/null @@ -1,143 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Nassim Jahnke -Date: Thu, 4 Jan 2024 13:49:14 +0100 -Subject: [PATCH] Validate ResourceLocation in NBT reading - - -diff --git a/src/main/java/net/minecraft/nbt/NbtUtils.java b/src/main/java/net/minecraft/nbt/NbtUtils.java -index 1d7c20fe14882fdeddf315a8923669e3385652f5..f88dd37783b3c155c23b547c360b8d3c16e030c0 100644 ---- a/src/main/java/net/minecraft/nbt/NbtUtils.java -+++ b/src/main/java/net/minecraft/nbt/NbtUtils.java -@@ -149,8 +149,10 @@ public final class NbtUtils { - if (!nbt.contains("Name", 8)) { - return Blocks.AIR.defaultBlockState(); - } else { -- ResourceLocation resourceLocation = new ResourceLocation(nbt.getString("Name")); -- Optional> optional = blockLookup.get(ResourceKey.create(Registries.BLOCK, resourceLocation)); -+ // Paper start - Validate resource location -+ ResourceLocation resourceLocation = ResourceLocation.tryParse(nbt.getString("Name")); -+ Optional> optional = resourceLocation != null ? blockLookup.get(ResourceKey.create(Registries.BLOCK, resourceLocation)) : Optional.empty(); -+ // Paper end - Validate resource location - if (optional.isEmpty()) { - return Blocks.AIR.defaultBlockState(); - } else { -diff --git a/src/main/java/net/minecraft/resources/ResourceLocation.java b/src/main/java/net/minecraft/resources/ResourceLocation.java -index d93b623ce973b63d4f3a77bfe459f51af7cb3c1c..2c4553312f2f37f8613ac813708b4b95f9675e9f 100644 ---- a/src/main/java/net/minecraft/resources/ResourceLocation.java -+++ b/src/main/java/net/minecraft/resources/ResourceLocation.java -@@ -37,6 +37,13 @@ public class ResourceLocation implements Comparable { - private final String path; - - protected ResourceLocation(String namespace, String path, @Nullable ResourceLocation.Dummy extraData) { -+ // Paper start - Validate ResourceLocation -+ // Check for the max network string length (capped at Short.MAX_VALUE) as well as the max bytes of a StringTag (length written as an unsigned short) -+ final String resourceLocation = namespace + ":" + path; -+ if (resourceLocation.length() > Short.MAX_VALUE || io.netty.buffer.ByteBufUtil.utf8MaxBytes(resourceLocation) > 2 * Short.MAX_VALUE + 1) { -+ throw new ResourceLocationException("Resource location too long: " + resourceLocation); -+ } -+ // Paper end - Validate ResourceLocation - this.namespace = namespace; - this.path = path; - } -diff --git a/src/main/java/net/minecraft/world/entity/EntityType.java b/src/main/java/net/minecraft/world/entity/EntityType.java -index 8c7cc9c5af0b8d8bef9b6e2d3d3e723cd76f3212..56b51096ca4147363a843accf6ef2510f05e8f1a 100644 ---- a/src/main/java/net/minecraft/world/entity/EntityType.java -+++ b/src/main/java/net/minecraft/world/entity/EntityType.java -@@ -623,7 +623,7 @@ public class EntityType implements FeatureElement, EntityTypeT - } - - public static Optional> by(CompoundTag nbt) { -- return BuiltInRegistries.ENTITY_TYPE.getOptional(new ResourceLocation(nbt.getString("id"))); -+ return BuiltInRegistries.ENTITY_TYPE.getOptional(ResourceLocation.tryParse(nbt.getString("id"))); // Paper - Validate ResourceLocation - } - - @Nullable -diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index 68c312dc39ef6fdb88e201ba096c9bd99aebddd7..0416af1342dad6a6789aa743aadf61b32b70f338 100644 ---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -911,12 +911,13 @@ public abstract class LivingEntity extends Entity implements Attackable { - - if (nbt.contains("SleepingX", 99) && nbt.contains("SleepingY", 99) && nbt.contains("SleepingZ", 99)) { - BlockPos blockposition = new BlockPos(nbt.getInt("SleepingX"), nbt.getInt("SleepingY"), nbt.getInt("SleepingZ")); -- -+ if (this.position().distanceToSqr(blockposition.getX(), blockposition.getY(), blockposition.getZ()) < 16 * 16) { // Paper - The sleeping pos will always also set the actual pos, so a desync suggests something is wrong - this.setSleepingPos(blockposition); - this.entityData.set(LivingEntity.DATA_POSE, Pose.SLEEPING); - if (!this.firstTick) { - this.setPosToBed(blockposition); - } -+ } // Paper - The sleeping pos will always also set the actual pos, so a desync suggests something is wrong - } - - if (nbt.contains("Brain", 10)) { -diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java -index 7f781afb996dee380487f5be77c2149525ef0cca..0fd307e214f544284606ac8aa57f3f6bdca8bca6 100644 ---- a/src/main/java/net/minecraft/world/entity/Mob.java -+++ b/src/main/java/net/minecraft/world/entity/Mob.java -@@ -645,7 +645,7 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti - - this.setLeftHanded(nbt.getBoolean("LeftHanded")); - if (nbt.contains("DeathLootTable", 8)) { -- this.lootTable = ResourceKey.create(Registries.LOOT_TABLE, new ResourceLocation(nbt.getString("DeathLootTable"))); -+ this.lootTable = net.minecraft.Optionull.map(ResourceLocation.tryParse(nbt.getString("DeathLootTable")), rl -> ResourceKey.create(Registries.LOOT_TABLE, rl)); // Paper - Validate ResourceLocation - this.lootTableSeed = nbt.getLong("DeathLootTableSeed"); - } - -diff --git a/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java b/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java -index fc5913910f5614cd3e10cd9c1aa482a4b70ac260..31b8a8bf78d52b5f11b68e780ec09bf78e7bda84 100644 ---- a/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java -+++ b/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java -@@ -561,7 +561,7 @@ public abstract class AbstractArrow extends Projectile { - this.setCritArrow(nbt.getBoolean("crit")); - this.setPierceLevel(nbt.getByte("PierceLevel")); - if (nbt.contains("SoundEvent", 8)) { -- this.soundEvent = (SoundEvent) BuiltInRegistries.SOUND_EVENT.getOptional(new ResourceLocation(nbt.getString("SoundEvent"))).orElse(this.getDefaultHitGroundSoundEvent()); -+ this.soundEvent = (SoundEvent) BuiltInRegistries.SOUND_EVENT.getOptional(ResourceLocation.tryParse(nbt.getString("SoundEvent"))).orElse(this.getDefaultHitGroundSoundEvent()); // Paper - Validate resource location - } - - this.setShotFromCrossbow(nbt.getBoolean("ShotFromCrossbow")); -diff --git a/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java b/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java -index f33e5cf6d456e615050047e924d9b24268a2c51e..c660e9b1e9635011b32fa19cf64118d64408f158 100644 ---- a/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java -+++ b/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java -@@ -72,7 +72,7 @@ public interface ContainerEntity extends Container, MenuProvider { - default void readChestVehicleSaveData(CompoundTag nbt, HolderLookup.Provider registriesLookup) { - this.clearItemStacks(); - if (nbt.contains("LootTable", 8)) { -- this.setLootTable(ResourceKey.create(Registries.LOOT_TABLE, new ResourceLocation(nbt.getString("LootTable")))); -+ this.setLootTable(net.minecraft.Optionull.map(ResourceLocation.tryParse(nbt.getString("LootTable")), rl -> ResourceKey.create(Registries.LOOT_TABLE, rl))); // Paper - Validate ResourceLocation - this.setLootTableSeed(nbt.getLong("LootTableSeed")); - } - ContainerHelper.loadAllItems(nbt, this.getItemStacks(), registriesLookup); // Paper - always save the items, table may still remain -diff --git a/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java -index f40b2582d9087f9dbb5cab950304698f33fdd879..a99fe191c429bb528209dd0f31b509acf9cccbb5 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java -@@ -295,7 +295,12 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit - while (iterator.hasNext()) { - String s = (String) iterator.next(); - -- this.recipesUsed.put(new ResourceLocation(s), nbttagcompound1.getInt(s)); -+ // Paper start - Validate ResourceLocation -+ final ResourceLocation resourceLocation = ResourceLocation.tryParse(s); -+ if (resourceLocation != null) { -+ this.recipesUsed.put(resourceLocation, nbttagcompound1.getInt(s)); -+ } -+ // Paper end - Validate ResourceLocation - } - - // Paper start - cook speed multiplier API -diff --git a/src/main/java/net/minecraft/world/level/block/entity/BrushableBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BrushableBlockEntity.java -index f78a9698afc87408fc46de2d3d00c923500885f4..dc02a3d84b397f634f77f4df9c06e245cc4dcb75 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/BrushableBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/BrushableBlockEntity.java -@@ -202,7 +202,7 @@ public class BrushableBlockEntity extends BlockEntity { - - private boolean tryLoadLootTable(CompoundTag nbt) { - if (nbt.contains("LootTable", 8)) { -- this.lootTable = ResourceKey.create(Registries.LOOT_TABLE, new ResourceLocation(nbt.getString("LootTable"))); -+ this.lootTable = net.minecraft.Optionull.map(ResourceLocation.tryParse(nbt.getString("LootTable")), rl -> ResourceKey.create(Registries.LOOT_TABLE, rl)); // Paper - Validate ResourceLocation - this.lootTableSeed = nbt.getLong("LootTableSeed"); - return true; - } else { diff --git a/patches/server/0951-Fixup-NamespacedKey-handling.patch b/patches/server/0951-Fixup-NamespacedKey-handling.patch new file mode 100644 index 0000000000..20c52c2fba --- /dev/null +++ b/patches/server/0951-Fixup-NamespacedKey-handling.patch @@ -0,0 +1,125 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Nassim Jahnke +Date: Sat, 6 Jan 2024 14:31:00 +0100 +Subject: [PATCH] Fixup NamespacedKey handling + + +diff --git a/src/main/java/com/destroystokyo/paper/loottable/PaperContainerEntityLootableInventory.java b/src/main/java/com/destroystokyo/paper/loottable/PaperContainerEntityLootableInventory.java +index 7f5283a8ee74253e326fe994f20ae00cf3c75e1b..183f31c81d9c0a966e6b20c2cff3d279dfc7e622 100644 +--- a/src/main/java/com/destroystokyo/paper/loottable/PaperContainerEntityLootableInventory.java ++++ b/src/main/java/com/destroystokyo/paper/loottable/PaperContainerEntityLootableInventory.java +@@ -20,7 +20,7 @@ public class PaperContainerEntityLootableInventory implements PaperLootableEntit + + @Override + public org.bukkit.loot.LootTable getLootTable() { +- return entity.getLootTable() != null ? Optionull.map(entity.getLootTable(), rk -> Bukkit.getLootTable(CraftNamespacedKey.fromMinecraft(rk.location()))) : null; ++ return entity.getLootTable() != null && !entity.getLootTable().location().getPath().isEmpty() ? Optionull.map(entity.getLootTable(), rk -> Bukkit.getLootTable(CraftNamespacedKey.fromMinecraft(rk.location()))) : null; + } + + @Override +diff --git a/src/main/java/com/destroystokyo/paper/loottable/PaperTileEntityLootableInventory.java b/src/main/java/com/destroystokyo/paper/loottable/PaperTileEntityLootableInventory.java +index fb72bdea520ccc0928cfbda0569e02a1917a7e86..6f6e19b7b57cb3070ef5b6810d8449344705af8c 100644 +--- a/src/main/java/com/destroystokyo/paper/loottable/PaperTileEntityLootableInventory.java ++++ b/src/main/java/com/destroystokyo/paper/loottable/PaperTileEntityLootableInventory.java +@@ -17,7 +17,7 @@ public class PaperTileEntityLootableInventory implements PaperLootableBlockInven + + @Override + public org.bukkit.loot.LootTable getLootTable() { +- return tileEntityLootable.lootTable != null ? Bukkit.getLootTable(CraftNamespacedKey.fromMinecraft(tileEntityLootable.lootTable.location())) : null; ++ return tileEntityLootable.lootTable != null && !tileEntityLootable.lootTable.location().getPath().isEmpty() ? Bukkit.getLootTable(CraftNamespacedKey.fromMinecraft(tileEntityLootable.lootTable.location())) : null; + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/attribute/CraftAttribute.java b/src/main/java/org/bukkit/craftbukkit/attribute/CraftAttribute.java +index 6fd032dfb50f6051d6a0b5fbd9d76f42c23354d6..c52f64bb608b67ac7e30fec0c3c9bdef080fa716 100644 +--- a/src/main/java/org/bukkit/craftbukkit/attribute/CraftAttribute.java ++++ b/src/main/java/org/bukkit/craftbukkit/attribute/CraftAttribute.java +@@ -29,7 +29,10 @@ public class CraftAttribute { + public static Attribute stringToBukkit(String bukkit) { + Preconditions.checkArgument(bukkit != null); + +- return Registry.ATTRIBUTE.get(NamespacedKey.fromString(bukkit)); ++ // Paper start - Fixup NamespacedKey handling ++ final NamespacedKey key = NamespacedKey.fromString(bukkit); ++ return key != null ? Registry.ATTRIBUTE.get(key) : null; ++ // Paper end - Fixup NamespacedKey handling + } + + public static net.minecraft.world.entity.ai.attributes.Attribute bukkitToMinecraft(Attribute bukkit) { +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaArmor.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaArmor.java +index 12b95c4455e741b65b844eab362f02bce54eb525..ac9279f7acb7077c08d7741435126adfef9e17b8 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaArmor.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaArmor.java +@@ -69,8 +69,9 @@ public class CraftMetaArmor extends CraftMetaItem implements ArmorMeta { + super(tag); + + getOrEmpty(tag, CraftMetaArmor.TRIM).ifPresent((trimCompound) -> { +- TrimMaterial trimMaterial = CraftTrimMaterial.minecraftHolderToBukkit(trimCompound.material()); +- TrimPattern trimPattern = CraftTrimPattern.minecraftHolderToBukkit(trimCompound.pattern()); ++ TrimMaterial trimMaterial = this.unwrapAndConvertHolder(Registry.TRIM_MATERIAL, trimCompound.material()); // Paper - fix upstream not being correct ++ TrimPattern trimPattern = this.unwrapAndConvertHolder(Registry.TRIM_PATTERN, trimCompound.pattern()); // Paper - fix upstream not being correct ++ if (trimMaterial == null || trimPattern == null) return; // Paper - just delete the trim because upstream is not doing this right + + this.trim = new ArmorTrim(trimMaterial, trimPattern); + +@@ -79,6 +80,11 @@ public class CraftMetaArmor extends CraftMetaItem implements ArmorMeta { + } + }); + } ++ // Paper start - fixup upstream being dum ++ private T unwrapAndConvertHolder(final Registry registry, final net.minecraft.core.Holder value) { ++ return value.unwrap().map(key -> registry.get(org.bukkit.craftbukkit.util.CraftNamespacedKey.fromMinecraft(key.location())), v -> null); ++ } ++ // Paper end - fixup upstream being dum + + CraftMetaArmor(Map map) { + super(map); +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaMusicInstrument.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaMusicInstrument.java +index e17093289884b904fc3720600ffa3e772e0935b3..7032f07e3872c65bbebb905e9d50057a113100d4 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaMusicInstrument.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaMusicInstrument.java +@@ -33,9 +33,14 @@ public class CraftMetaMusicInstrument extends CraftMetaItem implements MusicInst + super(tag); + + getOrEmpty(tag, CraftMetaMusicInstrument.GOAT_HORN_INSTRUMENT).ifPresent((instrument) -> { +- this.instrument = CraftMusicInstrument.minecraftHolderToBukkit(instrument); ++ this.instrument = this.unwrapAndConvertHolder(Registry.INSTRUMENT, instrument); // Paper - fix upstream not handling custom instruments + }); + } ++ // Paper start - fixup upstream being dum ++ private T unwrapAndConvertHolder(final Registry registry, final net.minecraft.core.Holder value) { ++ return value.unwrap().map(key -> registry.get(org.bukkit.craftbukkit.util.CraftNamespacedKey.fromMinecraft(key.location())), v -> null); ++ } ++ // Paper end - fixup upstream being dum + + CraftMetaMusicInstrument(Map map) { + super(map); +diff --git a/src/main/java/org/bukkit/craftbukkit/potion/CraftPotionType.java b/src/main/java/org/bukkit/craftbukkit/potion/CraftPotionType.java +index 7dc83b02810490c21b68be962c5ef6eb45e32baf..71cc45c4ef9ab98d509b0f8bf7b033a2b32641db 100644 +--- a/src/main/java/org/bukkit/craftbukkit/potion/CraftPotionType.java ++++ b/src/main/java/org/bukkit/craftbukkit/potion/CraftPotionType.java +@@ -61,7 +61,10 @@ public class CraftPotionType implements PotionType.InternalPotionData { + public static PotionType stringToBukkit(String string) { + Preconditions.checkArgument(string != null); + +- return Registry.POTION.get(NamespacedKey.fromString(string)); ++ // Paper start - Fixup NamespacedKey handling ++ final NamespacedKey key = NamespacedKey.fromString(string); ++ return key != null ? Registry.POTION.get(key) : null; ++ // Paper end - Fixup NamespacedKey handling + } + + private final NamespacedKey key; +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftNamespacedKey.java b/src/main/java/org/bukkit/craftbukkit/util/CraftNamespacedKey.java +index 5f40d240b879e3989897b6e45725a8e5a6a7f194..5014192edb9616ce725fc1592832034789527b6f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftNamespacedKey.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftNamespacedKey.java +@@ -13,7 +13,7 @@ public final class CraftNamespacedKey { + return null; + } + ResourceLocation minecraft = ResourceLocation.tryParse(string); +- return (minecraft == null) ? null : CraftNamespacedKey.fromMinecraft(minecraft); ++ return (minecraft == null || minecraft.getPath().isEmpty()) ? null : CraftNamespacedKey.fromMinecraft(minecraft); // Paper - Bukkit's parser does not match Vanilla for empty paths + } + + public static NamespacedKey fromString(String string) { diff --git a/patches/server/0951-Properly-handle-experience-dropping-on-block-break.patch b/patches/server/0951-Properly-handle-experience-dropping-on-block-break.patch deleted file mode 100644 index 3ee724786e..0000000000 --- a/patches/server/0951-Properly-handle-experience-dropping-on-block-break.patch +++ /dev/null @@ -1,94 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> -Date: Sat, 30 Dec 2023 15:00:06 -0500 -Subject: [PATCH] Properly handle experience dropping on block break - -This causes spawnAfterBreak to spawn xp by default, removing the need to manually add xp wherever this method is used. -For classes that use custom xp amounts, they can drop the resources with disabling - -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index 2f018922fb12dc4c3c9c28c7b0597a2b97d1cb02..51c4877ffb77ece41e90d9c6ac06c04dc941e35c 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -618,7 +618,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - if (drop) { - BlockEntity tileentity = iblockdata.hasBlockEntity() ? this.getBlockEntity(pos) : null; - -- Block.dropResources(iblockdata, this, pos, tileentity, breakingEntity, ItemStack.EMPTY); -+ Block.dropResources(iblockdata, this, pos, tileentity, breakingEntity, ItemStack.EMPTY, false); // Paper - Properly handle xp dropping -+ iblockdata.getBlock().popExperience((ServerLevel) this, pos, xp, breakingEntity); // Paper - Properly handle xp dropping; custom amount - } - - boolean flag1 = this.setBlock(pos, fluid.createLegacyBlock(), 3, maxUpdateDepth); -diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java -index a757a45d423e1729c9374391df4186ae0522e1ac..7f0c0ca49e7575c18935b71e3180d112440289f7 100644 ---- a/src/main/java/net/minecraft/world/level/block/Block.java -+++ b/src/main/java/net/minecraft/world/level/block/Block.java -@@ -313,23 +313,31 @@ public class Block extends BlockBehaviour implements ItemLike { - for (ItemStack drop : Block.getDrops(state, serverLevel, pos, blockEntity)) { - items.add(org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(drop)); - } -+ Block block = state.getBlock(); // Paper - Properly handle xp dropping - io.papermc.paper.event.block.BlockBreakBlockEvent event = new io.papermc.paper.event.block.BlockBreakBlockEvent(org.bukkit.craftbukkit.block.CraftBlock.at(levelAccessor, pos), org.bukkit.craftbukkit.block.CraftBlock.at(levelAccessor, source), items); -+ event.setExpToDrop(block.getExpDrop(state, serverLevel, pos, net.minecraft.world.item.ItemStack.EMPTY, true)); // Paper - Properly handle xp dropping - event.callEvent(); - for (org.bukkit.inventory.ItemStack drop : event.getDrops()) { - popResource(serverLevel, pos, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(drop)); - } -- state.spawnAfterBreak(serverLevel, pos, ItemStack.EMPTY, true); -+ state.spawnAfterBreak(serverLevel, pos, ItemStack.EMPTY, false); // Paper - Properly handle xp dropping -+ block.popExperience(serverLevel, pos, event.getExpToDrop()); // Paper - Properly handle xp dropping - } - return true; - } - // Paper end - Add BlockBreakBlockEvent - - public static void dropResources(BlockState state, Level world, BlockPos pos, @Nullable BlockEntity blockEntity, @Nullable Entity entity, ItemStack tool) { -+ // Paper start - Properly handle xp dropping -+ dropResources(state, world, pos, blockEntity, entity, tool, true); -+ } -+ public static void dropResources(BlockState state, Level world, BlockPos pos, @Nullable BlockEntity blockEntity, @Nullable Entity entity, ItemStack tool, boolean dropExperience) { -+ // Paper end - Properly handle xp dropping - if (world instanceof ServerLevel) { - Block.getDrops(state, (ServerLevel) world, pos, blockEntity, entity, tool).forEach((itemstack1) -> { - Block.popResource(world, pos, itemstack1); - }); -- state.spawnAfterBreak((ServerLevel) world, pos, tool, true); -+ state.spawnAfterBreak((ServerLevel) world, pos, tool, dropExperience); // Paper - Properly handle xp dropping - } - - } -@@ -413,7 +421,7 @@ public class Block extends BlockBehaviour implements ItemLike { - player.awardStat(Stats.BLOCK_MINED.get(this)); - player.causeFoodExhaustion(0.005F, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.BLOCK_MINED); // CraftBukkit - EntityExhaustionEvent - if (includeDrops) { // Paper - fix drops not preventing stats/food exhaustion -- Block.dropResources(state, world, pos, blockEntity, player, tool); -+ Block.dropResources(state, world, pos, blockEntity, player, tool, dropExp); // Paper - Properly handle xp dropping - } // Paper - fix drops not preventing stats/food exhaustion - } - -diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -index c762a006b3f586b32209c20e85f6b8bf169dbd06..f87d9cb38caf3bf92fd32f2118f76799ede418db 100644 ---- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -+++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -@@ -1118,6 +1118,7 @@ public abstract class BlockBehaviour implements FeatureElement { - - public void spawnAfterBreak(ServerLevel world, BlockPos pos, ItemStack tool, boolean dropExperience) { - this.getBlock().spawnAfterBreak(this.asState(), world, pos, tool, dropExperience); -+ if (dropExperience) {getBlock().popExperience(world, pos, this.getBlock().getExpDrop(asState(), world, pos, tool, true));} // Paper - Properly handle xp dropping - } - - public List getDrops(LootParams.Builder builder) { -diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java -index 461a66c323a74db5a70981fafc5fa20f54f0f40d..ac11f18690434922179b61ffcc3036dea025b0cb 100644 ---- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java -+++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java -@@ -509,7 +509,7 @@ public class CraftBlock implements Block { - - // Modelled off EntityHuman#hasBlock - if (block != Blocks.AIR && (item == null || !iblockdata.requiresCorrectToolForDrops() || nmsItem.isCorrectToolForDrops(iblockdata))) { -- net.minecraft.world.level.block.Block.dropResources(iblockdata, this.world.getMinecraftWorld(), this.position, this.world.getBlockEntity(this.position), null, nmsItem); -+ net.minecraft.world.level.block.Block.dropResources(iblockdata, this.world.getMinecraftWorld(), this.position, this.world.getBlockEntity(this.position), null, nmsItem, false); // Paper - Properly handle xp dropping - // Paper start - improve Block#breanNaturally - if (triggerEffect) { - if (iblockdata.getBlock() instanceof net.minecraft.world.level.block.BaseFireBlock) { diff --git a/patches/server/0952-Expose-LootTable-of-DecoratedPot.patch b/patches/server/0952-Expose-LootTable-of-DecoratedPot.patch new file mode 100644 index 0000000000..6b5fd029f3 --- /dev/null +++ b/patches/server/0952-Expose-LootTable-of-DecoratedPot.patch @@ -0,0 +1,50 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: FireInstall +Date: Sat, 20 Jan 2024 16:20:06 +0100 +Subject: [PATCH] Expose LootTable of DecoratedPot + + +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftDecoratedPot.java b/src/main/java/org/bukkit/craftbukkit/block/CraftDecoratedPot.java +index 6e0258d4d6a07e0f471640a9edda0adf7ef6cd9e..6015565aeedc1b2cdddcdb488f8de71bc84b61f2 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftDecoratedPot.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftDecoratedPot.java +@@ -43,6 +43,39 @@ public class CraftDecoratedPot extends CraftBlockEntityState key = (table == null) ? null : net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.LOOT_TABLE, org.bukkit.craftbukkit.util.CraftNamespacedKey.toMinecraft(table.getKey())); ++ this.getSnapshot().setLootTable(key, seed); ++ } ++ ++ @Override ++ public org.bukkit.loot.LootTable getLootTable() { ++ if (this.getSnapshot().getLootTable() == null) { ++ return null; ++ } ++ ++ net.minecraft.resources.ResourceKey key = this.getSnapshot().getLootTable(); ++ return org.bukkit.Bukkit.getLootTable(org.bukkit.craftbukkit.util.CraftNamespacedKey.fromMinecraft(key.location())); ++ } ++ ++ @Override ++ public void setSeed(final long seed) { ++ this.getSnapshot().setLootTableSeed(seed); ++ } ++ ++ @Override ++ public long getSeed() { ++ return this.getSnapshot().getLootTableSeed(); ++ } ++ // Paper end - expose loot table ++ + @Override + public void setSherd(Side face, Material sherd) { + Preconditions.checkArgument(face != null, "face must not be null"); diff --git a/patches/server/0952-Fixup-NamespacedKey-handling.patch b/patches/server/0952-Fixup-NamespacedKey-handling.patch deleted file mode 100644 index 20c52c2fba..0000000000 --- a/patches/server/0952-Fixup-NamespacedKey-handling.patch +++ /dev/null @@ -1,125 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Nassim Jahnke -Date: Sat, 6 Jan 2024 14:31:00 +0100 -Subject: [PATCH] Fixup NamespacedKey handling - - -diff --git a/src/main/java/com/destroystokyo/paper/loottable/PaperContainerEntityLootableInventory.java b/src/main/java/com/destroystokyo/paper/loottable/PaperContainerEntityLootableInventory.java -index 7f5283a8ee74253e326fe994f20ae00cf3c75e1b..183f31c81d9c0a966e6b20c2cff3d279dfc7e622 100644 ---- a/src/main/java/com/destroystokyo/paper/loottable/PaperContainerEntityLootableInventory.java -+++ b/src/main/java/com/destroystokyo/paper/loottable/PaperContainerEntityLootableInventory.java -@@ -20,7 +20,7 @@ public class PaperContainerEntityLootableInventory implements PaperLootableEntit - - @Override - public org.bukkit.loot.LootTable getLootTable() { -- return entity.getLootTable() != null ? Optionull.map(entity.getLootTable(), rk -> Bukkit.getLootTable(CraftNamespacedKey.fromMinecraft(rk.location()))) : null; -+ return entity.getLootTable() != null && !entity.getLootTable().location().getPath().isEmpty() ? Optionull.map(entity.getLootTable(), rk -> Bukkit.getLootTable(CraftNamespacedKey.fromMinecraft(rk.location()))) : null; - } - - @Override -diff --git a/src/main/java/com/destroystokyo/paper/loottable/PaperTileEntityLootableInventory.java b/src/main/java/com/destroystokyo/paper/loottable/PaperTileEntityLootableInventory.java -index fb72bdea520ccc0928cfbda0569e02a1917a7e86..6f6e19b7b57cb3070ef5b6810d8449344705af8c 100644 ---- a/src/main/java/com/destroystokyo/paper/loottable/PaperTileEntityLootableInventory.java -+++ b/src/main/java/com/destroystokyo/paper/loottable/PaperTileEntityLootableInventory.java -@@ -17,7 +17,7 @@ public class PaperTileEntityLootableInventory implements PaperLootableBlockInven - - @Override - public org.bukkit.loot.LootTable getLootTable() { -- return tileEntityLootable.lootTable != null ? Bukkit.getLootTable(CraftNamespacedKey.fromMinecraft(tileEntityLootable.lootTable.location())) : null; -+ return tileEntityLootable.lootTable != null && !tileEntityLootable.lootTable.location().getPath().isEmpty() ? Bukkit.getLootTable(CraftNamespacedKey.fromMinecraft(tileEntityLootable.lootTable.location())) : null; - } - - @Override -diff --git a/src/main/java/org/bukkit/craftbukkit/attribute/CraftAttribute.java b/src/main/java/org/bukkit/craftbukkit/attribute/CraftAttribute.java -index 6fd032dfb50f6051d6a0b5fbd9d76f42c23354d6..c52f64bb608b67ac7e30fec0c3c9bdef080fa716 100644 ---- a/src/main/java/org/bukkit/craftbukkit/attribute/CraftAttribute.java -+++ b/src/main/java/org/bukkit/craftbukkit/attribute/CraftAttribute.java -@@ -29,7 +29,10 @@ public class CraftAttribute { - public static Attribute stringToBukkit(String bukkit) { - Preconditions.checkArgument(bukkit != null); - -- return Registry.ATTRIBUTE.get(NamespacedKey.fromString(bukkit)); -+ // Paper start - Fixup NamespacedKey handling -+ final NamespacedKey key = NamespacedKey.fromString(bukkit); -+ return key != null ? Registry.ATTRIBUTE.get(key) : null; -+ // Paper end - Fixup NamespacedKey handling - } - - public static net.minecraft.world.entity.ai.attributes.Attribute bukkitToMinecraft(Attribute bukkit) { -diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaArmor.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaArmor.java -index 12b95c4455e741b65b844eab362f02bce54eb525..ac9279f7acb7077c08d7741435126adfef9e17b8 100644 ---- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaArmor.java -+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaArmor.java -@@ -69,8 +69,9 @@ public class CraftMetaArmor extends CraftMetaItem implements ArmorMeta { - super(tag); - - getOrEmpty(tag, CraftMetaArmor.TRIM).ifPresent((trimCompound) -> { -- TrimMaterial trimMaterial = CraftTrimMaterial.minecraftHolderToBukkit(trimCompound.material()); -- TrimPattern trimPattern = CraftTrimPattern.minecraftHolderToBukkit(trimCompound.pattern()); -+ TrimMaterial trimMaterial = this.unwrapAndConvertHolder(Registry.TRIM_MATERIAL, trimCompound.material()); // Paper - fix upstream not being correct -+ TrimPattern trimPattern = this.unwrapAndConvertHolder(Registry.TRIM_PATTERN, trimCompound.pattern()); // Paper - fix upstream not being correct -+ if (trimMaterial == null || trimPattern == null) return; // Paper - just delete the trim because upstream is not doing this right - - this.trim = new ArmorTrim(trimMaterial, trimPattern); - -@@ -79,6 +80,11 @@ public class CraftMetaArmor extends CraftMetaItem implements ArmorMeta { - } - }); - } -+ // Paper start - fixup upstream being dum -+ private T unwrapAndConvertHolder(final Registry registry, final net.minecraft.core.Holder value) { -+ return value.unwrap().map(key -> registry.get(org.bukkit.craftbukkit.util.CraftNamespacedKey.fromMinecraft(key.location())), v -> null); -+ } -+ // Paper end - fixup upstream being dum - - CraftMetaArmor(Map map) { - super(map); -diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaMusicInstrument.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaMusicInstrument.java -index e17093289884b904fc3720600ffa3e772e0935b3..7032f07e3872c65bbebb905e9d50057a113100d4 100644 ---- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaMusicInstrument.java -+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaMusicInstrument.java -@@ -33,9 +33,14 @@ public class CraftMetaMusicInstrument extends CraftMetaItem implements MusicInst - super(tag); - - getOrEmpty(tag, CraftMetaMusicInstrument.GOAT_HORN_INSTRUMENT).ifPresent((instrument) -> { -- this.instrument = CraftMusicInstrument.minecraftHolderToBukkit(instrument); -+ this.instrument = this.unwrapAndConvertHolder(Registry.INSTRUMENT, instrument); // Paper - fix upstream not handling custom instruments - }); - } -+ // Paper start - fixup upstream being dum -+ private T unwrapAndConvertHolder(final Registry registry, final net.minecraft.core.Holder value) { -+ return value.unwrap().map(key -> registry.get(org.bukkit.craftbukkit.util.CraftNamespacedKey.fromMinecraft(key.location())), v -> null); -+ } -+ // Paper end - fixup upstream being dum - - CraftMetaMusicInstrument(Map map) { - super(map); -diff --git a/src/main/java/org/bukkit/craftbukkit/potion/CraftPotionType.java b/src/main/java/org/bukkit/craftbukkit/potion/CraftPotionType.java -index 7dc83b02810490c21b68be962c5ef6eb45e32baf..71cc45c4ef9ab98d509b0f8bf7b033a2b32641db 100644 ---- a/src/main/java/org/bukkit/craftbukkit/potion/CraftPotionType.java -+++ b/src/main/java/org/bukkit/craftbukkit/potion/CraftPotionType.java -@@ -61,7 +61,10 @@ public class CraftPotionType implements PotionType.InternalPotionData { - public static PotionType stringToBukkit(String string) { - Preconditions.checkArgument(string != null); - -- return Registry.POTION.get(NamespacedKey.fromString(string)); -+ // Paper start - Fixup NamespacedKey handling -+ final NamespacedKey key = NamespacedKey.fromString(string); -+ return key != null ? Registry.POTION.get(key) : null; -+ // Paper end - Fixup NamespacedKey handling - } - - private final NamespacedKey key; -diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftNamespacedKey.java b/src/main/java/org/bukkit/craftbukkit/util/CraftNamespacedKey.java -index 5f40d240b879e3989897b6e45725a8e5a6a7f194..5014192edb9616ce725fc1592832034789527b6f 100644 ---- a/src/main/java/org/bukkit/craftbukkit/util/CraftNamespacedKey.java -+++ b/src/main/java/org/bukkit/craftbukkit/util/CraftNamespacedKey.java -@@ -13,7 +13,7 @@ public final class CraftNamespacedKey { - return null; - } - ResourceLocation minecraft = ResourceLocation.tryParse(string); -- return (minecraft == null) ? null : CraftNamespacedKey.fromMinecraft(minecraft); -+ return (minecraft == null || minecraft.getPath().isEmpty()) ? null : CraftNamespacedKey.fromMinecraft(minecraft); // Paper - Bukkit's parser does not match Vanilla for empty paths - } - - public static NamespacedKey fromString(String string) { diff --git a/patches/server/0953-Expose-LootTable-of-DecoratedPot.patch b/patches/server/0953-Expose-LootTable-of-DecoratedPot.patch deleted file mode 100644 index 6b5fd029f3..0000000000 --- a/patches/server/0953-Expose-LootTable-of-DecoratedPot.patch +++ /dev/null @@ -1,50 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: FireInstall -Date: Sat, 20 Jan 2024 16:20:06 +0100 -Subject: [PATCH] Expose LootTable of DecoratedPot - - -diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftDecoratedPot.java b/src/main/java/org/bukkit/craftbukkit/block/CraftDecoratedPot.java -index 6e0258d4d6a07e0f471640a9edda0adf7ef6cd9e..6015565aeedc1b2cdddcdb488f8de71bc84b61f2 100644 ---- a/src/main/java/org/bukkit/craftbukkit/block/CraftDecoratedPot.java -+++ b/src/main/java/org/bukkit/craftbukkit/block/CraftDecoratedPot.java -@@ -43,6 +43,39 @@ public class CraftDecoratedPot extends CraftBlockEntityState key = (table == null) ? null : net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.LOOT_TABLE, org.bukkit.craftbukkit.util.CraftNamespacedKey.toMinecraft(table.getKey())); -+ this.getSnapshot().setLootTable(key, seed); -+ } -+ -+ @Override -+ public org.bukkit.loot.LootTable getLootTable() { -+ if (this.getSnapshot().getLootTable() == null) { -+ return null; -+ } -+ -+ net.minecraft.resources.ResourceKey key = this.getSnapshot().getLootTable(); -+ return org.bukkit.Bukkit.getLootTable(org.bukkit.craftbukkit.util.CraftNamespacedKey.fromMinecraft(key.location())); -+ } -+ -+ @Override -+ public void setSeed(final long seed) { -+ this.getSnapshot().setLootTableSeed(seed); -+ } -+ -+ @Override -+ public long getSeed() { -+ return this.getSnapshot().getLootTableSeed(); -+ } -+ // Paper end - expose loot table -+ - @Override - public void setSherd(Side face, Material sherd) { - Preconditions.checkArgument(face != null, "face must not be null"); diff --git a/patches/server/0953-Reduce-allocation-of-Vec3D-by-entity-tracker.patch b/patches/server/0953-Reduce-allocation-of-Vec3D-by-entity-tracker.patch new file mode 100644 index 0000000000..dc2cbd7a08 --- /dev/null +++ b/patches/server/0953-Reduce-allocation-of-Vec3D-by-entity-tracker.patch @@ -0,0 +1,59 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Mon, 27 Apr 2020 00:04:16 -0700 +Subject: [PATCH] Reduce allocation of Vec3D by entity tracker + + +diff --git a/src/main/java/net/minecraft/network/protocol/game/VecDeltaCodec.java b/src/main/java/net/minecraft/network/protocol/game/VecDeltaCodec.java +index a3d247c93ac1a2d872ff0e3841efc3d7b84fcbc1..23fe7c4c1844d321efed6b7b886c422761ad6594 100644 +--- a/src/main/java/net/minecraft/network/protocol/game/VecDeltaCodec.java ++++ b/src/main/java/net/minecraft/network/protocol/game/VecDeltaCodec.java +@@ -5,7 +5,7 @@ import org.jetbrains.annotations.VisibleForTesting; + + public class VecDeltaCodec { + private static final double TRUNCATION_STEPS = 4096.0; +- private Vec3 base = Vec3.ZERO; ++ public Vec3 base = Vec3.ZERO; // Paper + + @VisibleForTesting + static long encode(double value) { +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index 027f2734b0ff226437f4c94430215929b6ea2219..35f627c58e93c03ee58b44877398432bba57dc2d 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -1748,10 +1748,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + public void updatePlayer(ServerPlayer player) { + org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot + if (player != this.entity) { +- Vec3 vec3d = player.position().subtract(this.entity.position()); ++ // Paper start - remove allocation of Vec3D here ++ // Vec3 vec3d = player.position().subtract(this.entity.position()); ++ double vec3d_dx = player.getX() - this.entity.getX(); ++ double vec3d_dz = player.getZ() - this.entity.getZ(); ++ // Paper end - remove allocation of Vec3D here + int i = ChunkMap.this.getPlayerViewDistance(player); + double d0 = (double) Math.min(this.getEffectiveRange(), i * 16); +- double d1 = vec3d.x * vec3d.x + vec3d.z * vec3d.z; ++ double d1 = vec3d_dx * vec3d_dx + vec3d_dz * vec3d_dz; // Paper + double d2 = d0 * d0; + boolean flag = d1 <= d2 && this.entity.broadcastToPlayer(player) && ChunkMap.this.isChunkTracked(player, this.entity.chunkPosition().x, this.entity.chunkPosition().z); + // Paper start - Configurable entity tracking range by Y +diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java +index 0373b2de2b992b648f346d6df3960a3586a54567..22eec853588ded2d255ab69d408f8e987832abe2 100644 +--- a/src/main/java/net/minecraft/server/level/ServerEntity.java ++++ b/src/main/java/net/minecraft/server/level/ServerEntity.java +@@ -164,7 +164,13 @@ public class ServerEntity { + i = Mth.floor(this.entity.getYRot() * 256.0F / 360.0F); + j = Mth.floor(this.entity.getXRot() * 256.0F / 360.0F); + Vec3 vec3d = this.entity.trackingPosition(); +- boolean flag1 = this.positionCodec.delta(vec3d).lengthSqr() >= 7.62939453125E-6D; ++ // Paper start - reduce allocation of Vec3D here ++ Vec3 base = this.positionCodec.base; ++ double vec3d_dx = vec3d.x - base.x; ++ double vec3d_dy = vec3d.y - base.y; ++ double vec3d_dz = vec3d.z - base.z; ++ boolean flag1 = (vec3d_dx * vec3d_dx + vec3d_dy * vec3d_dy + vec3d_dz * vec3d_dz) >= 7.62939453125E-6D; ++ // Paper end - reduce allocation of Vec3D here + Packet packet1 = null; + boolean flag2 = flag1 || this.tickCount % 60 == 0; + boolean flag3 = Math.abs(i - this.yRotp) >= 1 || Math.abs(j - this.xRotp) >= 1; diff --git a/patches/server/0954-Add-PlayerTradeEvent-and-PlayerPurchaseEvent.patch b/patches/server/0954-Add-PlayerTradeEvent-and-PlayerPurchaseEvent.patch new file mode 100644 index 0000000000..7d75e1147a --- /dev/null +++ b/patches/server/0954-Add-PlayerTradeEvent-and-PlayerPurchaseEvent.patch @@ -0,0 +1,240 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Thu, 2 Jul 2020 16:12:10 -0700 +Subject: [PATCH] Add PlayerTradeEvent and PlayerPurchaseEvent + +Co-authored-by: Alexander + +diff --git a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java +index 6e29b5729b8184d13065c9c4e18f6e450c9d88a6..d323cf157f2a910916baa9ce3f7e5bc81648c47d 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java ++++ b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java +@@ -139,11 +139,24 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa + @Override + public void overrideXp(int experience) {} + ++ // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent ++ @Override ++ public void processTrade(MerchantOffer recipe, @Nullable io.papermc.paper.event.player.PlayerPurchaseEvent event) { // The MerchantRecipe passed in here is the one set by the PlayerPurchaseEvent ++ if (event == null || event.willIncreaseTradeUses()) { ++ recipe.increaseUses(); ++ } ++ if (event == null || event.isRewardingExp()) { ++ this.rewardTradeXp(recipe); ++ } ++ this.notifyTrade(recipe); ++ } ++ // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent ++ + @Override + public void notifyTrade(MerchantOffer offer) { +- offer.increaseUses(); ++ // offer.increaseUses(); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent + this.ambientSoundTime = -this.getAmbientSoundInterval(); +- this.rewardTradeXp(offer); ++ // this.rewardTradeXp(offer); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent + if (this.tradingPlayer instanceof ServerPlayer) { + CriteriaTriggers.TRADE.trigger((ServerPlayer) this.tradingPlayer, this, offer.getResult()); + } +diff --git a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java +index 4c7e91977fa590abfe7eb3704d8008ed6d4e3ab3..32910f677b0522ac8ec513fa0d00b714b52cfae4 100644 +--- a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java +@@ -768,6 +768,14 @@ public abstract class AbstractContainerMenu { + public abstract boolean stillValid(Player player); + + protected boolean moveItemStackTo(ItemStack stack, int startIndex, int endIndex, boolean fromLast) { ++ // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent ++ return this.moveItemStackTo(stack, startIndex, endIndex, fromLast, false); ++ } ++ protected boolean moveItemStackTo(ItemStack stack, int startIndex, int endIndex, boolean fromLast, boolean isCheck) { ++ if (isCheck) { ++ stack = stack.copy(); ++ } ++ // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent + boolean flag1 = false; + int k = startIndex; + +@@ -791,6 +799,11 @@ public abstract class AbstractContainerMenu { + + slot = (Slot) this.slots.get(k); + itemstack1 = slot.getItem(); ++ // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent; clone if only a check ++ if (isCheck) { ++ itemstack1 = itemstack1.copy(); ++ } ++ // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent + if (!itemstack1.isEmpty() && ItemStack.isSameItemSameComponents(stack, itemstack1)) { + l = itemstack1.getCount() + stack.getCount(); + int i1 = slot.getMaxStackSize(itemstack1); +@@ -798,12 +811,16 @@ public abstract class AbstractContainerMenu { + if (l <= i1) { + stack.setCount(0); + itemstack1.setCount(l); ++ if (!isCheck) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent + slot.setChanged(); ++ } // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent + flag1 = true; + } else if (itemstack1.getCount() < i1) { + stack.shrink(i1 - itemstack1.getCount()); + itemstack1.setCount(i1); ++ if (!isCheck) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent + slot.setChanged(); ++ } // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent + flag1 = true; + } + } +@@ -834,10 +851,21 @@ public abstract class AbstractContainerMenu { + + slot = (Slot) this.slots.get(k); + itemstack1 = slot.getItem(); ++ // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent ++ if (isCheck) { ++ itemstack1 = itemstack1.copy(); ++ } ++ // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent + if (itemstack1.isEmpty() && slot.mayPlace(stack)) { + l = slot.getMaxStackSize(stack); ++ // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent ++ if (isCheck) { ++ stack.shrink(Math.min(stack.getCount(), l)); ++ } else { ++ // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent + slot.setByPlayer(stack.split(Math.min(stack.getCount(), l))); + slot.setChanged(); ++ } // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent + flag1 = true; + break; + } +diff --git a/src/main/java/net/minecraft/world/inventory/MerchantMenu.java b/src/main/java/net/minecraft/world/inventory/MerchantMenu.java +index ecefd4075c097e2118ec23e87baf36465c40f85f..2992e86f5f83431e230162380b33721df785ba91 100644 +--- a/src/main/java/net/minecraft/world/inventory/MerchantMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/MerchantMenu.java +@@ -135,12 +135,12 @@ public class MerchantMenu extends AbstractContainerMenu { + + itemstack = itemstack1.copy(); + if (slot == 2) { +- if (!this.moveItemStackTo(itemstack1, 3, 39, true)) { ++ if (!this.moveItemStackTo(itemstack1, 3, 39, true, true)) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent + return ItemStack.EMPTY; + } + +- slot1.onQuickCraft(itemstack1, itemstack); +- this.playTradeSound(); ++ // slot1.onQuickCraft(itemstack1, itemstack); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent; moved to after the non-check moveItemStackTo call ++ // this.playTradeSound(); + } else if (slot != 0 && slot != 1) { + if (slot >= 3 && slot < 30) { + if (!this.moveItemStackTo(itemstack1, 30, 39, false)) { +@@ -153,6 +153,7 @@ public class MerchantMenu extends AbstractContainerMenu { + return ItemStack.EMPTY; + } + ++ if (slot != 2) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent; moved down for slot 2 + if (itemstack1.isEmpty()) { + slot1.setByPlayer(ItemStack.EMPTY); + } else { +@@ -164,6 +165,21 @@ public class MerchantMenu extends AbstractContainerMenu { + } + + slot1.onTake(player, itemstack1); ++ } // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent; handle slot 2 ++ if (slot == 2) { // is merchant result slot ++ slot1.onTake(player, itemstack1); ++ if (itemstack1.isEmpty()) { ++ slot1.set(ItemStack.EMPTY); ++ return ItemStack.EMPTY; ++ } ++ ++ this.moveItemStackTo(itemstack1, 3, 39, true, false); // This should always succeed because it's checked above ++ ++ slot1.onQuickCraft(itemstack1, itemstack); ++ this.playTradeSound(); ++ slot1.set(ItemStack.EMPTY); // itemstack1 should ALWAYS be empty ++ } ++ // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent + } + + return itemstack; +diff --git a/src/main/java/net/minecraft/world/inventory/MerchantResultSlot.java b/src/main/java/net/minecraft/world/inventory/MerchantResultSlot.java +index 26227033613a641a9d5a6f29356b19e54753b3f1..c2376538215604210d08acd33e8e5fdc8a35c7d3 100644 +--- a/src/main/java/net/minecraft/world/inventory/MerchantResultSlot.java ++++ b/src/main/java/net/minecraft/world/inventory/MerchantResultSlot.java +@@ -47,13 +47,32 @@ public class MerchantResultSlot extends Slot { + + @Override + public void onTake(Player player, ItemStack stack) { +- this.checkTakeAchievements(stack); ++ // this.checkTakeAchievements(stack); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent; move to after event is called and not cancelled + MerchantOffer merchantOffer = this.slots.getActiveOffer(); ++ // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent ++ io.papermc.paper.event.player.PlayerPurchaseEvent event = null; ++ if (merchantOffer != null && player instanceof net.minecraft.server.level.ServerPlayer serverPlayer) { ++ if (this.merchant instanceof net.minecraft.world.entity.npc.AbstractVillager abstractVillager) { ++ event = new io.papermc.paper.event.player.PlayerTradeEvent(serverPlayer.getBukkitEntity(), (org.bukkit.entity.AbstractVillager) abstractVillager.getBukkitEntity(), merchantOffer.asBukkit(), true, true); ++ } else if (this.merchant instanceof org.bukkit.craftbukkit.inventory.CraftMerchantCustom.MinecraftMerchant) { ++ event = new io.papermc.paper.event.player.PlayerPurchaseEvent(serverPlayer.getBukkitEntity(), merchantOffer.asBukkit(), false, true); ++ } ++ if (event != null) { ++ if (!event.callEvent()) { ++ stack.setCount(0); ++ event.getPlayer().updateInventory(); ++ return; ++ } ++ merchantOffer = org.bukkit.craftbukkit.inventory.CraftMerchantRecipe.fromBukkit(event.getTrade()).toMinecraft(); ++ } ++ } ++ this.checkTakeAchievements(stack); ++ // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent + if (merchantOffer != null) { + ItemStack itemStack = this.slots.getItem(0); + ItemStack itemStack2 = this.slots.getItem(1); + if (merchantOffer.take(itemStack, itemStack2) || merchantOffer.take(itemStack2, itemStack)) { +- this.merchant.notifyTrade(merchantOffer); ++ this.merchant.processTrade(merchantOffer, event); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent + player.awardStat(Stats.TRADED_WITH_VILLAGER); + this.slots.setItem(0, itemStack); + this.slots.setItem(1, itemStack2); +diff --git a/src/main/java/net/minecraft/world/item/trading/Merchant.java b/src/main/java/net/minecraft/world/item/trading/Merchant.java +index 5a350948a4735902f5c612592bc9d100445a0c8a..716b30dcd7e63c66736c448dd136c9f74dc7fe43 100644 +--- a/src/main/java/net/minecraft/world/item/trading/Merchant.java ++++ b/src/main/java/net/minecraft/world/item/trading/Merchant.java +@@ -20,6 +20,7 @@ public interface Merchant { + + void overrideOffers(MerchantOffers offers); + ++ default void processTrade(MerchantOffer merchantRecipe, @Nullable io.papermc.paper.event.player.PlayerPurchaseEvent event) { this.notifyTrade(merchantRecipe); } // Paper + void notifyTrade(MerchantOffer offer); + + void notifyTradeUpdated(ItemStack stack); +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMerchantCustom.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMerchantCustom.java +index adf22ce4f0bcd3bd57dc2030c6c92d3df96566e3..e33ddcd967a427abfda9e6692338da4996a81c6c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMerchantCustom.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMerchantCustom.java +@@ -74,10 +74,25 @@ public class CraftMerchantCustom extends CraftMerchant { + return this.trades; + } + ++ // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent ++ @Override ++ public void processTrade(MerchantOffer merchantRecipe, @javax.annotation.Nullable io.papermc.paper.event.player.PlayerPurchaseEvent event) { // The MerchantRecipe passed in here is the one set by the PlayerPurchaseEvent ++ /** Based on {@link net.minecraft.world.entity.npc.AbstractVillager#processTrade(MerchantOffer, io.papermc.paper.event.player.PlayerPurchaseEvent)} */ ++ if (getTradingPlayer() instanceof net.minecraft.server.level.ServerPlayer) { ++ if (event == null || event.willIncreaseTradeUses()) { ++ merchantRecipe.increaseUses(); ++ } ++ if (event == null || event.isRewardingExp()) { ++ this.tradingPlayer.level().addFreshEntity(new net.minecraft.world.entity.ExperienceOrb(this.tradingPlayer.level(), this.tradingPlayer.getX(), this.tradingPlayer.getY(), this.tradingPlayer.getZ(), merchantRecipe.getXp(), org.bukkit.entity.ExperienceOrb.SpawnReason.VILLAGER_TRADE, this.tradingPlayer, null)); ++ } ++ } ++ this.notifyTrade(merchantRecipe); ++ } ++ // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent + @Override + public void notifyTrade(MerchantOffer offer) { + // increase recipe's uses +- offer.increaseUses(); ++ // offer.increaseUses(); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent; handled above in processTrade + } + + @Override diff --git a/patches/server/0954-Reduce-allocation-of-Vec3D-by-entity-tracker.patch b/patches/server/0954-Reduce-allocation-of-Vec3D-by-entity-tracker.patch deleted file mode 100644 index dc2cbd7a08..0000000000 --- a/patches/server/0954-Reduce-allocation-of-Vec3D-by-entity-tracker.patch +++ /dev/null @@ -1,59 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Mon, 27 Apr 2020 00:04:16 -0700 -Subject: [PATCH] Reduce allocation of Vec3D by entity tracker - - -diff --git a/src/main/java/net/minecraft/network/protocol/game/VecDeltaCodec.java b/src/main/java/net/minecraft/network/protocol/game/VecDeltaCodec.java -index a3d247c93ac1a2d872ff0e3841efc3d7b84fcbc1..23fe7c4c1844d321efed6b7b886c422761ad6594 100644 ---- a/src/main/java/net/minecraft/network/protocol/game/VecDeltaCodec.java -+++ b/src/main/java/net/minecraft/network/protocol/game/VecDeltaCodec.java -@@ -5,7 +5,7 @@ import org.jetbrains.annotations.VisibleForTesting; - - public class VecDeltaCodec { - private static final double TRUNCATION_STEPS = 4096.0; -- private Vec3 base = Vec3.ZERO; -+ public Vec3 base = Vec3.ZERO; // Paper - - @VisibleForTesting - static long encode(double value) { -diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java -index 027f2734b0ff226437f4c94430215929b6ea2219..35f627c58e93c03ee58b44877398432bba57dc2d 100644 ---- a/src/main/java/net/minecraft/server/level/ChunkMap.java -+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java -@@ -1748,10 +1748,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - public void updatePlayer(ServerPlayer player) { - org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot - if (player != this.entity) { -- Vec3 vec3d = player.position().subtract(this.entity.position()); -+ // Paper start - remove allocation of Vec3D here -+ // Vec3 vec3d = player.position().subtract(this.entity.position()); -+ double vec3d_dx = player.getX() - this.entity.getX(); -+ double vec3d_dz = player.getZ() - this.entity.getZ(); -+ // Paper end - remove allocation of Vec3D here - int i = ChunkMap.this.getPlayerViewDistance(player); - double d0 = (double) Math.min(this.getEffectiveRange(), i * 16); -- double d1 = vec3d.x * vec3d.x + vec3d.z * vec3d.z; -+ double d1 = vec3d_dx * vec3d_dx + vec3d_dz * vec3d_dz; // Paper - double d2 = d0 * d0; - boolean flag = d1 <= d2 && this.entity.broadcastToPlayer(player) && ChunkMap.this.isChunkTracked(player, this.entity.chunkPosition().x, this.entity.chunkPosition().z); - // Paper start - Configurable entity tracking range by Y -diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java -index 0373b2de2b992b648f346d6df3960a3586a54567..22eec853588ded2d255ab69d408f8e987832abe2 100644 ---- a/src/main/java/net/minecraft/server/level/ServerEntity.java -+++ b/src/main/java/net/minecraft/server/level/ServerEntity.java -@@ -164,7 +164,13 @@ public class ServerEntity { - i = Mth.floor(this.entity.getYRot() * 256.0F / 360.0F); - j = Mth.floor(this.entity.getXRot() * 256.0F / 360.0F); - Vec3 vec3d = this.entity.trackingPosition(); -- boolean flag1 = this.positionCodec.delta(vec3d).lengthSqr() >= 7.62939453125E-6D; -+ // Paper start - reduce allocation of Vec3D here -+ Vec3 base = this.positionCodec.base; -+ double vec3d_dx = vec3d.x - base.x; -+ double vec3d_dy = vec3d.y - base.y; -+ double vec3d_dz = vec3d.z - base.z; -+ boolean flag1 = (vec3d_dx * vec3d_dx + vec3d_dy * vec3d_dy + vec3d_dz * vec3d_dz) >= 7.62939453125E-6D; -+ // Paper end - reduce allocation of Vec3D here - Packet packet1 = null; - boolean flag2 = flag1 || this.tickCount % 60 == 0; - boolean flag3 = Math.abs(i - this.yRotp) >= 1 || Math.abs(j - this.xRotp) >= 1; diff --git a/patches/server/0955-Add-PlayerTradeEvent-and-PlayerPurchaseEvent.patch b/patches/server/0955-Add-PlayerTradeEvent-and-PlayerPurchaseEvent.patch deleted file mode 100644 index 7d75e1147a..0000000000 --- a/patches/server/0955-Add-PlayerTradeEvent-and-PlayerPurchaseEvent.patch +++ /dev/null @@ -1,240 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Thu, 2 Jul 2020 16:12:10 -0700 -Subject: [PATCH] Add PlayerTradeEvent and PlayerPurchaseEvent - -Co-authored-by: Alexander - -diff --git a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java -index 6e29b5729b8184d13065c9c4e18f6e450c9d88a6..d323cf157f2a910916baa9ce3f7e5bc81648c47d 100644 ---- a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java -+++ b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java -@@ -139,11 +139,24 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa - @Override - public void overrideXp(int experience) {} - -+ // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent -+ @Override -+ public void processTrade(MerchantOffer recipe, @Nullable io.papermc.paper.event.player.PlayerPurchaseEvent event) { // The MerchantRecipe passed in here is the one set by the PlayerPurchaseEvent -+ if (event == null || event.willIncreaseTradeUses()) { -+ recipe.increaseUses(); -+ } -+ if (event == null || event.isRewardingExp()) { -+ this.rewardTradeXp(recipe); -+ } -+ this.notifyTrade(recipe); -+ } -+ // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent -+ - @Override - public void notifyTrade(MerchantOffer offer) { -- offer.increaseUses(); -+ // offer.increaseUses(); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent - this.ambientSoundTime = -this.getAmbientSoundInterval(); -- this.rewardTradeXp(offer); -+ // this.rewardTradeXp(offer); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent - if (this.tradingPlayer instanceof ServerPlayer) { - CriteriaTriggers.TRADE.trigger((ServerPlayer) this.tradingPlayer, this, offer.getResult()); - } -diff --git a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java -index 4c7e91977fa590abfe7eb3704d8008ed6d4e3ab3..32910f677b0522ac8ec513fa0d00b714b52cfae4 100644 ---- a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java -+++ b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java -@@ -768,6 +768,14 @@ public abstract class AbstractContainerMenu { - public abstract boolean stillValid(Player player); - - protected boolean moveItemStackTo(ItemStack stack, int startIndex, int endIndex, boolean fromLast) { -+ // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent -+ return this.moveItemStackTo(stack, startIndex, endIndex, fromLast, false); -+ } -+ protected boolean moveItemStackTo(ItemStack stack, int startIndex, int endIndex, boolean fromLast, boolean isCheck) { -+ if (isCheck) { -+ stack = stack.copy(); -+ } -+ // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent - boolean flag1 = false; - int k = startIndex; - -@@ -791,6 +799,11 @@ public abstract class AbstractContainerMenu { - - slot = (Slot) this.slots.get(k); - itemstack1 = slot.getItem(); -+ // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent; clone if only a check -+ if (isCheck) { -+ itemstack1 = itemstack1.copy(); -+ } -+ // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent - if (!itemstack1.isEmpty() && ItemStack.isSameItemSameComponents(stack, itemstack1)) { - l = itemstack1.getCount() + stack.getCount(); - int i1 = slot.getMaxStackSize(itemstack1); -@@ -798,12 +811,16 @@ public abstract class AbstractContainerMenu { - if (l <= i1) { - stack.setCount(0); - itemstack1.setCount(l); -+ if (!isCheck) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent - slot.setChanged(); -+ } // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent - flag1 = true; - } else if (itemstack1.getCount() < i1) { - stack.shrink(i1 - itemstack1.getCount()); - itemstack1.setCount(i1); -+ if (!isCheck) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent - slot.setChanged(); -+ } // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent - flag1 = true; - } - } -@@ -834,10 +851,21 @@ public abstract class AbstractContainerMenu { - - slot = (Slot) this.slots.get(k); - itemstack1 = slot.getItem(); -+ // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent -+ if (isCheck) { -+ itemstack1 = itemstack1.copy(); -+ } -+ // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent - if (itemstack1.isEmpty() && slot.mayPlace(stack)) { - l = slot.getMaxStackSize(stack); -+ // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent -+ if (isCheck) { -+ stack.shrink(Math.min(stack.getCount(), l)); -+ } else { -+ // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent - slot.setByPlayer(stack.split(Math.min(stack.getCount(), l))); - slot.setChanged(); -+ } // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent - flag1 = true; - break; - } -diff --git a/src/main/java/net/minecraft/world/inventory/MerchantMenu.java b/src/main/java/net/minecraft/world/inventory/MerchantMenu.java -index ecefd4075c097e2118ec23e87baf36465c40f85f..2992e86f5f83431e230162380b33721df785ba91 100644 ---- a/src/main/java/net/minecraft/world/inventory/MerchantMenu.java -+++ b/src/main/java/net/minecraft/world/inventory/MerchantMenu.java -@@ -135,12 +135,12 @@ public class MerchantMenu extends AbstractContainerMenu { - - itemstack = itemstack1.copy(); - if (slot == 2) { -- if (!this.moveItemStackTo(itemstack1, 3, 39, true)) { -+ if (!this.moveItemStackTo(itemstack1, 3, 39, true, true)) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent - return ItemStack.EMPTY; - } - -- slot1.onQuickCraft(itemstack1, itemstack); -- this.playTradeSound(); -+ // slot1.onQuickCraft(itemstack1, itemstack); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent; moved to after the non-check moveItemStackTo call -+ // this.playTradeSound(); - } else if (slot != 0 && slot != 1) { - if (slot >= 3 && slot < 30) { - if (!this.moveItemStackTo(itemstack1, 30, 39, false)) { -@@ -153,6 +153,7 @@ public class MerchantMenu extends AbstractContainerMenu { - return ItemStack.EMPTY; - } - -+ if (slot != 2) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent; moved down for slot 2 - if (itemstack1.isEmpty()) { - slot1.setByPlayer(ItemStack.EMPTY); - } else { -@@ -164,6 +165,21 @@ public class MerchantMenu extends AbstractContainerMenu { - } - - slot1.onTake(player, itemstack1); -+ } // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent; handle slot 2 -+ if (slot == 2) { // is merchant result slot -+ slot1.onTake(player, itemstack1); -+ if (itemstack1.isEmpty()) { -+ slot1.set(ItemStack.EMPTY); -+ return ItemStack.EMPTY; -+ } -+ -+ this.moveItemStackTo(itemstack1, 3, 39, true, false); // This should always succeed because it's checked above -+ -+ slot1.onQuickCraft(itemstack1, itemstack); -+ this.playTradeSound(); -+ slot1.set(ItemStack.EMPTY); // itemstack1 should ALWAYS be empty -+ } -+ // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent - } - - return itemstack; -diff --git a/src/main/java/net/minecraft/world/inventory/MerchantResultSlot.java b/src/main/java/net/minecraft/world/inventory/MerchantResultSlot.java -index 26227033613a641a9d5a6f29356b19e54753b3f1..c2376538215604210d08acd33e8e5fdc8a35c7d3 100644 ---- a/src/main/java/net/minecraft/world/inventory/MerchantResultSlot.java -+++ b/src/main/java/net/minecraft/world/inventory/MerchantResultSlot.java -@@ -47,13 +47,32 @@ public class MerchantResultSlot extends Slot { - - @Override - public void onTake(Player player, ItemStack stack) { -- this.checkTakeAchievements(stack); -+ // this.checkTakeAchievements(stack); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent; move to after event is called and not cancelled - MerchantOffer merchantOffer = this.slots.getActiveOffer(); -+ // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent -+ io.papermc.paper.event.player.PlayerPurchaseEvent event = null; -+ if (merchantOffer != null && player instanceof net.minecraft.server.level.ServerPlayer serverPlayer) { -+ if (this.merchant instanceof net.minecraft.world.entity.npc.AbstractVillager abstractVillager) { -+ event = new io.papermc.paper.event.player.PlayerTradeEvent(serverPlayer.getBukkitEntity(), (org.bukkit.entity.AbstractVillager) abstractVillager.getBukkitEntity(), merchantOffer.asBukkit(), true, true); -+ } else if (this.merchant instanceof org.bukkit.craftbukkit.inventory.CraftMerchantCustom.MinecraftMerchant) { -+ event = new io.papermc.paper.event.player.PlayerPurchaseEvent(serverPlayer.getBukkitEntity(), merchantOffer.asBukkit(), false, true); -+ } -+ if (event != null) { -+ if (!event.callEvent()) { -+ stack.setCount(0); -+ event.getPlayer().updateInventory(); -+ return; -+ } -+ merchantOffer = org.bukkit.craftbukkit.inventory.CraftMerchantRecipe.fromBukkit(event.getTrade()).toMinecraft(); -+ } -+ } -+ this.checkTakeAchievements(stack); -+ // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent - if (merchantOffer != null) { - ItemStack itemStack = this.slots.getItem(0); - ItemStack itemStack2 = this.slots.getItem(1); - if (merchantOffer.take(itemStack, itemStack2) || merchantOffer.take(itemStack2, itemStack)) { -- this.merchant.notifyTrade(merchantOffer); -+ this.merchant.processTrade(merchantOffer, event); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent - player.awardStat(Stats.TRADED_WITH_VILLAGER); - this.slots.setItem(0, itemStack); - this.slots.setItem(1, itemStack2); -diff --git a/src/main/java/net/minecraft/world/item/trading/Merchant.java b/src/main/java/net/minecraft/world/item/trading/Merchant.java -index 5a350948a4735902f5c612592bc9d100445a0c8a..716b30dcd7e63c66736c448dd136c9f74dc7fe43 100644 ---- a/src/main/java/net/minecraft/world/item/trading/Merchant.java -+++ b/src/main/java/net/minecraft/world/item/trading/Merchant.java -@@ -20,6 +20,7 @@ public interface Merchant { - - void overrideOffers(MerchantOffers offers); - -+ default void processTrade(MerchantOffer merchantRecipe, @Nullable io.papermc.paper.event.player.PlayerPurchaseEvent event) { this.notifyTrade(merchantRecipe); } // Paper - void notifyTrade(MerchantOffer offer); - - void notifyTradeUpdated(ItemStack stack); -diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMerchantCustom.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMerchantCustom.java -index adf22ce4f0bcd3bd57dc2030c6c92d3df96566e3..e33ddcd967a427abfda9e6692338da4996a81c6c 100644 ---- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMerchantCustom.java -+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMerchantCustom.java -@@ -74,10 +74,25 @@ public class CraftMerchantCustom extends CraftMerchant { - return this.trades; - } - -+ // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent -+ @Override -+ public void processTrade(MerchantOffer merchantRecipe, @javax.annotation.Nullable io.papermc.paper.event.player.PlayerPurchaseEvent event) { // The MerchantRecipe passed in here is the one set by the PlayerPurchaseEvent -+ /** Based on {@link net.minecraft.world.entity.npc.AbstractVillager#processTrade(MerchantOffer, io.papermc.paper.event.player.PlayerPurchaseEvent)} */ -+ if (getTradingPlayer() instanceof net.minecraft.server.level.ServerPlayer) { -+ if (event == null || event.willIncreaseTradeUses()) { -+ merchantRecipe.increaseUses(); -+ } -+ if (event == null || event.isRewardingExp()) { -+ this.tradingPlayer.level().addFreshEntity(new net.minecraft.world.entity.ExperienceOrb(this.tradingPlayer.level(), this.tradingPlayer.getX(), this.tradingPlayer.getY(), this.tradingPlayer.getZ(), merchantRecipe.getXp(), org.bukkit.entity.ExperienceOrb.SpawnReason.VILLAGER_TRADE, this.tradingPlayer, null)); -+ } -+ } -+ this.notifyTrade(merchantRecipe); -+ } -+ // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent - @Override - public void notifyTrade(MerchantOffer offer) { - // increase recipe's uses -- offer.increaseUses(); -+ // offer.increaseUses(); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent; handled above in processTrade - } - - @Override diff --git a/patches/server/0955-Add-ShulkerDuplicateEvent.patch b/patches/server/0955-Add-ShulkerDuplicateEvent.patch new file mode 100644 index 0000000000..22315c9908 --- /dev/null +++ b/patches/server/0955-Add-ShulkerDuplicateEvent.patch @@ -0,0 +1,22 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Chase Henderson +Date: Fri, 5 Jan 2024 03:50:10 -0500 +Subject: [PATCH] Add ShulkerDuplicateEvent + + +diff --git a/src/main/java/net/minecraft/world/entity/monster/Shulker.java b/src/main/java/net/minecraft/world/entity/monster/Shulker.java +index c1f73e10d8473251425300cedbed2b82da6a79ce..5215fa54666979ef4da074ddfdb082e7274f2957 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Shulker.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Shulker.java +@@ -485,6 +485,11 @@ public class Shulker extends AbstractGolem implements VariantHolder -Date: Fri, 5 Jan 2024 03:50:10 -0500 -Subject: [PATCH] Add ShulkerDuplicateEvent - - -diff --git a/src/main/java/net/minecraft/world/entity/monster/Shulker.java b/src/main/java/net/minecraft/world/entity/monster/Shulker.java -index c1f73e10d8473251425300cedbed2b82da6a79ce..5215fa54666979ef4da074ddfdb082e7274f2957 100644 ---- a/src/main/java/net/minecraft/world/entity/monster/Shulker.java -+++ b/src/main/java/net/minecraft/world/entity/monster/Shulker.java -@@ -485,6 +485,11 @@ public class Shulker extends AbstractGolem implements VariantHolder +Date: Thu, 11 Jan 2024 19:58:23 +0100 +Subject: [PATCH] Add api for spawn egg texture colors + + +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +index 0a59d4354e332f4dba99404b770eb88f418546ca..4213e5debc1737364de0dae42fbd1cb68dcb0c83 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +@@ -659,6 +659,15 @@ public final class CraftMagicNumbers implements UnsafeValues { + } + // Paper end + ++ // Paper start - spawn egg color visibility ++ @Override ++ public org.bukkit.Color getSpawnEggLayerColor(final EntityType entityType, final int layer) { ++ final net.minecraft.world.entity.EntityType nmsType = org.bukkit.craftbukkit.entity.CraftEntityType.bukkitToMinecraft(entityType); ++ final net.minecraft.world.item.SpawnEggItem eggItem = net.minecraft.world.item.SpawnEggItem.byId(nmsType); ++ return eggItem == null ? null : org.bukkit.Color.fromRGB(eggItem.getColor(layer)); ++ } ++ // Paper end - spawn egg color visibility ++ + /** + * This helper class represents the different NBT Tags. + *

diff --git a/patches/server/0957-Add-Lifecycle-Event-system.patch b/patches/server/0957-Add-Lifecycle-Event-system.patch new file mode 100644 index 0000000000..6b41e179b3 --- /dev/null +++ b/patches/server/0957-Add-Lifecycle-Event-system.patch @@ -0,0 +1,781 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Tue, 18 Jul 2023 17:49:38 -0700 +Subject: [PATCH] Add Lifecycle Event system + +This event system is separate from Bukkit's event system and is +meant for managing resources across reloads and from points in the +PluginBootstrap. + +diff --git a/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrapContextImpl.java b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrapContextImpl.java +index 30b50e6294c6eaade5e17cfaf34600d122e6251c..0bb7694188d5fb75bb756ce75d0060ea980027ee 100644 +--- a/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrapContextImpl.java ++++ b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrapContextImpl.java +@@ -1,6 +1,8 @@ + package io.papermc.paper.plugin.bootstrap; + + import io.papermc.paper.plugin.configuration.PluginMeta; ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager; ++import io.papermc.paper.plugin.lifecycle.event.PaperLifecycleEventManager; + import io.papermc.paper.plugin.provider.PluginProvider; + import java.nio.file.Path; + import net.kyori.adventure.text.logger.slf4j.ComponentLogger; +@@ -12,6 +14,10 @@ public final class PluginBootstrapContextImpl implements BootstrapContext { + private final Path dataFolder; + private final ComponentLogger logger; + private final Path pluginSource; ++ // Paper start - lifecycle events ++ private boolean allowsLifecycleRegistration = true; ++ private final PaperLifecycleEventManager lifecycleEventManager = new PaperLifecycleEventManager<>(this, () -> this.allowsLifecycleRegistration); // Paper - lifecycle events ++ // Paper end - lifecycle events + + public PluginBootstrapContextImpl(PluginMeta config, Path dataFolder, ComponentLogger logger, Path pluginSource) { + this.config = config; +@@ -45,4 +51,20 @@ public final class PluginBootstrapContextImpl implements BootstrapContext { + public @NotNull Path getPluginSource() { + return this.pluginSource; + } ++ ++ // Paper start - lifecycle event system ++ @Override ++ public @NotNull PluginMeta getPluginMeta() { ++ return this.config; ++ } ++ ++ @Override ++ public LifecycleEventManager getLifecycleManager() { ++ return this.lifecycleEventManager; ++ } ++ ++ public void lockLifecycleEventRegistration() { ++ this.allowsLifecycleRegistration = false; ++ } ++ // Paper end + } +diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/LifecycleEventRunner.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/LifecycleEventRunner.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f84c9c80e701231e5c33ac3c5573f1093e80f38b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/LifecycleEventRunner.java +@@ -0,0 +1,110 @@ ++package io.papermc.paper.plugin.lifecycle.event; ++ ++import com.google.common.base.Suppliers; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.plugin.bootstrap.BootstrapContext; ++import io.papermc.paper.plugin.lifecycle.event.registrar.PaperRegistrar; ++import io.papermc.paper.plugin.lifecycle.event.registrar.RegistrarEvent; ++import io.papermc.paper.plugin.lifecycle.event.registrar.RegistrarEventImpl; ++import io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent; ++import io.papermc.paper.plugin.lifecycle.event.types.AbstractLifecycleEventType; ++import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventType; ++import io.papermc.paper.plugin.lifecycle.event.types.OwnerAwareLifecycleEvent; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Set; ++import java.util.function.Predicate; ++import java.util.function.Supplier; ++import org.bukkit.plugin.Plugin; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.slf4j.Logger; ++ ++@DefaultQualifier(NonNull.class) ++public class LifecycleEventRunner { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ private static final Supplier>> BLOCKS_RELOADING = Suppliers.memoize(() -> Set.of( // lazy due to cyclic initialization ++ )); ++ public static final LifecycleEventRunner INSTANCE = new LifecycleEventRunner(); ++ ++ private final List> lifecycleEventTypes = new ArrayList<>(); ++ private boolean blockPluginReloading = false; ++ ++ public void checkRegisteredHandler(final LifecycleEventOwner owner, final LifecycleEventType eventType) { ++ /* ++ Lifecycle event handlers for reloadable events that are registered from the BootstrapContext prevent ++ the server from reloading plugins. This is because reloading plugins requires disabling all the plugins, ++ running the reload logic (which would include places where these events should fire) and then re-enabling plugins. ++ */ ++ if (owner instanceof BootstrapContext && BLOCKS_RELOADING.get().contains(eventType)) { ++ this.blockPluginReloading = true; ++ } ++ } ++ ++ public boolean blocksPluginReloading() { ++ return this.blockPluginReloading; ++ } ++ ++ public > ET addEventType(final ET eventType) { ++ this.lifecycleEventTypes.add(eventType); ++ return eventType; ++ } ++ ++ public void callEvent(final LifecycleEventType eventType, final E event) { ++ this.callEvent(eventType, event, $ -> true); ++ } ++ ++ public void callEvent(final LifecycleEventType eventType, final E event, final Predicate ownerPredicate) { ++ final AbstractLifecycleEventType lifecycleEventType = (AbstractLifecycleEventType) eventType; ++ lifecycleEventType.forEachHandler(registeredHandler -> { ++ try { ++ if (event instanceof final OwnerAwareLifecycleEvent ownerAwareEvent) { ++ ownerAwareGenericHelper(ownerAwareEvent, registeredHandler.owner()); ++ } ++ registeredHandler.lifecycleEventHandler().run(event); ++ } catch (final Throwable ex) { ++ LOGGER.error("Could not run '{}' lifecycle event handler from {}", lifecycleEventType.name(), registeredHandler.owner().getPluginMeta().getDisplayName(), ex); ++ } finally { ++ if (event instanceof final OwnerAwareLifecycleEvent ownerAwareEvent) { ++ ownerAwareEvent.setOwner(null); ++ } ++ } ++ }, handler -> ownerPredicate.test(handler.owner())); ++ event.invalidate(); ++ } ++ ++ private static void ownerAwareGenericHelper(final OwnerAwareLifecycleEvent event, final LifecycleEventOwner possibleOwner) { ++ final @Nullable O owner = event.castOwner(possibleOwner); ++ if (owner != null) { ++ event.setOwner(owner); ++ } else { ++ throw new IllegalStateException("Found invalid owner " + possibleOwner + " for event " + event); ++ } ++ } ++ ++ public void unregisterAllEventHandlersFor(final Plugin plugin) { ++ for (final LifecycleEventType lifecycleEventType : this.lifecycleEventTypes) { ++ this.removeEventHandlersOwnedBy(lifecycleEventType, plugin); ++ } ++ } ++ ++ private void removeEventHandlersOwnedBy(final LifecycleEventType eventType, final Plugin possibleOwner) { ++ final AbstractLifecycleEventType lifecycleEventType = (AbstractLifecycleEventType) eventType; ++ lifecycleEventType.removeMatching(registeredHandler -> registeredHandler.owner().getPluginMeta().getName().equals(possibleOwner.getPluginMeta().getName())); ++ } ++ ++ @SuppressWarnings("unchecked") ++ public > void callStaticRegistrarEvent(final LifecycleEventType, ?> lifecycleEventType, final R registrar, final Class ownerClass) { ++ this.callEvent((LifecycleEventType, ?>) lifecycleEventType, new RegistrarEventImpl<>(registrar, ownerClass), ownerClass::isInstance); ++ } ++ ++ @SuppressWarnings("unchecked") ++ public > void callReloadableRegistrarEvent(final LifecycleEventType, ?> lifecycleEventType, final R registrar, final Class ownerClass, final ReloadableRegistrarEvent.Cause cause) { ++ this.callEvent((LifecycleEventType, ?>) lifecycleEventType, new RegistrarEventImpl.ReloadableImpl<>(registrar, ownerClass, cause), ownerClass::isInstance); ++ } ++ ++ private LifecycleEventRunner() { ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/PaperLifecycleEvent.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/PaperLifecycleEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e941405269a773e8a77e26ffd1afd84f53fadff5 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/PaperLifecycleEvent.java +@@ -0,0 +1,10 @@ ++package io.papermc.paper.plugin.lifecycle.event; ++ ++public interface PaperLifecycleEvent extends LifecycleEvent { ++ ++ // called after all handlers have been run. Can be ++ // used to invalid various contexts to plugins can't ++ // try to re-use them by storing them from the event ++ default void invalidate() { ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/PaperLifecycleEventManager.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/PaperLifecycleEventManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f1be5b9a29435bae0afd2bd951bfe88d1669e7eb +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/PaperLifecycleEventManager.java +@@ -0,0 +1,26 @@ ++package io.papermc.paper.plugin.lifecycle.event; ++ ++import com.google.common.base.Preconditions; ++import io.papermc.paper.plugin.lifecycle.event.handler.configuration.AbstractLifecycleEventHandlerConfiguration; ++import io.papermc.paper.plugin.lifecycle.event.handler.configuration.LifecycleEventHandlerConfiguration; ++import java.util.function.BooleanSupplier; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public final class PaperLifecycleEventManager implements LifecycleEventManager { ++ ++ private final O owner; ++ public final BooleanSupplier registrationCheck; ++ ++ public PaperLifecycleEventManager(final O owner, final BooleanSupplier registrationCheck) { ++ this.owner = owner; ++ this.registrationCheck = registrationCheck; ++ } ++ ++ @Override ++ public void registerEventHandler(final LifecycleEventHandlerConfiguration handlerConfiguration) { ++ Preconditions.checkState(this.registrationCheck.getAsBoolean(), "Cannot register lifecycle event handlers"); ++ ((AbstractLifecycleEventHandlerConfiguration) handlerConfiguration).registerFrom(this.owner); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/AbstractLifecycleEventHandlerConfiguration.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/AbstractLifecycleEventHandlerConfiguration.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6a85a4f581612efff04c1a955493aa2e32476277 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/AbstractLifecycleEventHandlerConfiguration.java +@@ -0,0 +1,26 @@ ++package io.papermc.paper.plugin.lifecycle.event.handler.configuration; ++ ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; ++import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler; ++import io.papermc.paper.plugin.lifecycle.event.types.AbstractLifecycleEventType; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public abstract class AbstractLifecycleEventHandlerConfiguration> implements LifecycleEventHandlerConfiguration { ++ ++ private final LifecycleEventHandler handler; ++ private final AbstractLifecycleEventType type; ++ ++ protected AbstractLifecycleEventHandlerConfiguration(final LifecycleEventHandler handler, final AbstractLifecycleEventType type) { ++ this.handler = handler; ++ this.type = type; ++ } ++ ++ public abstract CI config(); ++ ++ public final void registerFrom(final O owner) { ++ this.type.tryRegister(owner, this.handler, this.config()); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/MonitorLifecycleEventHandlerConfigurationImpl.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/MonitorLifecycleEventHandlerConfigurationImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e0699fcd0a098abc5e1206e7c0fa80b96eca7884 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/MonitorLifecycleEventHandlerConfigurationImpl.java +@@ -0,0 +1,33 @@ ++package io.papermc.paper.plugin.lifecycle.event.handler.configuration; ++ ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; ++import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler; ++import io.papermc.paper.plugin.lifecycle.event.types.AbstractLifecycleEventType; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public class MonitorLifecycleEventHandlerConfigurationImpl extends AbstractLifecycleEventHandlerConfiguration> implements MonitorLifecycleEventHandlerConfiguration { ++ ++ private boolean monitor = false; ++ ++ public MonitorLifecycleEventHandlerConfigurationImpl(final LifecycleEventHandler handler, final AbstractLifecycleEventType> eventType) { ++ super(handler, eventType); ++ } ++ ++ @Override ++ public MonitorLifecycleEventHandlerConfigurationImpl config() { ++ return this; ++ } ++ ++ public boolean isMonitor() { ++ return this.monitor; ++ } ++ ++ @Override ++ public MonitorLifecycleEventHandlerConfiguration monitor() { ++ this.monitor = true; ++ return this; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/PrioritizedLifecycleEventHandlerConfigurationImpl.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/PrioritizedLifecycleEventHandlerConfigurationImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c1d0070fc1594f7a7c29d7dc679da7b347a7140b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/PrioritizedLifecycleEventHandlerConfigurationImpl.java +@@ -0,0 +1,43 @@ ++package io.papermc.paper.plugin.lifecycle.event.handler.configuration; ++ ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; ++import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler; ++import io.papermc.paper.plugin.lifecycle.event.types.AbstractLifecycleEventType; ++import java.util.OptionalInt; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public class PrioritizedLifecycleEventHandlerConfigurationImpl extends AbstractLifecycleEventHandlerConfiguration> implements PrioritizedLifecycleEventHandlerConfiguration { ++ ++ private static final OptionalInt DEFAULT_PRIORITY = OptionalInt.of(0); ++ private static final OptionalInt MONITOR_PRIORITY = OptionalInt.empty(); ++ ++ private OptionalInt priority = DEFAULT_PRIORITY; ++ ++ public PrioritizedLifecycleEventHandlerConfigurationImpl(final LifecycleEventHandler handler, final AbstractLifecycleEventType> eventType) { ++ super(handler, eventType); ++ } ++ ++ @Override ++ public PrioritizedLifecycleEventHandlerConfigurationImpl config() { ++ return this; ++ } ++ ++ public OptionalInt priority() { ++ return this.priority; ++ } ++ ++ @Override ++ public PrioritizedLifecycleEventHandlerConfiguration priority(final int priority) { ++ this.priority = OptionalInt.of(priority); ++ return this; ++ } ++ ++ @Override ++ public PrioritizedLifecycleEventHandlerConfiguration monitor() { ++ this.priority = MONITOR_PRIORITY; ++ return this; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/registrar/PaperRegistrar.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/registrar/PaperRegistrar.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b2586c881988fbabe07eef1b43eb1b55f2d3fa52 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/registrar/PaperRegistrar.java +@@ -0,0 +1,15 @@ ++package io.papermc.paper.plugin.lifecycle.event.registrar; ++ ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public interface PaperRegistrar extends Registrar { ++ ++ void setCurrentContext(@Nullable O owner); ++ ++ default void invalidate() { ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/registrar/RegistrarEventImpl.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/registrar/RegistrarEventImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6d530c52aaf0dc2cdfe3bd56af557274a7f44256 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/registrar/RegistrarEventImpl.java +@@ -0,0 +1,70 @@ ++package io.papermc.paper.plugin.lifecycle.event.registrar; ++ ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; ++import io.papermc.paper.plugin.lifecycle.event.PaperLifecycleEvent; ++import io.papermc.paper.plugin.lifecycle.event.types.OwnerAwareLifecycleEvent; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public class RegistrarEventImpl, O extends LifecycleEventOwner> implements PaperLifecycleEvent, OwnerAwareLifecycleEvent, RegistrarEvent { ++ ++ private final R registrar; ++ private final Class ownerClass; ++ ++ public RegistrarEventImpl(final R registrar, final Class ownerClass) { ++ this.registrar = registrar; ++ this.ownerClass = ownerClass; ++ } ++ ++ @Override ++ public R registrar() { ++ return this.registrar; ++ } ++ ++ @Override ++ public final void setOwner(final @Nullable O owner) { ++ this.registrar.setCurrentContext(owner); ++ } ++ ++ @Override ++ public final @Nullable O castOwner(final LifecycleEventOwner owner) { ++ return this.ownerClass.isInstance(owner) ? this.ownerClass.cast(owner) : null; ++ } ++ ++ @Override ++ public void invalidate() { ++ this.registrar.invalidate(); ++ } ++ ++ @Override ++ public String toString() { ++ return "RegistrarEventImpl{" + ++ "registrar=" + this.registrar + ++ ", ownerClass=" + this.ownerClass + ++ '}'; ++ } ++ ++ public static class ReloadableImpl, O extends LifecycleEventOwner> extends RegistrarEventImpl implements ReloadableRegistrarEvent { ++ ++ private final ReloadableRegistrarEvent.Cause cause; ++ ++ public ReloadableImpl(final R registrar, final Class ownerClass, final Cause cause) { ++ super(registrar, ownerClass); ++ this.cause = cause; ++ } ++ ++ @Override ++ public Cause cause() { ++ return this.cause; ++ } ++ ++ @Override ++ public String toString() { ++ return "ReloadableImpl{" + ++ "cause=" + this.cause + ++ "} " + super.toString(); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/AbstractLifecycleEventType.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/AbstractLifecycleEventType.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a65fb37f4a729e2fe9fb81af822db626ec7e6d7b +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/AbstractLifecycleEventType.java +@@ -0,0 +1,50 @@ ++package io.papermc.paper.plugin.lifecycle.event.types; ++ ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner; ++import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler; ++import io.papermc.paper.plugin.lifecycle.event.handler.configuration.AbstractLifecycleEventHandlerConfiguration; ++import io.papermc.paper.plugin.lifecycle.event.handler.configuration.LifecycleEventHandlerConfiguration; ++import java.util.function.Consumer; ++import java.util.function.Predicate; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public abstract class AbstractLifecycleEventType, CI extends AbstractLifecycleEventHandlerConfiguration> implements LifecycleEventType { ++ ++ private final String name; ++ private final Class ownerType; ++ ++ protected AbstractLifecycleEventType(final String name, final Class ownerType) { ++ this.name = name; ++ this.ownerType = ownerType; ++ } ++ ++ @Override ++ public String name() { ++ return this.name; ++ } ++ ++ private void verifyOwner(final O owner) { ++ if (!this.ownerType.isInstance(owner)) { ++ throw new IllegalArgumentException("You cannot register the lifecycle event '" + this.name + "' on " + owner); ++ } ++ } ++ ++ public abstract void forEachHandler(Consumer> consumer, Predicate> predicate); ++ ++ public abstract void removeMatching(Predicate> predicate); ++ ++ protected abstract void register(O owner, LifecycleEventHandler handler, CI config); ++ ++ public final void tryRegister(final O owner, final LifecycleEventHandler handler, final CI config) { ++ this.verifyOwner(owner); ++ LifecycleEventRunner.INSTANCE.checkRegisteredHandler(owner, this); ++ this.register(owner, handler, config); ++ } ++ ++ public record RegisteredHandler(O owner, LifecycleEventHandler lifecycleEventHandler) { ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/LifecycleEventTypeProviderImpl.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/LifecycleEventTypeProviderImpl.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0886edad92b40276f268bd745b31bac359fd28af +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/LifecycleEventTypeProviderImpl.java +@@ -0,0 +1,25 @@ ++package io.papermc.paper.plugin.lifecycle.event.types; ++ ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public final class LifecycleEventTypeProviderImpl implements LifecycleEventTypeProvider { ++ ++ public static LifecycleEventTypeProviderImpl instance() { ++ return (LifecycleEventTypeProviderImpl) LifecycleEventTypeProvider.PROVIDER; ++ } ++ ++ @Override ++ public LifecycleEventType.Monitorable monitor(final String name, final Class ownerType) { ++ return LifecycleEventRunner.INSTANCE.addEventType(new MonitorableLifecycleEventType<>(name, ownerType)); ++ } ++ ++ @Override ++ public LifecycleEventType.Prioritizable prioritized(final String name, final Class ownerType) { ++ return LifecycleEventRunner.INSTANCE.addEventType(new PrioritizableLifecycleEventType<>(name, ownerType)); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/MonitorableLifecycleEventType.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/MonitorableLifecycleEventType.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6d92c1d3adf220154dfe7cba3a3f8158356c3e3c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/MonitorableLifecycleEventType.java +@@ -0,0 +1,54 @@ ++package io.papermc.paper.plugin.lifecycle.event.types; ++ ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; ++import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler; ++import io.papermc.paper.plugin.lifecycle.event.handler.configuration.MonitorLifecycleEventHandlerConfiguration; ++import io.papermc.paper.plugin.lifecycle.event.handler.configuration.MonitorLifecycleEventHandlerConfigurationImpl; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.function.Consumer; ++import java.util.function.Predicate; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public class MonitorableLifecycleEventType extends AbstractLifecycleEventType, MonitorLifecycleEventHandlerConfigurationImpl> implements LifecycleEventType.Monitorable { ++ ++ final List> handlers = new ArrayList<>(); ++ int nonMonitorIdx = 0; ++ ++ public MonitorableLifecycleEventType(final String name, final Class ownerType) { ++ super(name, ownerType); ++ } ++ ++ @Override ++ public MonitorLifecycleEventHandlerConfigurationImpl newHandler(final LifecycleEventHandler handler) { ++ return new MonitorLifecycleEventHandlerConfigurationImpl<>(handler, this); ++ } ++ ++ @Override ++ protected void register(final O owner, final LifecycleEventHandler handler, final MonitorLifecycleEventHandlerConfigurationImpl config) { ++ final RegisteredHandler registeredHandler = new RegisteredHandler<>(owner, handler); ++ if (!config.isMonitor()) { ++ this.handlers.add(this.nonMonitorIdx, registeredHandler); ++ this.nonMonitorIdx++; ++ } else { ++ this.handlers.add(registeredHandler); ++ } ++ } ++ ++ @Override ++ public void forEachHandler(final Consumer> consumer, final Predicate> predicate) { ++ for (final RegisteredHandler handler : this.handlers) { ++ if (predicate.test(handler)) { ++ consumer.accept(handler); ++ } ++ } ++ } ++ ++ @Override ++ public void removeMatching(final Predicate> predicate) { ++ this.handlers.removeIf(predicate); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/OwnerAwareLifecycleEvent.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/OwnerAwareLifecycleEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3e7e7474f301c0725fa2bcd6e19e476fc35f2d5a +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/OwnerAwareLifecycleEvent.java +@@ -0,0 +1,15 @@ ++package io.papermc.paper.plugin.lifecycle.event.types; ++ ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public interface OwnerAwareLifecycleEvent extends LifecycleEvent { ++ ++ void setOwner(@Nullable O owner); ++ ++ @Nullable O castOwner(LifecycleEventOwner owner); ++} +diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/PrioritizableLifecycleEventType.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/PrioritizableLifecycleEventType.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6629f7fabf66ce761024268043cc30076ba8a3f1 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/PrioritizableLifecycleEventType.java +@@ -0,0 +1,64 @@ ++package io.papermc.paper.plugin.lifecycle.event.types; ++ ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; ++import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; ++import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler; ++import io.papermc.paper.plugin.lifecycle.event.handler.configuration.PrioritizedLifecycleEventHandlerConfiguration; ++import io.papermc.paper.plugin.lifecycle.event.handler.configuration.PrioritizedLifecycleEventHandlerConfigurationImpl; ++import java.util.ArrayList; ++import java.util.Comparator; ++import java.util.List; ++import java.util.OptionalInt; ++import java.util.function.Consumer; ++import java.util.function.Predicate; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public class PrioritizableLifecycleEventType extends AbstractLifecycleEventType, PrioritizedLifecycleEventHandlerConfigurationImpl> implements LifecycleEventType.Prioritizable { ++ ++ private static final Comparator> COMPARATOR = Comparator.comparing(PrioritizedHandler::priority, (o1, o2) -> { ++ if (o1.equals(o2)) { ++ return 0; ++ } else if (o1.isEmpty()) { ++ return 1; ++ } else if (o2.isEmpty()) { ++ return -1; ++ } else { ++ return Integer.compare(o1.getAsInt(), o2.getAsInt()); ++ } ++ }); ++ ++ private final List> handlers = new ArrayList<>(); ++ ++ public PrioritizableLifecycleEventType(final String name, final Class ownerType) { ++ super(name, ownerType); ++ } ++ ++ @Override ++ public PrioritizedLifecycleEventHandlerConfiguration newHandler(final LifecycleEventHandler handler) { ++ return new PrioritizedLifecycleEventHandlerConfigurationImpl<>(handler, this); ++ } ++ ++ @Override ++ protected void register(final O owner, final LifecycleEventHandler handler, final PrioritizedLifecycleEventHandlerConfigurationImpl config) { ++ this.handlers.add(new PrioritizedHandler<>(new RegisteredHandler<>(owner, handler), config.priority())); ++ this.handlers.sort(COMPARATOR); ++ } ++ ++ @Override ++ public void forEachHandler(final Consumer> consumer, final Predicate> predicate) { ++ for (final PrioritizedHandler handler : this.handlers) { ++ if (predicate.test(handler.handler())) { ++ consumer.accept(handler.handler()); ++ } ++ } ++ } ++ ++ @Override ++ public void removeMatching(final Predicate> predicate) { ++ this.handlers.removeIf(prioritizedHandler -> predicate.test(prioritizedHandler.handler())); ++ } ++ ++ private record PrioritizedHandler(RegisteredHandler handler, OptionalInt priority) {} ++} +diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java +index 834b85f24df023642f8abf7213fe578ac8c17a3e..3e82ea07ca4194844c5528446e2c4a46ff4acee5 100644 +--- a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java ++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java +@@ -293,6 +293,15 @@ class PaperPluginInstanceManager { + + pluginName + " (Is it up to date?)", ex, plugin); // Paper + } + ++ // Paper start - lifecycle event system ++ try { ++ io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner.INSTANCE.unregisterAllEventHandlersFor(plugin); ++ } catch (Throwable ex) { ++ this.handlePluginException("Error occurred (in the plugin loader) while unregistering lifecycle event handlers for " ++ + pluginName + " (Is it up to date?)", ex, plugin); ++ } ++ // Paper end ++ + try { + this.server.getMessenger().unregisterIncomingPluginChannel(plugin); + this.server.getMessenger().unregisterOutgoingPluginChannel(plugin); +diff --git a/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java +index 2e96308696e131f3f013469a395e5ddda2c5d529..65a66e484c1c39c5f41d97db52f31c67b4479d20 100644 +--- a/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java ++++ b/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java +@@ -32,8 +32,9 @@ public class BootstrapProviderStorage extends SimpleProviderStorage provider, PluginBootstrap provided) { + try { +- BootstrapContext context = PluginBootstrapContextImpl.create(provider, PluginInitializerManager.instance().pluginDirectoryPath()); ++ PluginBootstrapContextImpl context = PluginBootstrapContextImpl.create(provider, PluginInitializerManager.instance().pluginDirectoryPath()); // Paper - lifecycle events + provided.bootstrap(context); ++ context.lockLifecycleEventRegistration(); // Paper - lifecycle events + return true; + } catch (Throwable e) { + LOGGER.error("Failed to run bootstrapper for %s. This plugin will not be loaded.".formatted(provider.getSource()), e); +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 1ba71b0cb6199ee4c3e2a04cfcd57cd3f4908bca..cacbb73e07ec2ea55d10af20c5f2a6d234c9bff3 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -1018,6 +1018,11 @@ public final class CraftServer implements Server { + + @Override + public void reload() { ++ // Paper start - lifecycle events ++ if (io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner.INSTANCE.blocksPluginReloading()) { ++ throw new IllegalStateException("A lifecycle event handler has been registered which makes reloading plugins not possible"); ++ } ++ // Paper end - lifecycle events + org.spigotmc.WatchdogThread.hasStarted = false; // Paper - Disable watchdog early timeout on reload + this.reloadCount++; + this.configuration = YamlConfiguration.loadConfiguration(this.getConfigFile()); +diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java +index d96399e9bf1a58db5a4a22e58abb99e7660e0694..66bdac50130f523f9dc4379b103b7a469f9ca36b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java ++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java +@@ -143,4 +143,11 @@ public class MinecraftInternalPlugin extends PluginBase { + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + throw new UnsupportedOperationException("Not supported."); + } ++ ++ // Paper start - lifecycle events ++ @Override ++ public @NotNull io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager getLifecycleManager() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ // Paper end - lifecycle events + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +index 4213e5debc1737364de0dae42fbd1cb68dcb0c83..92d7e2435ea5c8b6fe9e5d2abb9d3c8cb350ef4d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +@@ -668,6 +668,13 @@ public final class CraftMagicNumbers implements UnsafeValues { + } + // Paper end - spawn egg color visibility + ++ // Paper start - lifecycle event API ++ @Override ++ public io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager createPluginLifecycleEventManager(final org.bukkit.plugin.java.JavaPlugin plugin, final java.util.function.BooleanSupplier registrationCheck) { ++ return new io.papermc.paper.plugin.lifecycle.event.PaperLifecycleEventManager<>(plugin, registrationCheck); ++ } ++ // Paper end - lifecycle event API ++ + /** + * This helper class represents the different NBT Tags. + *

+diff --git a/src/main/resources/META-INF/services/io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventTypeProvider b/src/main/resources/META-INF/services/io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventTypeProvider +new file mode 100644 +index 0000000000000000000000000000000000000000..808b1192b60348ad05f0bfbdeda6f94df4876743 +--- /dev/null ++++ b/src/main/resources/META-INF/services/io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventTypeProvider +@@ -0,0 +1 @@ ++io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventTypeProviderImpl +diff --git a/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java b/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java +index 1d14f530ef888102e47eeeaf0d1a6076e51871c4..90cf0c702ca2ff9de64d9718ecba5f2d128953a6 100644 +--- a/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java ++++ b/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java +@@ -143,4 +143,11 @@ public class PaperTestPlugin extends PluginBase { + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + throw new UnsupportedOperationException("Not supported."); + } ++ ++ // Paper start - lifecycle events ++ @Override ++ public io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager getLifecycleManager() { ++ throw new UnsupportedOperationException("Not supported."); ++ } ++ // Paper end - lifecycle events + } diff --git a/patches/server/0957-Add-api-for-spawn-egg-texture-colors.patch b/patches/server/0957-Add-api-for-spawn-egg-texture-colors.patch deleted file mode 100644 index 3f171b51f4..0000000000 --- a/patches/server/0957-Add-api-for-spawn-egg-texture-colors.patch +++ /dev/null @@ -1,26 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Luis -Date: Thu, 11 Jan 2024 19:58:23 +0100 -Subject: [PATCH] Add api for spawn egg texture colors - - -diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -index 0a59d4354e332f4dba99404b770eb88f418546ca..4213e5debc1737364de0dae42fbd1cb68dcb0c83 100644 ---- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -+++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -@@ -659,6 +659,15 @@ public final class CraftMagicNumbers implements UnsafeValues { - } - // Paper end - -+ // Paper start - spawn egg color visibility -+ @Override -+ public org.bukkit.Color getSpawnEggLayerColor(final EntityType entityType, final int layer) { -+ final net.minecraft.world.entity.EntityType nmsType = org.bukkit.craftbukkit.entity.CraftEntityType.bukkitToMinecraft(entityType); -+ final net.minecraft.world.item.SpawnEggItem eggItem = net.minecraft.world.item.SpawnEggItem.byId(nmsType); -+ return eggItem == null ? null : org.bukkit.Color.fromRGB(eggItem.getColor(layer)); -+ } -+ // Paper end - spawn egg color visibility -+ - /** - * This helper class represents the different NBT Tags. - *

diff --git a/patches/server/0958-Add-Lifecycle-Event-system.patch b/patches/server/0958-Add-Lifecycle-Event-system.patch deleted file mode 100644 index 6b41e179b3..0000000000 --- a/patches/server/0958-Add-Lifecycle-Event-system.patch +++ /dev/null @@ -1,781 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Tue, 18 Jul 2023 17:49:38 -0700 -Subject: [PATCH] Add Lifecycle Event system - -This event system is separate from Bukkit's event system and is -meant for managing resources across reloads and from points in the -PluginBootstrap. - -diff --git a/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrapContextImpl.java b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrapContextImpl.java -index 30b50e6294c6eaade5e17cfaf34600d122e6251c..0bb7694188d5fb75bb756ce75d0060ea980027ee 100644 ---- a/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrapContextImpl.java -+++ b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrapContextImpl.java -@@ -1,6 +1,8 @@ - package io.papermc.paper.plugin.bootstrap; - - import io.papermc.paper.plugin.configuration.PluginMeta; -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager; -+import io.papermc.paper.plugin.lifecycle.event.PaperLifecycleEventManager; - import io.papermc.paper.plugin.provider.PluginProvider; - import java.nio.file.Path; - import net.kyori.adventure.text.logger.slf4j.ComponentLogger; -@@ -12,6 +14,10 @@ public final class PluginBootstrapContextImpl implements BootstrapContext { - private final Path dataFolder; - private final ComponentLogger logger; - private final Path pluginSource; -+ // Paper start - lifecycle events -+ private boolean allowsLifecycleRegistration = true; -+ private final PaperLifecycleEventManager lifecycleEventManager = new PaperLifecycleEventManager<>(this, () -> this.allowsLifecycleRegistration); // Paper - lifecycle events -+ // Paper end - lifecycle events - - public PluginBootstrapContextImpl(PluginMeta config, Path dataFolder, ComponentLogger logger, Path pluginSource) { - this.config = config; -@@ -45,4 +51,20 @@ public final class PluginBootstrapContextImpl implements BootstrapContext { - public @NotNull Path getPluginSource() { - return this.pluginSource; - } -+ -+ // Paper start - lifecycle event system -+ @Override -+ public @NotNull PluginMeta getPluginMeta() { -+ return this.config; -+ } -+ -+ @Override -+ public LifecycleEventManager getLifecycleManager() { -+ return this.lifecycleEventManager; -+ } -+ -+ public void lockLifecycleEventRegistration() { -+ this.allowsLifecycleRegistration = false; -+ } -+ // Paper end - } -diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/LifecycleEventRunner.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/LifecycleEventRunner.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f84c9c80e701231e5c33ac3c5573f1093e80f38b ---- /dev/null -+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/LifecycleEventRunner.java -@@ -0,0 +1,110 @@ -+package io.papermc.paper.plugin.lifecycle.event; -+ -+import com.google.common.base.Suppliers; -+import com.mojang.logging.LogUtils; -+import io.papermc.paper.plugin.bootstrap.BootstrapContext; -+import io.papermc.paper.plugin.lifecycle.event.registrar.PaperRegistrar; -+import io.papermc.paper.plugin.lifecycle.event.registrar.RegistrarEvent; -+import io.papermc.paper.plugin.lifecycle.event.registrar.RegistrarEventImpl; -+import io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent; -+import io.papermc.paper.plugin.lifecycle.event.types.AbstractLifecycleEventType; -+import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventType; -+import io.papermc.paper.plugin.lifecycle.event.types.OwnerAwareLifecycleEvent; -+import java.util.ArrayList; -+import java.util.List; -+import java.util.Set; -+import java.util.function.Predicate; -+import java.util.function.Supplier; -+import org.bukkit.plugin.Plugin; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.checker.nullness.qual.Nullable; -+import org.checkerframework.framework.qual.DefaultQualifier; -+import org.slf4j.Logger; -+ -+@DefaultQualifier(NonNull.class) -+public class LifecycleEventRunner { -+ -+ private static final Logger LOGGER = LogUtils.getClassLogger(); -+ private static final Supplier>> BLOCKS_RELOADING = Suppliers.memoize(() -> Set.of( // lazy due to cyclic initialization -+ )); -+ public static final LifecycleEventRunner INSTANCE = new LifecycleEventRunner(); -+ -+ private final List> lifecycleEventTypes = new ArrayList<>(); -+ private boolean blockPluginReloading = false; -+ -+ public void checkRegisteredHandler(final LifecycleEventOwner owner, final LifecycleEventType eventType) { -+ /* -+ Lifecycle event handlers for reloadable events that are registered from the BootstrapContext prevent -+ the server from reloading plugins. This is because reloading plugins requires disabling all the plugins, -+ running the reload logic (which would include places where these events should fire) and then re-enabling plugins. -+ */ -+ if (owner instanceof BootstrapContext && BLOCKS_RELOADING.get().contains(eventType)) { -+ this.blockPluginReloading = true; -+ } -+ } -+ -+ public boolean blocksPluginReloading() { -+ return this.blockPluginReloading; -+ } -+ -+ public > ET addEventType(final ET eventType) { -+ this.lifecycleEventTypes.add(eventType); -+ return eventType; -+ } -+ -+ public void callEvent(final LifecycleEventType eventType, final E event) { -+ this.callEvent(eventType, event, $ -> true); -+ } -+ -+ public void callEvent(final LifecycleEventType eventType, final E event, final Predicate ownerPredicate) { -+ final AbstractLifecycleEventType lifecycleEventType = (AbstractLifecycleEventType) eventType; -+ lifecycleEventType.forEachHandler(registeredHandler -> { -+ try { -+ if (event instanceof final OwnerAwareLifecycleEvent ownerAwareEvent) { -+ ownerAwareGenericHelper(ownerAwareEvent, registeredHandler.owner()); -+ } -+ registeredHandler.lifecycleEventHandler().run(event); -+ } catch (final Throwable ex) { -+ LOGGER.error("Could not run '{}' lifecycle event handler from {}", lifecycleEventType.name(), registeredHandler.owner().getPluginMeta().getDisplayName(), ex); -+ } finally { -+ if (event instanceof final OwnerAwareLifecycleEvent ownerAwareEvent) { -+ ownerAwareEvent.setOwner(null); -+ } -+ } -+ }, handler -> ownerPredicate.test(handler.owner())); -+ event.invalidate(); -+ } -+ -+ private static void ownerAwareGenericHelper(final OwnerAwareLifecycleEvent event, final LifecycleEventOwner possibleOwner) { -+ final @Nullable O owner = event.castOwner(possibleOwner); -+ if (owner != null) { -+ event.setOwner(owner); -+ } else { -+ throw new IllegalStateException("Found invalid owner " + possibleOwner + " for event " + event); -+ } -+ } -+ -+ public void unregisterAllEventHandlersFor(final Plugin plugin) { -+ for (final LifecycleEventType lifecycleEventType : this.lifecycleEventTypes) { -+ this.removeEventHandlersOwnedBy(lifecycleEventType, plugin); -+ } -+ } -+ -+ private void removeEventHandlersOwnedBy(final LifecycleEventType eventType, final Plugin possibleOwner) { -+ final AbstractLifecycleEventType lifecycleEventType = (AbstractLifecycleEventType) eventType; -+ lifecycleEventType.removeMatching(registeredHandler -> registeredHandler.owner().getPluginMeta().getName().equals(possibleOwner.getPluginMeta().getName())); -+ } -+ -+ @SuppressWarnings("unchecked") -+ public > void callStaticRegistrarEvent(final LifecycleEventType, ?> lifecycleEventType, final R registrar, final Class ownerClass) { -+ this.callEvent((LifecycleEventType, ?>) lifecycleEventType, new RegistrarEventImpl<>(registrar, ownerClass), ownerClass::isInstance); -+ } -+ -+ @SuppressWarnings("unchecked") -+ public > void callReloadableRegistrarEvent(final LifecycleEventType, ?> lifecycleEventType, final R registrar, final Class ownerClass, final ReloadableRegistrarEvent.Cause cause) { -+ this.callEvent((LifecycleEventType, ?>) lifecycleEventType, new RegistrarEventImpl.ReloadableImpl<>(registrar, ownerClass, cause), ownerClass::isInstance); -+ } -+ -+ private LifecycleEventRunner() { -+ } -+} -diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/PaperLifecycleEvent.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/PaperLifecycleEvent.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e941405269a773e8a77e26ffd1afd84f53fadff5 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/PaperLifecycleEvent.java -@@ -0,0 +1,10 @@ -+package io.papermc.paper.plugin.lifecycle.event; -+ -+public interface PaperLifecycleEvent extends LifecycleEvent { -+ -+ // called after all handlers have been run. Can be -+ // used to invalid various contexts to plugins can't -+ // try to re-use them by storing them from the event -+ default void invalidate() { -+ } -+} -diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/PaperLifecycleEventManager.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/PaperLifecycleEventManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f1be5b9a29435bae0afd2bd951bfe88d1669e7eb ---- /dev/null -+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/PaperLifecycleEventManager.java -@@ -0,0 +1,26 @@ -+package io.papermc.paper.plugin.lifecycle.event; -+ -+import com.google.common.base.Preconditions; -+import io.papermc.paper.plugin.lifecycle.event.handler.configuration.AbstractLifecycleEventHandlerConfiguration; -+import io.papermc.paper.plugin.lifecycle.event.handler.configuration.LifecycleEventHandlerConfiguration; -+import java.util.function.BooleanSupplier; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.framework.qual.DefaultQualifier; -+ -+@DefaultQualifier(NonNull.class) -+public final class PaperLifecycleEventManager implements LifecycleEventManager { -+ -+ private final O owner; -+ public final BooleanSupplier registrationCheck; -+ -+ public PaperLifecycleEventManager(final O owner, final BooleanSupplier registrationCheck) { -+ this.owner = owner; -+ this.registrationCheck = registrationCheck; -+ } -+ -+ @Override -+ public void registerEventHandler(final LifecycleEventHandlerConfiguration handlerConfiguration) { -+ Preconditions.checkState(this.registrationCheck.getAsBoolean(), "Cannot register lifecycle event handlers"); -+ ((AbstractLifecycleEventHandlerConfiguration) handlerConfiguration).registerFrom(this.owner); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/AbstractLifecycleEventHandlerConfiguration.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/AbstractLifecycleEventHandlerConfiguration.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6a85a4f581612efff04c1a955493aa2e32476277 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/AbstractLifecycleEventHandlerConfiguration.java -@@ -0,0 +1,26 @@ -+package io.papermc.paper.plugin.lifecycle.event.handler.configuration; -+ -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; -+import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler; -+import io.papermc.paper.plugin.lifecycle.event.types.AbstractLifecycleEventType; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.framework.qual.DefaultQualifier; -+ -+@DefaultQualifier(NonNull.class) -+public abstract class AbstractLifecycleEventHandlerConfiguration> implements LifecycleEventHandlerConfiguration { -+ -+ private final LifecycleEventHandler handler; -+ private final AbstractLifecycleEventType type; -+ -+ protected AbstractLifecycleEventHandlerConfiguration(final LifecycleEventHandler handler, final AbstractLifecycleEventType type) { -+ this.handler = handler; -+ this.type = type; -+ } -+ -+ public abstract CI config(); -+ -+ public final void registerFrom(final O owner) { -+ this.type.tryRegister(owner, this.handler, this.config()); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/MonitorLifecycleEventHandlerConfigurationImpl.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/MonitorLifecycleEventHandlerConfigurationImpl.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e0699fcd0a098abc5e1206e7c0fa80b96eca7884 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/MonitorLifecycleEventHandlerConfigurationImpl.java -@@ -0,0 +1,33 @@ -+package io.papermc.paper.plugin.lifecycle.event.handler.configuration; -+ -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; -+import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler; -+import io.papermc.paper.plugin.lifecycle.event.types.AbstractLifecycleEventType; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.framework.qual.DefaultQualifier; -+ -+@DefaultQualifier(NonNull.class) -+public class MonitorLifecycleEventHandlerConfigurationImpl extends AbstractLifecycleEventHandlerConfiguration> implements MonitorLifecycleEventHandlerConfiguration { -+ -+ private boolean monitor = false; -+ -+ public MonitorLifecycleEventHandlerConfigurationImpl(final LifecycleEventHandler handler, final AbstractLifecycleEventType> eventType) { -+ super(handler, eventType); -+ } -+ -+ @Override -+ public MonitorLifecycleEventHandlerConfigurationImpl config() { -+ return this; -+ } -+ -+ public boolean isMonitor() { -+ return this.monitor; -+ } -+ -+ @Override -+ public MonitorLifecycleEventHandlerConfiguration monitor() { -+ this.monitor = true; -+ return this; -+ } -+} -diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/PrioritizedLifecycleEventHandlerConfigurationImpl.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/PrioritizedLifecycleEventHandlerConfigurationImpl.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c1d0070fc1594f7a7c29d7dc679da7b347a7140b ---- /dev/null -+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/PrioritizedLifecycleEventHandlerConfigurationImpl.java -@@ -0,0 +1,43 @@ -+package io.papermc.paper.plugin.lifecycle.event.handler.configuration; -+ -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; -+import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler; -+import io.papermc.paper.plugin.lifecycle.event.types.AbstractLifecycleEventType; -+import java.util.OptionalInt; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.framework.qual.DefaultQualifier; -+ -+@DefaultQualifier(NonNull.class) -+public class PrioritizedLifecycleEventHandlerConfigurationImpl extends AbstractLifecycleEventHandlerConfiguration> implements PrioritizedLifecycleEventHandlerConfiguration { -+ -+ private static final OptionalInt DEFAULT_PRIORITY = OptionalInt.of(0); -+ private static final OptionalInt MONITOR_PRIORITY = OptionalInt.empty(); -+ -+ private OptionalInt priority = DEFAULT_PRIORITY; -+ -+ public PrioritizedLifecycleEventHandlerConfigurationImpl(final LifecycleEventHandler handler, final AbstractLifecycleEventType> eventType) { -+ super(handler, eventType); -+ } -+ -+ @Override -+ public PrioritizedLifecycleEventHandlerConfigurationImpl config() { -+ return this; -+ } -+ -+ public OptionalInt priority() { -+ return this.priority; -+ } -+ -+ @Override -+ public PrioritizedLifecycleEventHandlerConfiguration priority(final int priority) { -+ this.priority = OptionalInt.of(priority); -+ return this; -+ } -+ -+ @Override -+ public PrioritizedLifecycleEventHandlerConfiguration monitor() { -+ this.priority = MONITOR_PRIORITY; -+ return this; -+ } -+} -diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/registrar/PaperRegistrar.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/registrar/PaperRegistrar.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b2586c881988fbabe07eef1b43eb1b55f2d3fa52 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/registrar/PaperRegistrar.java -@@ -0,0 +1,15 @@ -+package io.papermc.paper.plugin.lifecycle.event.registrar; -+ -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.checker.nullness.qual.Nullable; -+import org.checkerframework.framework.qual.DefaultQualifier; -+ -+@DefaultQualifier(NonNull.class) -+public interface PaperRegistrar extends Registrar { -+ -+ void setCurrentContext(@Nullable O owner); -+ -+ default void invalidate() { -+ } -+} -diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/registrar/RegistrarEventImpl.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/registrar/RegistrarEventImpl.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6d530c52aaf0dc2cdfe3bd56af557274a7f44256 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/registrar/RegistrarEventImpl.java -@@ -0,0 +1,70 @@ -+package io.papermc.paper.plugin.lifecycle.event.registrar; -+ -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; -+import io.papermc.paper.plugin.lifecycle.event.PaperLifecycleEvent; -+import io.papermc.paper.plugin.lifecycle.event.types.OwnerAwareLifecycleEvent; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.checker.nullness.qual.Nullable; -+import org.checkerframework.framework.qual.DefaultQualifier; -+ -+@DefaultQualifier(NonNull.class) -+public class RegistrarEventImpl, O extends LifecycleEventOwner> implements PaperLifecycleEvent, OwnerAwareLifecycleEvent, RegistrarEvent { -+ -+ private final R registrar; -+ private final Class ownerClass; -+ -+ public RegistrarEventImpl(final R registrar, final Class ownerClass) { -+ this.registrar = registrar; -+ this.ownerClass = ownerClass; -+ } -+ -+ @Override -+ public R registrar() { -+ return this.registrar; -+ } -+ -+ @Override -+ public final void setOwner(final @Nullable O owner) { -+ this.registrar.setCurrentContext(owner); -+ } -+ -+ @Override -+ public final @Nullable O castOwner(final LifecycleEventOwner owner) { -+ return this.ownerClass.isInstance(owner) ? this.ownerClass.cast(owner) : null; -+ } -+ -+ @Override -+ public void invalidate() { -+ this.registrar.invalidate(); -+ } -+ -+ @Override -+ public String toString() { -+ return "RegistrarEventImpl{" + -+ "registrar=" + this.registrar + -+ ", ownerClass=" + this.ownerClass + -+ '}'; -+ } -+ -+ public static class ReloadableImpl, O extends LifecycleEventOwner> extends RegistrarEventImpl implements ReloadableRegistrarEvent { -+ -+ private final ReloadableRegistrarEvent.Cause cause; -+ -+ public ReloadableImpl(final R registrar, final Class ownerClass, final Cause cause) { -+ super(registrar, ownerClass); -+ this.cause = cause; -+ } -+ -+ @Override -+ public Cause cause() { -+ return this.cause; -+ } -+ -+ @Override -+ public String toString() { -+ return "ReloadableImpl{" + -+ "cause=" + this.cause + -+ "} " + super.toString(); -+ } -+ } -+} -diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/AbstractLifecycleEventType.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/AbstractLifecycleEventType.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a65fb37f4a729e2fe9fb81af822db626ec7e6d7b ---- /dev/null -+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/AbstractLifecycleEventType.java -@@ -0,0 +1,50 @@ -+package io.papermc.paper.plugin.lifecycle.event.types; -+ -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner; -+import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler; -+import io.papermc.paper.plugin.lifecycle.event.handler.configuration.AbstractLifecycleEventHandlerConfiguration; -+import io.papermc.paper.plugin.lifecycle.event.handler.configuration.LifecycleEventHandlerConfiguration; -+import java.util.function.Consumer; -+import java.util.function.Predicate; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.framework.qual.DefaultQualifier; -+ -+@DefaultQualifier(NonNull.class) -+public abstract class AbstractLifecycleEventType, CI extends AbstractLifecycleEventHandlerConfiguration> implements LifecycleEventType { -+ -+ private final String name; -+ private final Class ownerType; -+ -+ protected AbstractLifecycleEventType(final String name, final Class ownerType) { -+ this.name = name; -+ this.ownerType = ownerType; -+ } -+ -+ @Override -+ public String name() { -+ return this.name; -+ } -+ -+ private void verifyOwner(final O owner) { -+ if (!this.ownerType.isInstance(owner)) { -+ throw new IllegalArgumentException("You cannot register the lifecycle event '" + this.name + "' on " + owner); -+ } -+ } -+ -+ public abstract void forEachHandler(Consumer> consumer, Predicate> predicate); -+ -+ public abstract void removeMatching(Predicate> predicate); -+ -+ protected abstract void register(O owner, LifecycleEventHandler handler, CI config); -+ -+ public final void tryRegister(final O owner, final LifecycleEventHandler handler, final CI config) { -+ this.verifyOwner(owner); -+ LifecycleEventRunner.INSTANCE.checkRegisteredHandler(owner, this); -+ this.register(owner, handler, config); -+ } -+ -+ public record RegisteredHandler(O owner, LifecycleEventHandler lifecycleEventHandler) { -+ } -+} -diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/LifecycleEventTypeProviderImpl.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/LifecycleEventTypeProviderImpl.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0886edad92b40276f268bd745b31bac359fd28af ---- /dev/null -+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/LifecycleEventTypeProviderImpl.java -@@ -0,0 +1,25 @@ -+package io.papermc.paper.plugin.lifecycle.event.types; -+ -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.framework.qual.DefaultQualifier; -+ -+@DefaultQualifier(NonNull.class) -+public final class LifecycleEventTypeProviderImpl implements LifecycleEventTypeProvider { -+ -+ public static LifecycleEventTypeProviderImpl instance() { -+ return (LifecycleEventTypeProviderImpl) LifecycleEventTypeProvider.PROVIDER; -+ } -+ -+ @Override -+ public LifecycleEventType.Monitorable monitor(final String name, final Class ownerType) { -+ return LifecycleEventRunner.INSTANCE.addEventType(new MonitorableLifecycleEventType<>(name, ownerType)); -+ } -+ -+ @Override -+ public LifecycleEventType.Prioritizable prioritized(final String name, final Class ownerType) { -+ return LifecycleEventRunner.INSTANCE.addEventType(new PrioritizableLifecycleEventType<>(name, ownerType)); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/MonitorableLifecycleEventType.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/MonitorableLifecycleEventType.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6d92c1d3adf220154dfe7cba3a3f8158356c3e3c ---- /dev/null -+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/MonitorableLifecycleEventType.java -@@ -0,0 +1,54 @@ -+package io.papermc.paper.plugin.lifecycle.event.types; -+ -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; -+import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler; -+import io.papermc.paper.plugin.lifecycle.event.handler.configuration.MonitorLifecycleEventHandlerConfiguration; -+import io.papermc.paper.plugin.lifecycle.event.handler.configuration.MonitorLifecycleEventHandlerConfigurationImpl; -+import java.util.ArrayList; -+import java.util.List; -+import java.util.function.Consumer; -+import java.util.function.Predicate; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.framework.qual.DefaultQualifier; -+ -+@DefaultQualifier(NonNull.class) -+public class MonitorableLifecycleEventType extends AbstractLifecycleEventType, MonitorLifecycleEventHandlerConfigurationImpl> implements LifecycleEventType.Monitorable { -+ -+ final List> handlers = new ArrayList<>(); -+ int nonMonitorIdx = 0; -+ -+ public MonitorableLifecycleEventType(final String name, final Class ownerType) { -+ super(name, ownerType); -+ } -+ -+ @Override -+ public MonitorLifecycleEventHandlerConfigurationImpl newHandler(final LifecycleEventHandler handler) { -+ return new MonitorLifecycleEventHandlerConfigurationImpl<>(handler, this); -+ } -+ -+ @Override -+ protected void register(final O owner, final LifecycleEventHandler handler, final MonitorLifecycleEventHandlerConfigurationImpl config) { -+ final RegisteredHandler registeredHandler = new RegisteredHandler<>(owner, handler); -+ if (!config.isMonitor()) { -+ this.handlers.add(this.nonMonitorIdx, registeredHandler); -+ this.nonMonitorIdx++; -+ } else { -+ this.handlers.add(registeredHandler); -+ } -+ } -+ -+ @Override -+ public void forEachHandler(final Consumer> consumer, final Predicate> predicate) { -+ for (final RegisteredHandler handler : this.handlers) { -+ if (predicate.test(handler)) { -+ consumer.accept(handler); -+ } -+ } -+ } -+ -+ @Override -+ public void removeMatching(final Predicate> predicate) { -+ this.handlers.removeIf(predicate); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/OwnerAwareLifecycleEvent.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/OwnerAwareLifecycleEvent.java -new file mode 100644 -index 0000000000000000000000000000000000000000..3e7e7474f301c0725fa2bcd6e19e476fc35f2d5a ---- /dev/null -+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/OwnerAwareLifecycleEvent.java -@@ -0,0 +1,15 @@ -+package io.papermc.paper.plugin.lifecycle.event.types; -+ -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.checker.nullness.qual.Nullable; -+import org.checkerframework.framework.qual.DefaultQualifier; -+ -+@DefaultQualifier(NonNull.class) -+public interface OwnerAwareLifecycleEvent extends LifecycleEvent { -+ -+ void setOwner(@Nullable O owner); -+ -+ @Nullable O castOwner(LifecycleEventOwner owner); -+} -diff --git a/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/PrioritizableLifecycleEventType.java b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/PrioritizableLifecycleEventType.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6629f7fabf66ce761024268043cc30076ba8a3f1 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/PrioritizableLifecycleEventType.java -@@ -0,0 +1,64 @@ -+package io.papermc.paper.plugin.lifecycle.event.types; -+ -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent; -+import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; -+import io.papermc.paper.plugin.lifecycle.event.handler.LifecycleEventHandler; -+import io.papermc.paper.plugin.lifecycle.event.handler.configuration.PrioritizedLifecycleEventHandlerConfiguration; -+import io.papermc.paper.plugin.lifecycle.event.handler.configuration.PrioritizedLifecycleEventHandlerConfigurationImpl; -+import java.util.ArrayList; -+import java.util.Comparator; -+import java.util.List; -+import java.util.OptionalInt; -+import java.util.function.Consumer; -+import java.util.function.Predicate; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.framework.qual.DefaultQualifier; -+ -+@DefaultQualifier(NonNull.class) -+public class PrioritizableLifecycleEventType extends AbstractLifecycleEventType, PrioritizedLifecycleEventHandlerConfigurationImpl> implements LifecycleEventType.Prioritizable { -+ -+ private static final Comparator> COMPARATOR = Comparator.comparing(PrioritizedHandler::priority, (o1, o2) -> { -+ if (o1.equals(o2)) { -+ return 0; -+ } else if (o1.isEmpty()) { -+ return 1; -+ } else if (o2.isEmpty()) { -+ return -1; -+ } else { -+ return Integer.compare(o1.getAsInt(), o2.getAsInt()); -+ } -+ }); -+ -+ private final List> handlers = new ArrayList<>(); -+ -+ public PrioritizableLifecycleEventType(final String name, final Class ownerType) { -+ super(name, ownerType); -+ } -+ -+ @Override -+ public PrioritizedLifecycleEventHandlerConfiguration newHandler(final LifecycleEventHandler handler) { -+ return new PrioritizedLifecycleEventHandlerConfigurationImpl<>(handler, this); -+ } -+ -+ @Override -+ protected void register(final O owner, final LifecycleEventHandler handler, final PrioritizedLifecycleEventHandlerConfigurationImpl config) { -+ this.handlers.add(new PrioritizedHandler<>(new RegisteredHandler<>(owner, handler), config.priority())); -+ this.handlers.sort(COMPARATOR); -+ } -+ -+ @Override -+ public void forEachHandler(final Consumer> consumer, final Predicate> predicate) { -+ for (final PrioritizedHandler handler : this.handlers) { -+ if (predicate.test(handler.handler())) { -+ consumer.accept(handler.handler()); -+ } -+ } -+ } -+ -+ @Override -+ public void removeMatching(final Predicate> predicate) { -+ this.handlers.removeIf(prioritizedHandler -> predicate.test(prioritizedHandler.handler())); -+ } -+ -+ private record PrioritizedHandler(RegisteredHandler handler, OptionalInt priority) {} -+} -diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java -index 834b85f24df023642f8abf7213fe578ac8c17a3e..3e82ea07ca4194844c5528446e2c4a46ff4acee5 100644 ---- a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java -+++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java -@@ -293,6 +293,15 @@ class PaperPluginInstanceManager { - + pluginName + " (Is it up to date?)", ex, plugin); // Paper - } - -+ // Paper start - lifecycle event system -+ try { -+ io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner.INSTANCE.unregisterAllEventHandlersFor(plugin); -+ } catch (Throwable ex) { -+ this.handlePluginException("Error occurred (in the plugin loader) while unregistering lifecycle event handlers for " -+ + pluginName + " (Is it up to date?)", ex, plugin); -+ } -+ // Paper end -+ - try { - this.server.getMessenger().unregisterIncomingPluginChannel(plugin); - this.server.getMessenger().unregisterOutgoingPluginChannel(plugin); -diff --git a/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java -index 2e96308696e131f3f013469a395e5ddda2c5d529..65a66e484c1c39c5f41d97db52f31c67b4479d20 100644 ---- a/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java -+++ b/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java -@@ -32,8 +32,9 @@ public class BootstrapProviderStorage extends SimpleProviderStorage provider, PluginBootstrap provided) { - try { -- BootstrapContext context = PluginBootstrapContextImpl.create(provider, PluginInitializerManager.instance().pluginDirectoryPath()); -+ PluginBootstrapContextImpl context = PluginBootstrapContextImpl.create(provider, PluginInitializerManager.instance().pluginDirectoryPath()); // Paper - lifecycle events - provided.bootstrap(context); -+ context.lockLifecycleEventRegistration(); // Paper - lifecycle events - return true; - } catch (Throwable e) { - LOGGER.error("Failed to run bootstrapper for %s. This plugin will not be loaded.".formatted(provider.getSource()), e); -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index 1ba71b0cb6199ee4c3e2a04cfcd57cd3f4908bca..cacbb73e07ec2ea55d10af20c5f2a6d234c9bff3 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -@@ -1018,6 +1018,11 @@ public final class CraftServer implements Server { - - @Override - public void reload() { -+ // Paper start - lifecycle events -+ if (io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner.INSTANCE.blocksPluginReloading()) { -+ throw new IllegalStateException("A lifecycle event handler has been registered which makes reloading plugins not possible"); -+ } -+ // Paper end - lifecycle events - org.spigotmc.WatchdogThread.hasStarted = false; // Paper - Disable watchdog early timeout on reload - this.reloadCount++; - this.configuration = YamlConfiguration.loadConfiguration(this.getConfigFile()); -diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java -index d96399e9bf1a58db5a4a22e58abb99e7660e0694..66bdac50130f523f9dc4379b103b7a469f9ca36b 100644 ---- a/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java -+++ b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java -@@ -143,4 +143,11 @@ public class MinecraftInternalPlugin extends PluginBase { - public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { - throw new UnsupportedOperationException("Not supported."); - } -+ -+ // Paper start - lifecycle events -+ @Override -+ public @NotNull io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager getLifecycleManager() { -+ throw new UnsupportedOperationException("Not supported."); -+ } -+ // Paper end - lifecycle events - } -diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -index 4213e5debc1737364de0dae42fbd1cb68dcb0c83..92d7e2435ea5c8b6fe9e5d2abb9d3c8cb350ef4d 100644 ---- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -+++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -@@ -668,6 +668,13 @@ public final class CraftMagicNumbers implements UnsafeValues { - } - // Paper end - spawn egg color visibility - -+ // Paper start - lifecycle event API -+ @Override -+ public io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager createPluginLifecycleEventManager(final org.bukkit.plugin.java.JavaPlugin plugin, final java.util.function.BooleanSupplier registrationCheck) { -+ return new io.papermc.paper.plugin.lifecycle.event.PaperLifecycleEventManager<>(plugin, registrationCheck); -+ } -+ // Paper end - lifecycle event API -+ - /** - * This helper class represents the different NBT Tags. - *

-diff --git a/src/main/resources/META-INF/services/io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventTypeProvider b/src/main/resources/META-INF/services/io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventTypeProvider -new file mode 100644 -index 0000000000000000000000000000000000000000..808b1192b60348ad05f0bfbdeda6f94df4876743 ---- /dev/null -+++ b/src/main/resources/META-INF/services/io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventTypeProvider -@@ -0,0 +1 @@ -+io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventTypeProviderImpl -diff --git a/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java b/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java -index 1d14f530ef888102e47eeeaf0d1a6076e51871c4..90cf0c702ca2ff9de64d9718ecba5f2d128953a6 100644 ---- a/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java -+++ b/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java -@@ -143,4 +143,11 @@ public class PaperTestPlugin extends PluginBase { - public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { - throw new UnsupportedOperationException("Not supported."); - } -+ -+ // Paper start - lifecycle events -+ @Override -+ public io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager getLifecycleManager() { -+ throw new UnsupportedOperationException("Not supported."); -+ } -+ // Paper end - lifecycle events - } diff --git a/patches/server/0958-ItemStack-Tooltip-API.patch b/patches/server/0958-ItemStack-Tooltip-API.patch new file mode 100644 index 0000000000..477d02c3fa --- /dev/null +++ b/patches/server/0958-ItemStack-Tooltip-API.patch @@ -0,0 +1,31 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Yannick Lamprecht +Date: Mon, 22 Jan 2024 13:27:30 +0100 +Subject: [PATCH] ItemStack Tooltip API + + +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +index 92d7e2435ea5c8b6fe9e5d2abb9d3c8cb350ef4d..2f44452631256e3d6abce02f861cc5bfa5208b53 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +@@ -658,6 +658,20 @@ public final class CraftMagicNumbers implements UnsafeValues { + return org.bukkit.craftbukkit.CraftStatistic.getNMSStatistic(statistic).getName(); + } + // Paper end ++ // Paper start - expose itemstack tooltip lines ++ @Override ++ public java.util.List computeTooltipLines(final ItemStack itemStack, final io.papermc.paper.inventory.tooltip.TooltipContext tooltipContext, final org.bukkit.entity.Player player) { ++ Preconditions.checkArgument(tooltipContext != null, "tooltipContext cannot be null"); ++ net.minecraft.world.item.TooltipFlag.Default flag = tooltipContext.isAdvanced() ? net.minecraft.world.item.TooltipFlag.ADVANCED : net.minecraft.world.item.TooltipFlag.NORMAL; ++ if (tooltipContext.isCreative()) { ++ flag = flag.asCreative(); ++ } ++ final java.util.List lines = CraftItemStack.asNMSCopy(itemStack).getTooltipLines( ++ net.minecraft.world.item.Item.TooltipContext.of(player == null ? net.minecraft.server.MinecraftServer.getServer().registryAccess() : ((org.bukkit.craftbukkit.entity.CraftPlayer) player).getHandle().level().registryAccess()), ++ player == null ? null : ((org.bukkit.craftbukkit.entity.CraftPlayer) player).getHandle(), flag); ++ return lines.stream().map(io.papermc.paper.adventure.PaperAdventure::asAdventure).toList(); ++ } ++ // Paper end - expose itemstack tooltip lines + + // Paper start - spawn egg color visibility + @Override diff --git a/patches/server/0959-Add-getChunkSnapshot-includeLightData-parameter.patch b/patches/server/0959-Add-getChunkSnapshot-includeLightData-parameter.patch new file mode 100644 index 0000000000..70efa7282a --- /dev/null +++ b/patches/server/0959-Add-getChunkSnapshot-includeLightData-parameter.patch @@ -0,0 +1,70 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Warrior <50800980+Warriorrrr@users.noreply.github.com> +Date: Sat, 10 Feb 2024 10:03:48 +0100 +Subject: [PATCH] Add getChunkSnapshot includeLightData parameter + + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +index 5b59b19a6d913ebdfc28a755e3a1a8b8384a3116..7dae8d91b74cc7df0745f0c121e3bea09b8d0b6d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +@@ -327,12 +327,21 @@ public class CraftChunk implements Chunk { + + @Override + public ChunkSnapshot getChunkSnapshot(boolean includeMaxBlockY, boolean includeBiome, boolean includeBiomeTempRain) { ++ // Paper start - Add getChunkSnapshot includeLightData parameter ++ return getChunkSnapshot(includeMaxBlockY, includeBiome, includeBiomeTempRain, true); ++ } ++ ++ @Override ++ public ChunkSnapshot getChunkSnapshot(boolean includeMaxBlockY, boolean includeBiome, boolean includeBiomeTempRain, boolean includeLightData) { ++ // Paper end - Add getChunkSnapshot includeLightData parameter + ChunkAccess chunk = this.getHandle(ChunkStatus.FULL); + + LevelChunkSection[] cs = chunk.getSections(); + PalettedContainer[] sectionBlockIDs = new PalettedContainer[cs.length]; +- byte[][] sectionSkyLights = new byte[cs.length][]; +- byte[][] sectionEmitLights = new byte[cs.length][]; ++ // Paper start - Add getChunkSnapshot includeLightData parameter ++ byte[][] sectionSkyLights = includeLightData ? new byte[cs.length][] : null; ++ byte[][] sectionEmitLights = includeLightData ? new byte[cs.length][] : null; ++ // Paper end - Add getChunkSnapshot includeLightData parameter + boolean[] sectionEmpty = new boolean[cs.length]; + PalettedContainerRO>[] biome = (includeBiome || includeBiomeTempRain) ? new PalettedContainer[cs.length] : null; + +@@ -349,6 +358,7 @@ public class CraftChunk implements Chunk { + } + // Paper end - Fix ChunkSnapshot#isSectionEmpty(int) + ++ if (includeLightData) { // Paper - Add getChunkSnapshot includeLightData parameter + LevelLightEngine lightengine = this.worldServer.getLightEngine(); + DataLayer skyLightArray = lightengine.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(this.x, chunk.getSectionYFromSectionIndex(i), this.z)); // SPIGOT-7498: Convert section index + if (skyLightArray == null) { +@@ -364,6 +374,7 @@ public class CraftChunk implements Chunk { + sectionEmitLights[i] = new byte[2048]; + System.arraycopy(emitLightArray.getData(), 0, sectionEmitLights[i], 0, 2048); + } ++ } // Paper - Add getChunkSnapshot includeLightData parameter + + if (biome != null) { + biome[i] = ((PalettedContainer>) cs[i].getBiomes()).copy(); // Paper - Perf: use copy instead of round tripping with codecs +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunkSnapshot.java b/src/main/java/org/bukkit/craftbukkit/CraftChunkSnapshot.java +index 85029f1acfdbb411d9ebdf95838d6db3898f4e58..0756b5adb3039997feadeb94afb10b596abd9424 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftChunkSnapshot.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftChunkSnapshot.java +@@ -118,6 +118,7 @@ public class CraftChunkSnapshot implements ChunkSnapshot { + + @Override + public final int getBlockSkyLight(int x, int y, int z) { ++ Preconditions.checkState(this.skylight != null, "ChunkSnapshot created without light data. Please call getSnapshot with includeLightData=true"); // Paper - Add getChunkSnapshot includeLightData parameter + this.validateChunkCoordinates(x, y, z); + + int off = ((y & 0xF) << 7) | (z << 3) | (x >> 1); +@@ -126,6 +127,7 @@ public class CraftChunkSnapshot implements ChunkSnapshot { + + @Override + public final int getBlockEmittedLight(int x, int y, int z) { ++ Preconditions.checkState(this.emitlight != null, "ChunkSnapshot created without light data. Please call getSnapshot with includeLightData=true"); // Paper - Add getChunkSnapshot includeLightData parameter + this.validateChunkCoordinates(x, y, z); + + int off = ((y & 0xF) << 7) | (z << 3) | (x >> 1); diff --git a/patches/server/0959-ItemStack-Tooltip-API.patch b/patches/server/0959-ItemStack-Tooltip-API.patch deleted file mode 100644 index 477d02c3fa..0000000000 --- a/patches/server/0959-ItemStack-Tooltip-API.patch +++ /dev/null @@ -1,31 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Yannick Lamprecht -Date: Mon, 22 Jan 2024 13:27:30 +0100 -Subject: [PATCH] ItemStack Tooltip API - - -diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -index 92d7e2435ea5c8b6fe9e5d2abb9d3c8cb350ef4d..2f44452631256e3d6abce02f861cc5bfa5208b53 100644 ---- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -+++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -@@ -658,6 +658,20 @@ public final class CraftMagicNumbers implements UnsafeValues { - return org.bukkit.craftbukkit.CraftStatistic.getNMSStatistic(statistic).getName(); - } - // Paper end -+ // Paper start - expose itemstack tooltip lines -+ @Override -+ public java.util.List computeTooltipLines(final ItemStack itemStack, final io.papermc.paper.inventory.tooltip.TooltipContext tooltipContext, final org.bukkit.entity.Player player) { -+ Preconditions.checkArgument(tooltipContext != null, "tooltipContext cannot be null"); -+ net.minecraft.world.item.TooltipFlag.Default flag = tooltipContext.isAdvanced() ? net.minecraft.world.item.TooltipFlag.ADVANCED : net.minecraft.world.item.TooltipFlag.NORMAL; -+ if (tooltipContext.isCreative()) { -+ flag = flag.asCreative(); -+ } -+ final java.util.List lines = CraftItemStack.asNMSCopy(itemStack).getTooltipLines( -+ net.minecraft.world.item.Item.TooltipContext.of(player == null ? net.minecraft.server.MinecraftServer.getServer().registryAccess() : ((org.bukkit.craftbukkit.entity.CraftPlayer) player).getHandle().level().registryAccess()), -+ player == null ? null : ((org.bukkit.craftbukkit.entity.CraftPlayer) player).getHandle(), flag); -+ return lines.stream().map(io.papermc.paper.adventure.PaperAdventure::asAdventure).toList(); -+ } -+ // Paper end - expose itemstack tooltip lines - - // Paper start - spawn egg color visibility - @Override diff --git a/patches/server/0960-Add-FluidState-API.patch b/patches/server/0960-Add-FluidState-API.patch new file mode 100644 index 0000000000..de7b959ccc --- /dev/null +++ b/patches/server/0960-Add-FluidState-API.patch @@ -0,0 +1,208 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: vicisacat +Date: Fri, 17 Nov 2023 20:22:43 +0100 +Subject: [PATCH] Add FluidState API + + +diff --git a/src/main/java/io/papermc/paper/block/fluid/PaperFluidData.java b/src/main/java/io/papermc/paper/block/fluid/PaperFluidData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..479bc32241ebadf8bbc1080b601f61391ad37fa4 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/fluid/PaperFluidData.java +@@ -0,0 +1,110 @@ ++package io.papermc.paper.block.fluid; ++ ++import com.google.common.base.Preconditions; ++import io.papermc.paper.block.fluid.type.PaperFallingFluidData; ++import io.papermc.paper.block.fluid.type.PaperFlowingFluidData; ++import io.papermc.paper.util.MCUtil; ++import java.util.HashMap; ++import java.util.Map; ++import java.util.function.Function; ++import net.minecraft.world.level.material.FluidState; ++import net.minecraft.world.level.material.LavaFluid; ++import net.minecraft.world.level.material.WaterFluid; ++import org.bukkit.Fluid; ++import org.bukkit.Location; ++import org.bukkit.craftbukkit.CraftFluid; ++import org.bukkit.craftbukkit.CraftWorld; ++import org.bukkit.craftbukkit.util.CraftVector; ++import org.bukkit.util.Vector; ++import org.jetbrains.annotations.NotNull; ++ ++public class PaperFluidData implements FluidData { ++ ++ private final FluidState state; ++ ++ protected PaperFluidData(final FluidState state) { ++ this.state = state; ++ } ++ ++ /** ++ * Provides the internal server representation of this fluid data. ++ * @return the fluid state. ++ */ ++ public FluidState getState() { ++ return this.state; ++ } ++ ++ @Override ++ public final @NotNull Fluid getFluidType() { ++ return CraftFluid.minecraftToBukkit(this.state.getType()); ++ } ++ ++ @Override ++ public @NotNull PaperFluidData clone() { ++ try { ++ return (PaperFluidData) super.clone(); ++ } catch (final CloneNotSupportedException ex) { ++ throw new AssertionError("Clone not supported", ex); ++ } ++ } ++ ++ @Override ++ public @NotNull Vector computeFlowDirection(final Location location) { ++ Preconditions.checkArgument(location.getWorld() != null, "Cannot compute flow direction on world-less location"); ++ return CraftVector.toBukkit(this.state.getFlow( ++ ((CraftWorld) location.getWorld()).getHandle(), ++ MCUtil.toBlockPosition(location) ++ )); ++ } ++ ++ @Override ++ public int getLevel() { ++ return this.state.getAmount(); ++ } ++ ++ @Override ++ public float computeHeight(@NotNull final Location location) { ++ Preconditions.checkArgument(location.getWorld() != null, "Cannot compute height on world-less location"); ++ return this.state.getHeight(((CraftWorld) location.getWorld()).getHandle(), MCUtil.toBlockPos(location)); ++ } ++ ++ @Override ++ public boolean isSource() { ++ return this.state.isSource(); ++ } ++ ++ @Override ++ public int hashCode() { ++ return this.state.hashCode(); ++ } ++ ++ @Override ++ public boolean equals(final Object obj) { ++ return obj instanceof final PaperFluidData paperFluidData && this.state.equals(paperFluidData.state); ++ } ++ ++ @Override ++ public String toString() { ++ return "PaperFluidData{" + this.state + "}"; ++ } ++ ++ /* Registry */ ++ private static final Map, Function> MAP = new HashMap<>(); ++ static { ++ // ++ register(LavaFluid.Source.class, PaperFallingFluidData::new); ++ register(WaterFluid.Source.class, PaperFallingFluidData::new); ++ register(LavaFluid.Flowing.class, PaperFlowingFluidData::new); ++ register(WaterFluid.Flowing.class, PaperFlowingFluidData::new); ++ // ++ } ++ ++ static void register(final Class fluid, final Function creator) { ++ Preconditions.checkState(MAP.put(fluid, creator) == null, "Duplicate mapping %s->%s", fluid, creator); ++ MAP.put(fluid, creator); ++ } ++ ++ public static PaperFluidData createData(final FluidState state) { ++ return MAP.getOrDefault(state.getType().getClass(), PaperFluidData::new).apply(state); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/block/fluid/package-info.java b/src/main/java/io/papermc/paper/block/fluid/package-info.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cfabb814ebd281aab299c6c655266ff357e08806 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/fluid/package-info.java +@@ -0,0 +1,5 @@ ++@DefaultQualifier(NonNull.class) ++package io.papermc.paper.block.fluid; ++ ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; +diff --git a/src/main/java/io/papermc/paper/block/fluid/type/PaperFallingFluidData.java b/src/main/java/io/papermc/paper/block/fluid/type/PaperFallingFluidData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..655dbd83ff4e632f1168b75e9b402b05aa9d8edf +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/fluid/type/PaperFallingFluidData.java +@@ -0,0 +1,18 @@ ++ ++package io.papermc.paper.block.fluid.type; ++ ++import io.papermc.paper.block.fluid.PaperFluidData; ++import net.minecraft.world.level.material.FlowingFluid; ++import net.minecraft.world.level.material.FluidState; ++ ++public class PaperFallingFluidData extends PaperFluidData implements FallingFluidData { ++ ++ public PaperFallingFluidData(final FluidState state) { ++ super(state); ++ } ++ ++ @Override ++ public boolean isFalling() { ++ return this.getState().getValue(FlowingFluid.FALLING); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/block/fluid/type/PaperFlowingFluidData.java b/src/main/java/io/papermc/paper/block/fluid/type/PaperFlowingFluidData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c0c2805cb045cdd835b402776a6923fe2ecc2a99 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/block/fluid/type/PaperFlowingFluidData.java +@@ -0,0 +1,11 @@ ++package io.papermc.paper.block.fluid.type; ++ ++import net.minecraft.world.level.material.FluidState; ++ ++public class PaperFlowingFluidData extends PaperFallingFluidData implements FlowingFluidData { ++ ++ public PaperFlowingFluidData(final FluidState state) { ++ super(state); ++ } ++ ++} +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java b/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java +index 0e307c2bb788c1ec856613b0203f5fc7aca7e85d..1d16cd01e55f3e5bb8f49ad4c9f777d30180aab5 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java +@@ -107,6 +107,13 @@ public abstract class CraftRegionAccessor implements RegionAccessor { + return CraftBlock.at(this.getHandle(), new BlockPos(x, y, z)).getState(); + } + ++ // Paper start - FluidState API ++ @Override ++ public io.papermc.paper.block.fluid.FluidData getFluidData(final int x, final int y, final int z) { ++ return io.papermc.paper.block.fluid.PaperFluidData.createData(getHandle().getFluidState(new BlockPos(x, y, z))); ++ } ++ // Paper end ++ + @Override + public BlockData getBlockData(Location location) { + return this.getBlockData(location.getBlockX(), location.getBlockY(), location.getBlockZ()); +diff --git a/src/main/java/org/bukkit/craftbukkit/generator/CraftLimitedRegion.java b/src/main/java/org/bukkit/craftbukkit/generator/CraftLimitedRegion.java +index eaa9ba70b0b80d86eb376a0641420093a7c9dfdb..25598df0bb0d4347b2c17b6ec0afbfe4ecf808b9 100644 +--- a/src/main/java/org/bukkit/craftbukkit/generator/CraftLimitedRegion.java ++++ b/src/main/java/org/bukkit/craftbukkit/generator/CraftLimitedRegion.java +@@ -303,4 +303,11 @@ public class CraftLimitedRegion extends CraftRegionAccessor implements LimitedRe + return centerChunkZ; + } + // Paper end - Add more LimitedRegion API ++ // Paper start - Fluid API ++ @Override ++ public io.papermc.paper.block.fluid.FluidData getFluidData(int x, int y, int z) { ++ Preconditions.checkArgument(this.isInRegion(x, y, z), "Coordinates %s, %s, %s are not in the region", x, y, z); ++ return super.getFluidData(x, y, z); ++ } ++ // Paper end + } diff --git a/patches/server/0960-Add-getChunkSnapshot-includeLightData-parameter.patch b/patches/server/0960-Add-getChunkSnapshot-includeLightData-parameter.patch deleted file mode 100644 index 70efa7282a..0000000000 --- a/patches/server/0960-Add-getChunkSnapshot-includeLightData-parameter.patch +++ /dev/null @@ -1,70 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Warrior <50800980+Warriorrrr@users.noreply.github.com> -Date: Sat, 10 Feb 2024 10:03:48 +0100 -Subject: [PATCH] Add getChunkSnapshot includeLightData parameter - - -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java -index 5b59b19a6d913ebdfc28a755e3a1a8b8384a3116..7dae8d91b74cc7df0745f0c121e3bea09b8d0b6d 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java -@@ -327,12 +327,21 @@ public class CraftChunk implements Chunk { - - @Override - public ChunkSnapshot getChunkSnapshot(boolean includeMaxBlockY, boolean includeBiome, boolean includeBiomeTempRain) { -+ // Paper start - Add getChunkSnapshot includeLightData parameter -+ return getChunkSnapshot(includeMaxBlockY, includeBiome, includeBiomeTempRain, true); -+ } -+ -+ @Override -+ public ChunkSnapshot getChunkSnapshot(boolean includeMaxBlockY, boolean includeBiome, boolean includeBiomeTempRain, boolean includeLightData) { -+ // Paper end - Add getChunkSnapshot includeLightData parameter - ChunkAccess chunk = this.getHandle(ChunkStatus.FULL); - - LevelChunkSection[] cs = chunk.getSections(); - PalettedContainer[] sectionBlockIDs = new PalettedContainer[cs.length]; -- byte[][] sectionSkyLights = new byte[cs.length][]; -- byte[][] sectionEmitLights = new byte[cs.length][]; -+ // Paper start - Add getChunkSnapshot includeLightData parameter -+ byte[][] sectionSkyLights = includeLightData ? new byte[cs.length][] : null; -+ byte[][] sectionEmitLights = includeLightData ? new byte[cs.length][] : null; -+ // Paper end - Add getChunkSnapshot includeLightData parameter - boolean[] sectionEmpty = new boolean[cs.length]; - PalettedContainerRO>[] biome = (includeBiome || includeBiomeTempRain) ? new PalettedContainer[cs.length] : null; - -@@ -349,6 +358,7 @@ public class CraftChunk implements Chunk { - } - // Paper end - Fix ChunkSnapshot#isSectionEmpty(int) - -+ if (includeLightData) { // Paper - Add getChunkSnapshot includeLightData parameter - LevelLightEngine lightengine = this.worldServer.getLightEngine(); - DataLayer skyLightArray = lightengine.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(this.x, chunk.getSectionYFromSectionIndex(i), this.z)); // SPIGOT-7498: Convert section index - if (skyLightArray == null) { -@@ -364,6 +374,7 @@ public class CraftChunk implements Chunk { - sectionEmitLights[i] = new byte[2048]; - System.arraycopy(emitLightArray.getData(), 0, sectionEmitLights[i], 0, 2048); - } -+ } // Paper - Add getChunkSnapshot includeLightData parameter - - if (biome != null) { - biome[i] = ((PalettedContainer>) cs[i].getBiomes()).copy(); // Paper - Perf: use copy instead of round tripping with codecs -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunkSnapshot.java b/src/main/java/org/bukkit/craftbukkit/CraftChunkSnapshot.java -index 85029f1acfdbb411d9ebdf95838d6db3898f4e58..0756b5adb3039997feadeb94afb10b596abd9424 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftChunkSnapshot.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftChunkSnapshot.java -@@ -118,6 +118,7 @@ public class CraftChunkSnapshot implements ChunkSnapshot { - - @Override - public final int getBlockSkyLight(int x, int y, int z) { -+ Preconditions.checkState(this.skylight != null, "ChunkSnapshot created without light data. Please call getSnapshot with includeLightData=true"); // Paper - Add getChunkSnapshot includeLightData parameter - this.validateChunkCoordinates(x, y, z); - - int off = ((y & 0xF) << 7) | (z << 3) | (x >> 1); -@@ -126,6 +127,7 @@ public class CraftChunkSnapshot implements ChunkSnapshot { - - @Override - public final int getBlockEmittedLight(int x, int y, int z) { -+ Preconditions.checkState(this.emitlight != null, "ChunkSnapshot created without light data. Please call getSnapshot with includeLightData=true"); // Paper - Add getChunkSnapshot includeLightData parameter - this.validateChunkCoordinates(x, y, z); - - int off = ((y & 0xF) << 7) | (z << 3) | (x >> 1); diff --git a/patches/server/0961-Add-FluidState-API.patch b/patches/server/0961-Add-FluidState-API.patch deleted file mode 100644 index de7b959ccc..0000000000 --- a/patches/server/0961-Add-FluidState-API.patch +++ /dev/null @@ -1,208 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: vicisacat -Date: Fri, 17 Nov 2023 20:22:43 +0100 -Subject: [PATCH] Add FluidState API - - -diff --git a/src/main/java/io/papermc/paper/block/fluid/PaperFluidData.java b/src/main/java/io/papermc/paper/block/fluid/PaperFluidData.java -new file mode 100644 -index 0000000000000000000000000000000000000000..479bc32241ebadf8bbc1080b601f61391ad37fa4 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/block/fluid/PaperFluidData.java -@@ -0,0 +1,110 @@ -+package io.papermc.paper.block.fluid; -+ -+import com.google.common.base.Preconditions; -+import io.papermc.paper.block.fluid.type.PaperFallingFluidData; -+import io.papermc.paper.block.fluid.type.PaperFlowingFluidData; -+import io.papermc.paper.util.MCUtil; -+import java.util.HashMap; -+import java.util.Map; -+import java.util.function.Function; -+import net.minecraft.world.level.material.FluidState; -+import net.minecraft.world.level.material.LavaFluid; -+import net.minecraft.world.level.material.WaterFluid; -+import org.bukkit.Fluid; -+import org.bukkit.Location; -+import org.bukkit.craftbukkit.CraftFluid; -+import org.bukkit.craftbukkit.CraftWorld; -+import org.bukkit.craftbukkit.util.CraftVector; -+import org.bukkit.util.Vector; -+import org.jetbrains.annotations.NotNull; -+ -+public class PaperFluidData implements FluidData { -+ -+ private final FluidState state; -+ -+ protected PaperFluidData(final FluidState state) { -+ this.state = state; -+ } -+ -+ /** -+ * Provides the internal server representation of this fluid data. -+ * @return the fluid state. -+ */ -+ public FluidState getState() { -+ return this.state; -+ } -+ -+ @Override -+ public final @NotNull Fluid getFluidType() { -+ return CraftFluid.minecraftToBukkit(this.state.getType()); -+ } -+ -+ @Override -+ public @NotNull PaperFluidData clone() { -+ try { -+ return (PaperFluidData) super.clone(); -+ } catch (final CloneNotSupportedException ex) { -+ throw new AssertionError("Clone not supported", ex); -+ } -+ } -+ -+ @Override -+ public @NotNull Vector computeFlowDirection(final Location location) { -+ Preconditions.checkArgument(location.getWorld() != null, "Cannot compute flow direction on world-less location"); -+ return CraftVector.toBukkit(this.state.getFlow( -+ ((CraftWorld) location.getWorld()).getHandle(), -+ MCUtil.toBlockPosition(location) -+ )); -+ } -+ -+ @Override -+ public int getLevel() { -+ return this.state.getAmount(); -+ } -+ -+ @Override -+ public float computeHeight(@NotNull final Location location) { -+ Preconditions.checkArgument(location.getWorld() != null, "Cannot compute height on world-less location"); -+ return this.state.getHeight(((CraftWorld) location.getWorld()).getHandle(), MCUtil.toBlockPos(location)); -+ } -+ -+ @Override -+ public boolean isSource() { -+ return this.state.isSource(); -+ } -+ -+ @Override -+ public int hashCode() { -+ return this.state.hashCode(); -+ } -+ -+ @Override -+ public boolean equals(final Object obj) { -+ return obj instanceof final PaperFluidData paperFluidData && this.state.equals(paperFluidData.state); -+ } -+ -+ @Override -+ public String toString() { -+ return "PaperFluidData{" + this.state + "}"; -+ } -+ -+ /* Registry */ -+ private static final Map, Function> MAP = new HashMap<>(); -+ static { -+ // -+ register(LavaFluid.Source.class, PaperFallingFluidData::new); -+ register(WaterFluid.Source.class, PaperFallingFluidData::new); -+ register(LavaFluid.Flowing.class, PaperFlowingFluidData::new); -+ register(WaterFluid.Flowing.class, PaperFlowingFluidData::new); -+ // -+ } -+ -+ static void register(final Class fluid, final Function creator) { -+ Preconditions.checkState(MAP.put(fluid, creator) == null, "Duplicate mapping %s->%s", fluid, creator); -+ MAP.put(fluid, creator); -+ } -+ -+ public static PaperFluidData createData(final FluidState state) { -+ return MAP.getOrDefault(state.getType().getClass(), PaperFluidData::new).apply(state); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/block/fluid/package-info.java b/src/main/java/io/papermc/paper/block/fluid/package-info.java -new file mode 100644 -index 0000000000000000000000000000000000000000..cfabb814ebd281aab299c6c655266ff357e08806 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/block/fluid/package-info.java -@@ -0,0 +1,5 @@ -+@DefaultQualifier(NonNull.class) -+package io.papermc.paper.block.fluid; -+ -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.framework.qual.DefaultQualifier; -diff --git a/src/main/java/io/papermc/paper/block/fluid/type/PaperFallingFluidData.java b/src/main/java/io/papermc/paper/block/fluid/type/PaperFallingFluidData.java -new file mode 100644 -index 0000000000000000000000000000000000000000..655dbd83ff4e632f1168b75e9b402b05aa9d8edf ---- /dev/null -+++ b/src/main/java/io/papermc/paper/block/fluid/type/PaperFallingFluidData.java -@@ -0,0 +1,18 @@ -+ -+package io.papermc.paper.block.fluid.type; -+ -+import io.papermc.paper.block.fluid.PaperFluidData; -+import net.minecraft.world.level.material.FlowingFluid; -+import net.minecraft.world.level.material.FluidState; -+ -+public class PaperFallingFluidData extends PaperFluidData implements FallingFluidData { -+ -+ public PaperFallingFluidData(final FluidState state) { -+ super(state); -+ } -+ -+ @Override -+ public boolean isFalling() { -+ return this.getState().getValue(FlowingFluid.FALLING); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/block/fluid/type/PaperFlowingFluidData.java b/src/main/java/io/papermc/paper/block/fluid/type/PaperFlowingFluidData.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c0c2805cb045cdd835b402776a6923fe2ecc2a99 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/block/fluid/type/PaperFlowingFluidData.java -@@ -0,0 +1,11 @@ -+package io.papermc.paper.block.fluid.type; -+ -+import net.minecraft.world.level.material.FluidState; -+ -+public class PaperFlowingFluidData extends PaperFallingFluidData implements FlowingFluidData { -+ -+ public PaperFlowingFluidData(final FluidState state) { -+ super(state); -+ } -+ -+} -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java b/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java -index 0e307c2bb788c1ec856613b0203f5fc7aca7e85d..1d16cd01e55f3e5bb8f49ad4c9f777d30180aab5 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java -@@ -107,6 +107,13 @@ public abstract class CraftRegionAccessor implements RegionAccessor { - return CraftBlock.at(this.getHandle(), new BlockPos(x, y, z)).getState(); - } - -+ // Paper start - FluidState API -+ @Override -+ public io.papermc.paper.block.fluid.FluidData getFluidData(final int x, final int y, final int z) { -+ return io.papermc.paper.block.fluid.PaperFluidData.createData(getHandle().getFluidState(new BlockPos(x, y, z))); -+ } -+ // Paper end -+ - @Override - public BlockData getBlockData(Location location) { - return this.getBlockData(location.getBlockX(), location.getBlockY(), location.getBlockZ()); -diff --git a/src/main/java/org/bukkit/craftbukkit/generator/CraftLimitedRegion.java b/src/main/java/org/bukkit/craftbukkit/generator/CraftLimitedRegion.java -index eaa9ba70b0b80d86eb376a0641420093a7c9dfdb..25598df0bb0d4347b2c17b6ec0afbfe4ecf808b9 100644 ---- a/src/main/java/org/bukkit/craftbukkit/generator/CraftLimitedRegion.java -+++ b/src/main/java/org/bukkit/craftbukkit/generator/CraftLimitedRegion.java -@@ -303,4 +303,11 @@ public class CraftLimitedRegion extends CraftRegionAccessor implements LimitedRe - return centerChunkZ; - } - // Paper end - Add more LimitedRegion API -+ // Paper start - Fluid API -+ @Override -+ public io.papermc.paper.block.fluid.FluidData getFluidData(int x, int y, int z) { -+ Preconditions.checkArgument(this.isInRegion(x, y, z), "Coordinates %s, %s, %s are not in the region", x, y, z); -+ return super.getFluidData(x, y, z); -+ } -+ // Paper end - } diff --git a/patches/server/0961-add-number-format-api.patch b/patches/server/0961-add-number-format-api.patch new file mode 100644 index 0000000000..d412b5f929 --- /dev/null +++ b/patches/server/0961-add-number-format-api.patch @@ -0,0 +1,138 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: David Mayr +Date: Sat, 16 Dec 2023 10:40:29 +0100 +Subject: [PATCH] add number format api + +== AT == +public net.minecraft.network.chat.numbers.FixedFormat value +public net.minecraft.network.chat.numbers.StyledFormat style + +diff --git a/src/main/java/io/papermc/paper/util/PaperScoreboardFormat.java b/src/main/java/io/papermc/paper/util/PaperScoreboardFormat.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6064086cc76ef0df999c7057121d0ac22bd4df65 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/PaperScoreboardFormat.java +@@ -0,0 +1,38 @@ ++package io.papermc.paper.util; ++ ++import io.papermc.paper.adventure.PaperAdventure; ++import io.papermc.paper.scoreboard.numbers.NumberFormat; ++ ++public final class PaperScoreboardFormat { ++ ++ private PaperScoreboardFormat() { ++ } ++ ++ public static net.minecraft.network.chat.numbers.NumberFormat asVanilla(final NumberFormat format) { ++ final net.minecraft.network.chat.numbers.NumberFormat vanilla; ++ if (format instanceof final io.papermc.paper.scoreboard.numbers.StyledFormat styled) { ++ vanilla = new net.minecraft.network.chat.numbers.StyledFormat(PaperAdventure.asVanilla(styled.style())); ++ } else if (format instanceof final io.papermc.paper.scoreboard.numbers.FixedFormat fixed) { ++ vanilla = new net.minecraft.network.chat.numbers.FixedFormat(io.papermc.paper.adventure.PaperAdventure ++ .asVanilla(fixed.component())); ++ } else if (format.equals(NumberFormat.blank())) { ++ vanilla = net.minecraft.network.chat.numbers.BlankFormat.INSTANCE; ++ } else { ++ throw new IllegalArgumentException("Unknown format type " + format.getClass()); ++ } ++ ++ return vanilla; ++ } ++ ++ public static NumberFormat asPaper(final net.minecraft.network.chat.numbers.NumberFormat vanilla) { ++ if (vanilla instanceof final net.minecraft.network.chat.numbers.StyledFormat styled) { ++ return NumberFormat.styled(PaperAdventure.asAdventure(styled.style)); ++ } else if (vanilla instanceof final net.minecraft.network.chat.numbers.FixedFormat fixed) { ++ return NumberFormat.fixed(io.papermc.paper.adventure.PaperAdventure.asAdventure(fixed.value)); ++ } else if (vanilla instanceof net.minecraft.network.chat.numbers.BlankFormat) { ++ return NumberFormat.blank(); ++ } ++ ++ throw new IllegalArgumentException("Unknown format type " + vanilla.getClass()); ++ } ++} +diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java +index 2d3abf2a1da487ead74d698cc5ea4eb729c35c8d..1fec80c4f02aab3770c05bac8bfa2b622625e630 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java ++++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java +@@ -153,6 +153,34 @@ final class CraftObjective extends CraftScoreboardComponent implements Objective + } + // Paper end + ++ // Paper start - add number format ++ @Override ++ public io.papermc.paper.scoreboard.numbers.NumberFormat numberFormat() { ++ this.checkState(); ++ ++ net.minecraft.network.chat.numbers.NumberFormat vanilla = this.objective.numberFormat(); ++ ++ if (vanilla == null) { ++ return null; ++ } ++ ++ return io.papermc.paper.util.PaperScoreboardFormat.asPaper(vanilla); ++ } ++ ++ ++ @Override ++ public void numberFormat(io.papermc.paper.scoreboard.numbers.NumberFormat format) { ++ this.checkState(); ++ ++ if (format == null) { ++ this.objective.setNumberFormat(null); ++ return; ++ } ++ ++ this.objective.setNumberFormat(io.papermc.paper.util.PaperScoreboardFormat.asVanilla(format)); ++ } ++ // Paper end - add number format ++ + @Override + public void unregister() { + CraftScoreboard scoreboard = this.checkState(); +diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScore.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScore.java +index 74d9c407e971804bed420370f7b684d8658eb5aa..e307e897d6e1ba4cb21883dfeaf334bfbf56cfc4 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScore.java ++++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScore.java +@@ -55,6 +55,41 @@ final class CraftScore implements Score { + this.objective.checkState().board.getOrCreatePlayerScore(this.entry, this.objective.getHandle()).set(score); + } + ++ ++ // Paper start - add number format ++ @Override ++ public io.papermc.paper.scoreboard.numbers.NumberFormat numberFormat() { ++ ReadOnlyScoreInfo scoreInfo = this.objective.checkState().board ++ .getPlayerScoreInfo(this.entry, this.objective.getHandle()); ++ ++ if (scoreInfo == null) { ++ return null; ++ } ++ ++ net.minecraft.network.chat.numbers.NumberFormat vanilla = scoreInfo.numberFormat(); ++ ++ if (vanilla == null) { ++ return null; ++ } ++ ++ return io.papermc.paper.util.PaperScoreboardFormat.asPaper(vanilla); ++ } ++ ++ ++ @Override ++ public void numberFormat(io.papermc.paper.scoreboard.numbers.NumberFormat format) { ++ final net.minecraft.world.scores.ScoreAccess access = this.objective.checkState() ++ .board.getOrCreatePlayerScore(this.entry, this.objective.getHandle()); ++ ++ if (format == null) { ++ access.numberFormatOverride(null); ++ return; ++ } ++ ++ access.numberFormatOverride(io.papermc.paper.util.PaperScoreboardFormat.asVanilla(format)); ++ } ++ // Paper end - add number format ++ + @Override + public boolean isScoreSet() { + Scoreboard board = this.objective.checkState().board; diff --git a/patches/server/0962-add-number-format-api.patch b/patches/server/0962-add-number-format-api.patch deleted file mode 100644 index d412b5f929..0000000000 --- a/patches/server/0962-add-number-format-api.patch +++ /dev/null @@ -1,138 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: David Mayr -Date: Sat, 16 Dec 2023 10:40:29 +0100 -Subject: [PATCH] add number format api - -== AT == -public net.minecraft.network.chat.numbers.FixedFormat value -public net.minecraft.network.chat.numbers.StyledFormat style - -diff --git a/src/main/java/io/papermc/paper/util/PaperScoreboardFormat.java b/src/main/java/io/papermc/paper/util/PaperScoreboardFormat.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6064086cc76ef0df999c7057121d0ac22bd4df65 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/util/PaperScoreboardFormat.java -@@ -0,0 +1,38 @@ -+package io.papermc.paper.util; -+ -+import io.papermc.paper.adventure.PaperAdventure; -+import io.papermc.paper.scoreboard.numbers.NumberFormat; -+ -+public final class PaperScoreboardFormat { -+ -+ private PaperScoreboardFormat() { -+ } -+ -+ public static net.minecraft.network.chat.numbers.NumberFormat asVanilla(final NumberFormat format) { -+ final net.minecraft.network.chat.numbers.NumberFormat vanilla; -+ if (format instanceof final io.papermc.paper.scoreboard.numbers.StyledFormat styled) { -+ vanilla = new net.minecraft.network.chat.numbers.StyledFormat(PaperAdventure.asVanilla(styled.style())); -+ } else if (format instanceof final io.papermc.paper.scoreboard.numbers.FixedFormat fixed) { -+ vanilla = new net.minecraft.network.chat.numbers.FixedFormat(io.papermc.paper.adventure.PaperAdventure -+ .asVanilla(fixed.component())); -+ } else if (format.equals(NumberFormat.blank())) { -+ vanilla = net.minecraft.network.chat.numbers.BlankFormat.INSTANCE; -+ } else { -+ throw new IllegalArgumentException("Unknown format type " + format.getClass()); -+ } -+ -+ return vanilla; -+ } -+ -+ public static NumberFormat asPaper(final net.minecraft.network.chat.numbers.NumberFormat vanilla) { -+ if (vanilla instanceof final net.minecraft.network.chat.numbers.StyledFormat styled) { -+ return NumberFormat.styled(PaperAdventure.asAdventure(styled.style)); -+ } else if (vanilla instanceof final net.minecraft.network.chat.numbers.FixedFormat fixed) { -+ return NumberFormat.fixed(io.papermc.paper.adventure.PaperAdventure.asAdventure(fixed.value)); -+ } else if (vanilla instanceof net.minecraft.network.chat.numbers.BlankFormat) { -+ return NumberFormat.blank(); -+ } -+ -+ throw new IllegalArgumentException("Unknown format type " + vanilla.getClass()); -+ } -+} -diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java -index 2d3abf2a1da487ead74d698cc5ea4eb729c35c8d..1fec80c4f02aab3770c05bac8bfa2b622625e630 100644 ---- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java -+++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftObjective.java -@@ -153,6 +153,34 @@ final class CraftObjective extends CraftScoreboardComponent implements Objective - } - // Paper end - -+ // Paper start - add number format -+ @Override -+ public io.papermc.paper.scoreboard.numbers.NumberFormat numberFormat() { -+ this.checkState(); -+ -+ net.minecraft.network.chat.numbers.NumberFormat vanilla = this.objective.numberFormat(); -+ -+ if (vanilla == null) { -+ return null; -+ } -+ -+ return io.papermc.paper.util.PaperScoreboardFormat.asPaper(vanilla); -+ } -+ -+ -+ @Override -+ public void numberFormat(io.papermc.paper.scoreboard.numbers.NumberFormat format) { -+ this.checkState(); -+ -+ if (format == null) { -+ this.objective.setNumberFormat(null); -+ return; -+ } -+ -+ this.objective.setNumberFormat(io.papermc.paper.util.PaperScoreboardFormat.asVanilla(format)); -+ } -+ // Paper end - add number format -+ - @Override - public void unregister() { - CraftScoreboard scoreboard = this.checkState(); -diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScore.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScore.java -index 74d9c407e971804bed420370f7b684d8658eb5aa..e307e897d6e1ba4cb21883dfeaf334bfbf56cfc4 100644 ---- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScore.java -+++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScore.java -@@ -55,6 +55,41 @@ final class CraftScore implements Score { - this.objective.checkState().board.getOrCreatePlayerScore(this.entry, this.objective.getHandle()).set(score); - } - -+ -+ // Paper start - add number format -+ @Override -+ public io.papermc.paper.scoreboard.numbers.NumberFormat numberFormat() { -+ ReadOnlyScoreInfo scoreInfo = this.objective.checkState().board -+ .getPlayerScoreInfo(this.entry, this.objective.getHandle()); -+ -+ if (scoreInfo == null) { -+ return null; -+ } -+ -+ net.minecraft.network.chat.numbers.NumberFormat vanilla = scoreInfo.numberFormat(); -+ -+ if (vanilla == null) { -+ return null; -+ } -+ -+ return io.papermc.paper.util.PaperScoreboardFormat.asPaper(vanilla); -+ } -+ -+ -+ @Override -+ public void numberFormat(io.papermc.paper.scoreboard.numbers.NumberFormat format) { -+ final net.minecraft.world.scores.ScoreAccess access = this.objective.checkState() -+ .board.getOrCreatePlayerScore(this.entry, this.objective.getHandle()); -+ -+ if (format == null) { -+ access.numberFormatOverride(null); -+ return; -+ } -+ -+ access.numberFormatOverride(io.papermc.paper.util.PaperScoreboardFormat.asVanilla(format)); -+ } -+ // Paper end - add number format -+ - @Override - public boolean isScoreSet() { - Scoreboard board = this.objective.checkState().board; diff --git a/patches/server/0962-improve-BanList-types.patch b/patches/server/0962-improve-BanList-types.patch new file mode 100644 index 0000000000..4be3dd5076 --- /dev/null +++ b/patches/server/0962-improve-BanList-types.patch @@ -0,0 +1,32 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Yannick Lamprecht +Date: Sat, 10 Feb 2024 20:50:01 +0100 +Subject: [PATCH] improve BanList types + + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index cacbb73e07ec2ea55d10af20c5f2a6d234c9bff3..5467a5985eaf159e2a4f19dd5723d05d8f2d598e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -2223,6 +2223,21 @@ public final class CraftServer implements Server { + }; + } + ++ // Paper start - add BanListType (which has a generic) ++ @SuppressWarnings("unchecked") ++ @Override ++ public , E> B getBanList(final io.papermc.paper.ban.BanListType type) { ++ Preconditions.checkArgument(type != null, "BanList.BanType cannot be null"); ++ if (type == io.papermc.paper.ban.BanListType.IP) { ++ return (B) new CraftIpBanList(this.playerList.getIpBans()); ++ } else if (type == io.papermc.paper.ban.BanListType.PROFILE) { ++ return (B) new CraftProfileBanList(this.playerList.getBans()); ++ } else { ++ throw new IllegalArgumentException("Unknown BanListType: " + type); ++ } ++ } ++ // Paper end - add BanListType (which has a generic) ++ + @Override + public void setWhitelist(boolean value) { + this.playerList.setUsingWhiteList(value); diff --git a/patches/server/0963-Expanded-Hopper-API.patch b/patches/server/0963-Expanded-Hopper-API.patch new file mode 100644 index 0000000000..2b59ac9974 --- /dev/null +++ b/patches/server/0963-Expanded-Hopper-API.patch @@ -0,0 +1,31 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: vicisacat +Date: Fri, 15 Mar 2024 17:35:40 +0100 +Subject: [PATCH] Expanded Hopper API + +== AT == +public net.minecraft.world.level.block.entity.HopperBlockEntity setCooldown(I)V +public net.minecraft.world.level.block.entity.HopperBlockEntity cooldownTime + +diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftHopper.java b/src/main/java/org/bukkit/craftbukkit/block/CraftHopper.java +index 2995b407415c77288a54c3f4eb8cb93e1a289283..f3c4d3835a18475e2cd2f519ac3dd9d9b59c454d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/block/CraftHopper.java ++++ b/src/main/java/org/bukkit/craftbukkit/block/CraftHopper.java +@@ -40,4 +40,17 @@ public class CraftHopper extends CraftLootable implements Hop + public CraftHopper copy(Location location) { + return new CraftHopper(this, location); + } ++ ++ // Paper start - Expanded Hopper API ++ @Override ++ public void setTransferCooldown(final int cooldown) { ++ com.google.common.base.Preconditions.checkArgument(cooldown >= 0, "Hooper transfer cooldown cannot be negative (" + cooldown + ")"); ++ getSnapshot().setCooldown(cooldown); ++ } ++ ++ @Override ++ public int getTransferCooldown() { ++ return getSnapshot().cooldownTime; ++ } ++ // Paper end - Expanded Hopper API + } diff --git a/patches/server/0963-improve-BanList-types.patch b/patches/server/0963-improve-BanList-types.patch deleted file mode 100644 index 4be3dd5076..0000000000 --- a/patches/server/0963-improve-BanList-types.patch +++ /dev/null @@ -1,32 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Yannick Lamprecht -Date: Sat, 10 Feb 2024 20:50:01 +0100 -Subject: [PATCH] improve BanList types - - -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index cacbb73e07ec2ea55d10af20c5f2a6d234c9bff3..5467a5985eaf159e2a4f19dd5723d05d8f2d598e 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -@@ -2223,6 +2223,21 @@ public final class CraftServer implements Server { - }; - } - -+ // Paper start - add BanListType (which has a generic) -+ @SuppressWarnings("unchecked") -+ @Override -+ public , E> B getBanList(final io.papermc.paper.ban.BanListType type) { -+ Preconditions.checkArgument(type != null, "BanList.BanType cannot be null"); -+ if (type == io.papermc.paper.ban.BanListType.IP) { -+ return (B) new CraftIpBanList(this.playerList.getIpBans()); -+ } else if (type == io.papermc.paper.ban.BanListType.PROFILE) { -+ return (B) new CraftProfileBanList(this.playerList.getBans()); -+ } else { -+ throw new IllegalArgumentException("Unknown BanListType: " + type); -+ } -+ } -+ // Paper end - add BanListType (which has a generic) -+ - @Override - public void setWhitelist(boolean value) { - this.playerList.setUsingWhiteList(value); diff --git a/patches/server/0964-Add-BlockBreakProgressUpdateEvent.patch b/patches/server/0964-Add-BlockBreakProgressUpdateEvent.patch new file mode 100644 index 0000000000..67abc67fb7 --- /dev/null +++ b/patches/server/0964-Add-BlockBreakProgressUpdateEvent.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Badbird5907 <50347938+Badbird5907@users.noreply.github.com> +Date: Mon, 4 Mar 2024 22:18:28 -0500 +Subject: [PATCH] Add BlockBreakProgressUpdateEvent + + +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 6e7f98fd7b43b46171c3cd58ed91b53960bd0d8a..55c7c078e0219e4a6a9e22db23f671ba728ff036 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -1328,6 +1328,17 @@ public class ServerLevel extends Level implements WorldGenLevel { + if (entity instanceof Player) entityhuman = (Player) entity; + // CraftBukkit end + ++ // Paper start - Add BlockBreakProgressUpdateEvent ++ // If a plugin is using this method to send destroy packets for a client-side only entity id, no block progress occurred on the server. ++ // Hence, do not call the event. ++ if (entity != null) { ++ float progressFloat = Mth.clamp(progress, 0, 10) / 10.0f; ++ org.bukkit.craftbukkit.block.CraftBlock bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(this, pos); ++ new io.papermc.paper.event.block.BlockBreakProgressUpdateEvent(bukkitBlock, progressFloat, entity.getBukkitEntity()) ++ .callEvent(); ++ } ++ // Paper end - Add BlockBreakProgressUpdateEvent ++ + while (iterator.hasNext()) { + ServerPlayer entityplayer = (ServerPlayer) iterator.next(); + diff --git a/patches/server/0964-Expanded-Hopper-API.patch b/patches/server/0964-Expanded-Hopper-API.patch deleted file mode 100644 index 2b59ac9974..0000000000 --- a/patches/server/0964-Expanded-Hopper-API.patch +++ /dev/null @@ -1,31 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: vicisacat -Date: Fri, 15 Mar 2024 17:35:40 +0100 -Subject: [PATCH] Expanded Hopper API - -== AT == -public net.minecraft.world.level.block.entity.HopperBlockEntity setCooldown(I)V -public net.minecraft.world.level.block.entity.HopperBlockEntity cooldownTime - -diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftHopper.java b/src/main/java/org/bukkit/craftbukkit/block/CraftHopper.java -index 2995b407415c77288a54c3f4eb8cb93e1a289283..f3c4d3835a18475e2cd2f519ac3dd9d9b59c454d 100644 ---- a/src/main/java/org/bukkit/craftbukkit/block/CraftHopper.java -+++ b/src/main/java/org/bukkit/craftbukkit/block/CraftHopper.java -@@ -40,4 +40,17 @@ public class CraftHopper extends CraftLootable implements Hop - public CraftHopper copy(Location location) { - return new CraftHopper(this, location); - } -+ -+ // Paper start - Expanded Hopper API -+ @Override -+ public void setTransferCooldown(final int cooldown) { -+ com.google.common.base.Preconditions.checkArgument(cooldown >= 0, "Hooper transfer cooldown cannot be negative (" + cooldown + ")"); -+ getSnapshot().setCooldown(cooldown); -+ } -+ -+ @Override -+ public int getTransferCooldown() { -+ return getSnapshot().cooldownTime; -+ } -+ // Paper end - Expanded Hopper API - } diff --git a/patches/server/0965-Add-BlockBreakProgressUpdateEvent.patch b/patches/server/0965-Add-BlockBreakProgressUpdateEvent.patch deleted file mode 100644 index 67abc67fb7..0000000000 --- a/patches/server/0965-Add-BlockBreakProgressUpdateEvent.patch +++ /dev/null @@ -1,28 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Badbird5907 <50347938+Badbird5907@users.noreply.github.com> -Date: Mon, 4 Mar 2024 22:18:28 -0500 -Subject: [PATCH] Add BlockBreakProgressUpdateEvent - - -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 6e7f98fd7b43b46171c3cd58ed91b53960bd0d8a..55c7c078e0219e4a6a9e22db23f671ba728ff036 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -1328,6 +1328,17 @@ public class ServerLevel extends Level implements WorldGenLevel { - if (entity instanceof Player) entityhuman = (Player) entity; - // CraftBukkit end - -+ // Paper start - Add BlockBreakProgressUpdateEvent -+ // If a plugin is using this method to send destroy packets for a client-side only entity id, no block progress occurred on the server. -+ // Hence, do not call the event. -+ if (entity != null) { -+ float progressFloat = Mth.clamp(progress, 0, 10) / 10.0f; -+ org.bukkit.craftbukkit.block.CraftBlock bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(this, pos); -+ new io.papermc.paper.event.block.BlockBreakProgressUpdateEvent(bukkitBlock, progressFloat, entity.getBukkitEntity()) -+ .callEvent(); -+ } -+ // Paper end - Add BlockBreakProgressUpdateEvent -+ - while (iterator.hasNext()) { - ServerPlayer entityplayer = (ServerPlayer) iterator.next(); - diff --git a/patches/server/0965-Deprecate-ItemStack-setType.patch b/patches/server/0965-Deprecate-ItemStack-setType.patch new file mode 100644 index 0000000000..a252511a0b --- /dev/null +++ b/patches/server/0965-Deprecate-ItemStack-setType.patch @@ -0,0 +1,35 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> +Date: Tue, 26 Mar 2024 21:42:23 -0400 +Subject: [PATCH] Deprecate ItemStack#setType + + +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java +index 2ba771efa61e109804f3141e95f77613ac952ed1..39e46f3f4b51fce9bc492fa498aa7427b27b78d1 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java +@@ -715,4 +715,24 @@ public final class CraftItemStack extends ItemStack { + static boolean hasItemMeta(net.minecraft.world.item.ItemStack item) { + return !(item == null || item.getComponentsPatch().isEmpty()); + } ++ // Paper start - with type ++ @Override ++ public ItemStack withType(final Material type) { ++ if (type == Material.AIR) { ++ return CraftItemStack.asCraftMirror(null); ++ } ++ ++ final net.minecraft.world.item.ItemStack copy = new net.minecraft.world.item.ItemStack( ++ CraftItemType.bukkitToMinecraft(type), this.getAmount() ++ ); ++ ++ if (this.handle != null) { ++ copy.applyComponents(this.handle.getComponents()); ++ } ++ ++ final CraftItemStack mirrored = CraftItemStack.asCraftMirror(copy); ++ mirrored.setItemMeta(mirrored.getItemMeta()); ++ return mirrored; ++ } ++ // Paper end + } diff --git a/patches/server/0966-Add-CartographyItemEvent.patch b/patches/server/0966-Add-CartographyItemEvent.patch new file mode 100644 index 0000000000..4502c9aa8a --- /dev/null +++ b/patches/server/0966-Add-CartographyItemEvent.patch @@ -0,0 +1,43 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Janet Blackquill +Date: Sun, 7 Apr 2024 16:52:42 -0400 +Subject: [PATCH] Add CartographyItemEvent + + +diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 4850971b46586cc6a4aeeba014bc6dcf6453d820..4db287b7d0d25b0e2b4943a02a72964c387ca321 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -3094,6 +3094,19 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + } + ++ // Paper start - cartography item event ++ if (packet.getSlotNum() == net.minecraft.world.inventory.CartographyTableMenu.RESULT_SLOT && top instanceof org.bukkit.inventory.CartographyInventory cartographyInventory) { ++ org.bukkit.inventory.ItemStack result = cartographyInventory.getResult(); ++ if (result != null && !result.isEmpty()) { ++ if (click == ClickType.NUMBER_KEY) { ++ event = new io.papermc.paper.event.player.CartographyItemEvent(inventory, type, packet.getSlotNum(), click, action, packet.getButtonNum()); ++ } else { ++ event = new io.papermc.paper.event.player.CartographyItemEvent(inventory, type, packet.getSlotNum(), click, action); ++ } ++ } ++ } ++ // Paper end - cartography item event ++ + event.setCancelled(cancelled); + AbstractContainerMenu oldContainer = this.player.containerMenu; // SPIGOT-1224 + this.cserver.getPluginManager().callEvent(event); +diff --git a/src/main/java/net/minecraft/world/inventory/CartographyTableMenu.java b/src/main/java/net/minecraft/world/inventory/CartographyTableMenu.java +index ab98637bf967ac19f0bc06e8cb7f18a8b13ec809..a90e2c56c54797b2fec40eb3865835c5f640a544 100644 +--- a/src/main/java/net/minecraft/world/inventory/CartographyTableMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/CartographyTableMenu.java +@@ -71,7 +71,7 @@ public class CartographyTableMenu extends AbstractContainerMenu { + this.resultContainer = new ResultContainer(this.createBlockHolder(context)) { // Paper - Add missing InventoryHolders + @Override + public void setChanged() { +- CartographyTableMenu.this.slotsChanged(this); ++ // CartographyTableMenu.this.slotsChanged(this); // Paper - Add CatographyItemEvent - do not recompute results if the result slot changes - allows to set the result slot via api + super.setChanged(); + } + diff --git a/patches/server/0966-Deprecate-ItemStack-setType.patch b/patches/server/0966-Deprecate-ItemStack-setType.patch deleted file mode 100644 index a252511a0b..0000000000 --- a/patches/server/0966-Deprecate-ItemStack-setType.patch +++ /dev/null @@ -1,35 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> -Date: Tue, 26 Mar 2024 21:42:23 -0400 -Subject: [PATCH] Deprecate ItemStack#setType - - -diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java -index 2ba771efa61e109804f3141e95f77613ac952ed1..39e46f3f4b51fce9bc492fa498aa7427b27b78d1 100644 ---- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java -+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java -@@ -715,4 +715,24 @@ public final class CraftItemStack extends ItemStack { - static boolean hasItemMeta(net.minecraft.world.item.ItemStack item) { - return !(item == null || item.getComponentsPatch().isEmpty()); - } -+ // Paper start - with type -+ @Override -+ public ItemStack withType(final Material type) { -+ if (type == Material.AIR) { -+ return CraftItemStack.asCraftMirror(null); -+ } -+ -+ final net.minecraft.world.item.ItemStack copy = new net.minecraft.world.item.ItemStack( -+ CraftItemType.bukkitToMinecraft(type), this.getAmount() -+ ); -+ -+ if (this.handle != null) { -+ copy.applyComponents(this.handle.getComponents()); -+ } -+ -+ final CraftItemStack mirrored = CraftItemStack.asCraftMirror(copy); -+ mirrored.setItemMeta(mirrored.getItemMeta()); -+ return mirrored; -+ } -+ // Paper end - } diff --git a/patches/server/0967-Add-CartographyItemEvent.patch b/patches/server/0967-Add-CartographyItemEvent.patch deleted file mode 100644 index 4502c9aa8a..0000000000 --- a/patches/server/0967-Add-CartographyItemEvent.patch +++ /dev/null @@ -1,43 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Janet Blackquill -Date: Sun, 7 Apr 2024 16:52:42 -0400 -Subject: [PATCH] Add CartographyItemEvent - - -diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index 4850971b46586cc6a4aeeba014bc6dcf6453d820..4db287b7d0d25b0e2b4943a02a72964c387ca321 100644 ---- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -3094,6 +3094,19 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - } - } - -+ // Paper start - cartography item event -+ if (packet.getSlotNum() == net.minecraft.world.inventory.CartographyTableMenu.RESULT_SLOT && top instanceof org.bukkit.inventory.CartographyInventory cartographyInventory) { -+ org.bukkit.inventory.ItemStack result = cartographyInventory.getResult(); -+ if (result != null && !result.isEmpty()) { -+ if (click == ClickType.NUMBER_KEY) { -+ event = new io.papermc.paper.event.player.CartographyItemEvent(inventory, type, packet.getSlotNum(), click, action, packet.getButtonNum()); -+ } else { -+ event = new io.papermc.paper.event.player.CartographyItemEvent(inventory, type, packet.getSlotNum(), click, action); -+ } -+ } -+ } -+ // Paper end - cartography item event -+ - event.setCancelled(cancelled); - AbstractContainerMenu oldContainer = this.player.containerMenu; // SPIGOT-1224 - this.cserver.getPluginManager().callEvent(event); -diff --git a/src/main/java/net/minecraft/world/inventory/CartographyTableMenu.java b/src/main/java/net/minecraft/world/inventory/CartographyTableMenu.java -index ab98637bf967ac19f0bc06e8cb7f18a8b13ec809..a90e2c56c54797b2fec40eb3865835c5f640a544 100644 ---- a/src/main/java/net/minecraft/world/inventory/CartographyTableMenu.java -+++ b/src/main/java/net/minecraft/world/inventory/CartographyTableMenu.java -@@ -71,7 +71,7 @@ public class CartographyTableMenu extends AbstractContainerMenu { - this.resultContainer = new ResultContainer(this.createBlockHolder(context)) { // Paper - Add missing InventoryHolders - @Override - public void setChanged() { -- CartographyTableMenu.this.slotsChanged(this); -+ // CartographyTableMenu.this.slotsChanged(this); // Paper - Add CatographyItemEvent - do not recompute results if the result slot changes - allows to set the result slot via api - super.setChanged(); - } - diff --git a/patches/server/0967-More-Raid-API.patch b/patches/server/0967-More-Raid-API.patch new file mode 100644 index 0000000000..14fcb7449a --- /dev/null +++ b/patches/server/0967-More-Raid-API.patch @@ -0,0 +1,106 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Fri, 4 Mar 2022 09:46:33 -0800 +Subject: [PATCH] More Raid API + +== AT == +public net.minecraft.world.entity.raid.Raid raidEvent + +diff --git a/src/main/java/net/minecraft/world/entity/raid/Raid.java b/src/main/java/net/minecraft/world/entity/raid/Raid.java +index 364d233ca318bbf8c1bc3acfbaf2d2420fdf922f..fdff9788eaf663be79214b2ca491f0f0444f6136 100644 +--- a/src/main/java/net/minecraft/world/entity/raid/Raid.java ++++ b/src/main/java/net/minecraft/world/entity/raid/Raid.java +@@ -110,6 +110,11 @@ public class Raid { + private Raid.RaidStatus status; + private int celebrationTicks; + private Optional waveSpawnPos; ++ // Paper start ++ private static final String PDC_NBT_KEY = "BukkitValues"; ++ private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry PDC_TYPE_REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry(); ++ public final org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer(PDC_TYPE_REGISTRY); ++ // Paper end + + public Raid(int id, ServerLevel world, BlockPos pos) { + this.raidEvent = new ServerBossEvent(Raid.RAID_NAME_COMPONENT, BossEvent.BossBarColor.RED, BossEvent.BossBarOverlay.NOTCHED_10); +@@ -153,6 +158,11 @@ public class Raid { + this.heroesOfTheVillage.add(NbtUtils.loadUUID(nbtbase)); + } + } ++ // Paper start ++ if (nbt.contains(PDC_NBT_KEY, net.minecraft.nbt.Tag.TAG_COMPOUND)) { ++ this.persistentDataContainer.putAll(nbt.getCompound(PDC_NBT_KEY)); ++ } ++ // Paper end + + } + +@@ -870,6 +880,11 @@ public class Raid { + } + + nbt.put("HeroesOfTheVillage", nbttaglist); ++ // Paper start ++ if (!this.persistentDataContainer.isEmpty()) { ++ nbt.put(PDC_NBT_KEY, this.persistentDataContainer.toTagCompound()); ++ } ++ // Paper end + return nbt; + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftRaid.java b/src/main/java/org/bukkit/craftbukkit/CraftRaid.java +index b8ce1c1c2447f9cff1717bfcfd6eb911ade0d4b3..51f21af9d75769abdcba713b9aa33392e994d9b0 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftRaid.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftRaid.java +@@ -103,4 +103,34 @@ public final class CraftRaid implements Raid { + public net.minecraft.world.entity.raid.Raid getHandle() { + return this.handle; + } ++ ++ // Paper start - more Raid API ++ @Override ++ public int getId() { ++ return this.handle.getId(); ++ } ++ ++ @Override ++ public org.bukkit.boss.BossBar getBossBar() { ++ return new org.bukkit.craftbukkit.boss.CraftBossBar(this.handle.raidEvent); ++ } ++ ++ @Override ++ public org.bukkit.persistence.PersistentDataContainer getPersistentDataContainer() { ++ return this.handle.persistentDataContainer; ++ } ++ ++ @Override ++ public boolean equals(final Object o) { ++ if (this == o) return true; ++ if (o == null || this.getClass() != o.getClass()) return false; ++ final org.bukkit.craftbukkit.CraftRaid craftRaid = (org.bukkit.craftbukkit.CraftRaid) o; ++ return this.handle.equals(craftRaid.handle); ++ } ++ ++ @Override ++ public int hashCode() { ++ return this.handle.hashCode(); ++ } ++ // Paper end - more Raid API + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 4a62482b7c73728de8a94542e8978e836540b328..34f03dd227181a03fa90845067424a26382bab9b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -2373,6 +2373,14 @@ public class CraftWorld extends CraftRegionAccessor implements World { + return (raid == null) ? null : new CraftRaid(raid); + } + ++ // Paper start - more Raid API ++ @Override ++ public @Nullable Raid getRaid(final int id) { ++ final net.minecraft.world.entity.raid.@Nullable Raid nmsRaid = this.world.getRaids().raidMap.get(id); ++ return nmsRaid != null ? new CraftRaid(nmsRaid) : null; ++ } ++ // Paper end - more Raid API ++ + @Override + public List getRaids() { + Raids persistentRaid = this.world.getRaids(); diff --git a/patches/server/0968-Add-onboarding-message-for-initial-server-start.patch b/patches/server/0968-Add-onboarding-message-for-initial-server-start.patch new file mode 100644 index 0000000000..7d72f645d2 --- /dev/null +++ b/patches/server/0968-Add-onboarding-message-for-initial-server-start.patch @@ -0,0 +1,103 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: leguan +Date: Sun, 10 Mar 2024 20:10:41 +0100 +Subject: [PATCH] Add onboarding message for initial server start + + +diff --git a/src/main/java/io/papermc/paper/configuration/Configurations.java b/src/main/java/io/papermc/paper/configuration/Configurations.java +index c01b4393439838976965823298f12e4762e72eff..218bf89fd7583d6db9f64754c4db8fcce5415bdb 100644 +--- a/src/main/java/io/papermc/paper/configuration/Configurations.java ++++ b/src/main/java/io/papermc/paper/configuration/Configurations.java +@@ -126,6 +126,7 @@ public abstract class Configurations { + if (Files.notExists(configFile)) { + node = CommentedConfigurationNode.root(loader.defaultOptions()); + node.node(Configuration.VERSION_FIELD).raw(this.globalConfigVersion()); ++ GlobalConfiguration.isFirstStart = true; + } else { + node = loader.load(); + this.verifyGlobalConfigVersion(node); +diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +index 0b283171294eca65a898ddd9ab1b7295ad183b0d..481cfe3a291196c0391c2ec8560e566e0e1e2fb8 100644 +--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java ++++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +@@ -25,6 +25,7 @@ public class GlobalConfiguration extends ConfigurationPart { + private static final Logger LOGGER = LogUtils.getLogger(); + static final int CURRENT_VERSION = 29; // (when you change the version, change the comment, so it conflicts on rebases): + private static GlobalConfiguration instance; ++ public static boolean isFirstStart = false; + public static GlobalConfiguration get() { + return instance; + } +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 3c3383b00dc76c6d0ffd9b2a6a8cf97ec00f91be..881679270a344f1a4d70c3c47871a495af202109 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -1132,6 +1132,16 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop " + onboardingLink + ""); ++ link.setFont(MinecraftServerGui.MONOSPACED); ++ link.setCursor(new java.awt.Cursor(java.awt.Cursor.HAND_CURSOR)); ++ link.addMouseListener(new java.awt.event.MouseAdapter() { ++ @Override ++ public void mouseClicked(final java.awt.event.MouseEvent e) { ++ try { ++ java.awt.Desktop.getDesktop().browse(java.net.URI.create(onboardingLink)); ++ } catch (java.io.IOException exception) { ++ LOGGER.error("Unable to find a default browser. Please manually visit the website: " + onboardingLink, exception); ++ } catch (UnsupportedOperationException exception) { ++ LOGGER.error("This platform does not support the BROWSE action. Please manually visit the website: " + onboardingLink, exception); ++ } catch (SecurityException exception) { ++ LOGGER.error("This action has been denied by the security manager. Please manually visit the website: " + onboardingLink, exception); ++ } ++ } ++ }); ++ ++ jPanel.add(jLabel); ++ jPanel.add(link); ++ ++ return jPanel; ++ } ++ // Paper end - Add onboarding message for initial server start ++ + private JComponent buildPlayerPanel() { + JList jlist = new PlayerListComponent(this.server); + JScrollPane jscrollpane = new JScrollPane(jlist, 22, 30); diff --git a/patches/server/0968-More-Raid-API.patch b/patches/server/0968-More-Raid-API.patch deleted file mode 100644 index 14fcb7449a..0000000000 --- a/patches/server/0968-More-Raid-API.patch +++ /dev/null @@ -1,106 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Fri, 4 Mar 2022 09:46:33 -0800 -Subject: [PATCH] More Raid API - -== AT == -public net.minecraft.world.entity.raid.Raid raidEvent - -diff --git a/src/main/java/net/minecraft/world/entity/raid/Raid.java b/src/main/java/net/minecraft/world/entity/raid/Raid.java -index 364d233ca318bbf8c1bc3acfbaf2d2420fdf922f..fdff9788eaf663be79214b2ca491f0f0444f6136 100644 ---- a/src/main/java/net/minecraft/world/entity/raid/Raid.java -+++ b/src/main/java/net/minecraft/world/entity/raid/Raid.java -@@ -110,6 +110,11 @@ public class Raid { - private Raid.RaidStatus status; - private int celebrationTicks; - private Optional waveSpawnPos; -+ // Paper start -+ private static final String PDC_NBT_KEY = "BukkitValues"; -+ private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry PDC_TYPE_REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry(); -+ public final org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer(PDC_TYPE_REGISTRY); -+ // Paper end - - public Raid(int id, ServerLevel world, BlockPos pos) { - this.raidEvent = new ServerBossEvent(Raid.RAID_NAME_COMPONENT, BossEvent.BossBarColor.RED, BossEvent.BossBarOverlay.NOTCHED_10); -@@ -153,6 +158,11 @@ public class Raid { - this.heroesOfTheVillage.add(NbtUtils.loadUUID(nbtbase)); - } - } -+ // Paper start -+ if (nbt.contains(PDC_NBT_KEY, net.minecraft.nbt.Tag.TAG_COMPOUND)) { -+ this.persistentDataContainer.putAll(nbt.getCompound(PDC_NBT_KEY)); -+ } -+ // Paper end - - } - -@@ -870,6 +880,11 @@ public class Raid { - } - - nbt.put("HeroesOfTheVillage", nbttaglist); -+ // Paper start -+ if (!this.persistentDataContainer.isEmpty()) { -+ nbt.put(PDC_NBT_KEY, this.persistentDataContainer.toTagCompound()); -+ } -+ // Paper end - return nbt; - } - -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftRaid.java b/src/main/java/org/bukkit/craftbukkit/CraftRaid.java -index b8ce1c1c2447f9cff1717bfcfd6eb911ade0d4b3..51f21af9d75769abdcba713b9aa33392e994d9b0 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftRaid.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftRaid.java -@@ -103,4 +103,34 @@ public final class CraftRaid implements Raid { - public net.minecraft.world.entity.raid.Raid getHandle() { - return this.handle; - } -+ -+ // Paper start - more Raid API -+ @Override -+ public int getId() { -+ return this.handle.getId(); -+ } -+ -+ @Override -+ public org.bukkit.boss.BossBar getBossBar() { -+ return new org.bukkit.craftbukkit.boss.CraftBossBar(this.handle.raidEvent); -+ } -+ -+ @Override -+ public org.bukkit.persistence.PersistentDataContainer getPersistentDataContainer() { -+ return this.handle.persistentDataContainer; -+ } -+ -+ @Override -+ public boolean equals(final Object o) { -+ if (this == o) return true; -+ if (o == null || this.getClass() != o.getClass()) return false; -+ final org.bukkit.craftbukkit.CraftRaid craftRaid = (org.bukkit.craftbukkit.CraftRaid) o; -+ return this.handle.equals(craftRaid.handle); -+ } -+ -+ @Override -+ public int hashCode() { -+ return this.handle.hashCode(); -+ } -+ // Paper end - more Raid API - } -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index 4a62482b7c73728de8a94542e8978e836540b328..34f03dd227181a03fa90845067424a26382bab9b 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -@@ -2373,6 +2373,14 @@ public class CraftWorld extends CraftRegionAccessor implements World { - return (raid == null) ? null : new CraftRaid(raid); - } - -+ // Paper start - more Raid API -+ @Override -+ public @Nullable Raid getRaid(final int id) { -+ final net.minecraft.world.entity.raid.@Nullable Raid nmsRaid = this.world.getRaids().raidMap.get(id); -+ return nmsRaid != null ? new CraftRaid(nmsRaid) : null; -+ } -+ // Paper end - more Raid API -+ - @Override - public List getRaids() { - Raids persistentRaid = this.world.getRaids(); diff --git a/patches/server/0969-Add-onboarding-message-for-initial-server-start.patch b/patches/server/0969-Add-onboarding-message-for-initial-server-start.patch deleted file mode 100644 index 7d72f645d2..0000000000 --- a/patches/server/0969-Add-onboarding-message-for-initial-server-start.patch +++ /dev/null @@ -1,103 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: leguan -Date: Sun, 10 Mar 2024 20:10:41 +0100 -Subject: [PATCH] Add onboarding message for initial server start - - -diff --git a/src/main/java/io/papermc/paper/configuration/Configurations.java b/src/main/java/io/papermc/paper/configuration/Configurations.java -index c01b4393439838976965823298f12e4762e72eff..218bf89fd7583d6db9f64754c4db8fcce5415bdb 100644 ---- a/src/main/java/io/papermc/paper/configuration/Configurations.java -+++ b/src/main/java/io/papermc/paper/configuration/Configurations.java -@@ -126,6 +126,7 @@ public abstract class Configurations { - if (Files.notExists(configFile)) { - node = CommentedConfigurationNode.root(loader.defaultOptions()); - node.node(Configuration.VERSION_FIELD).raw(this.globalConfigVersion()); -+ GlobalConfiguration.isFirstStart = true; - } else { - node = loader.load(); - this.verifyGlobalConfigVersion(node); -diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java -index 0b283171294eca65a898ddd9ab1b7295ad183b0d..481cfe3a291196c0391c2ec8560e566e0e1e2fb8 100644 ---- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java -+++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java -@@ -25,6 +25,7 @@ public class GlobalConfiguration extends ConfigurationPart { - private static final Logger LOGGER = LogUtils.getLogger(); - static final int CURRENT_VERSION = 29; // (when you change the version, change the comment, so it conflicts on rebases): - private static GlobalConfiguration instance; -+ public static boolean isFirstStart = false; - public static GlobalConfiguration get() { - return instance; - } -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 3c3383b00dc76c6d0ffd9b2a6a8cf97ec00f91be..881679270a344f1a4d70c3c47871a495af202109 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1132,6 +1132,16 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop " + onboardingLink + ""); -+ link.setFont(MinecraftServerGui.MONOSPACED); -+ link.setCursor(new java.awt.Cursor(java.awt.Cursor.HAND_CURSOR)); -+ link.addMouseListener(new java.awt.event.MouseAdapter() { -+ @Override -+ public void mouseClicked(final java.awt.event.MouseEvent e) { -+ try { -+ java.awt.Desktop.getDesktop().browse(java.net.URI.create(onboardingLink)); -+ } catch (java.io.IOException exception) { -+ LOGGER.error("Unable to find a default browser. Please manually visit the website: " + onboardingLink, exception); -+ } catch (UnsupportedOperationException exception) { -+ LOGGER.error("This platform does not support the BROWSE action. Please manually visit the website: " + onboardingLink, exception); -+ } catch (SecurityException exception) { -+ LOGGER.error("This action has been denied by the security manager. Please manually visit the website: " + onboardingLink, exception); -+ } -+ } -+ }); -+ -+ jPanel.add(jLabel); -+ jPanel.add(link); -+ -+ return jPanel; -+ } -+ // Paper end - Add onboarding message for initial server start -+ - private JComponent buildPlayerPanel() { - JList jlist = new PlayerListComponent(this.server); - JScrollPane jscrollpane = new JScrollPane(jlist, 22, 30); diff --git a/patches/server/0969-Configurable-max-block-fluid-ticks.patch b/patches/server/0969-Configurable-max-block-fluid-ticks.patch new file mode 100644 index 0000000000..ed1c561d6e --- /dev/null +++ b/patches/server/0969-Configurable-max-block-fluid-ticks.patch @@ -0,0 +1,22 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Gero +Date: Mon, 19 Feb 2024 17:39:59 +0100 +Subject: [PATCH] Configurable max block/fluid ticks + + +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 55c7c078e0219e4a6a9e22db23f671ba728ff036..5eab8532bfe3b7082123a12b8a3e62d25a4463d3 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -506,9 +506,9 @@ public class ServerLevel extends Level implements WorldGenLevel { + if (!this.isDebug() && flag) { + j = this.getGameTime(); + gameprofilerfiller.push("blockTicks"); +- this.blockTicks.tick(j, 65536, this::tickBlock); ++ this.blockTicks.tick(j, paperConfig().environment.maxBlockTicks, this::tickBlock); // Paper - configurable max block ticks + gameprofilerfiller.popPush("fluidTicks"); +- this.fluidTicks.tick(j, 65536, this::tickFluid); ++ this.fluidTicks.tick(j, paperConfig().environment.maxFluidTicks, this::tickFluid); // Paper - configurable max fluid ticks + gameprofilerfiller.pop(); + } + this.timings.scheduledBlocks.stopTiming(); // Paper diff --git a/patches/server/0970-Configurable-max-block-fluid-ticks.patch b/patches/server/0970-Configurable-max-block-fluid-ticks.patch deleted file mode 100644 index ed1c561d6e..0000000000 --- a/patches/server/0970-Configurable-max-block-fluid-ticks.patch +++ /dev/null @@ -1,22 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Gero -Date: Mon, 19 Feb 2024 17:39:59 +0100 -Subject: [PATCH] Configurable max block/fluid ticks - - -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 55c7c078e0219e4a6a9e22db23f671ba728ff036..5eab8532bfe3b7082123a12b8a3e62d25a4463d3 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -506,9 +506,9 @@ public class ServerLevel extends Level implements WorldGenLevel { - if (!this.isDebug() && flag) { - j = this.getGameTime(); - gameprofilerfiller.push("blockTicks"); -- this.blockTicks.tick(j, 65536, this::tickBlock); -+ this.blockTicks.tick(j, paperConfig().environment.maxBlockTicks, this::tickBlock); // Paper - configurable max block ticks - gameprofilerfiller.popPush("fluidTicks"); -- this.fluidTicks.tick(j, 65536, this::tickFluid); -+ this.fluidTicks.tick(j, paperConfig().environment.maxFluidTicks, this::tickFluid); // Paper - configurable max fluid ticks - gameprofilerfiller.pop(); - } - this.timings.scheduledBlocks.stopTiming(); // Paper diff --git a/patches/server/0970-Fix-bees-aging-inside-hives.patch b/patches/server/0970-Fix-bees-aging-inside-hives.patch new file mode 100644 index 0000000000..ae6a0ff63d --- /dev/null +++ b/patches/server/0970-Fix-bees-aging-inside-hives.patch @@ -0,0 +1,41 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Sat, 21 Aug 2021 21:54:16 -0700 +Subject: [PATCH] Fix bees aging inside hives + +Fixes bees incorrectly being aged up due to upstream's +resetting the ticks inside hive on a failed release + +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java +index 6fb7ec36f6f7a3021fac4b9e31fd333dfd5ea5e5..7b263fab4f0014400b3b8e7e33db32f9a125f6ba 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java +@@ -314,7 +314,7 @@ public class BeehiveBlockEntity extends BlockEntity { + iterator.remove(); + // CraftBukkit start + } else { +- tileentitybeehive_hivebee.ticksInHive = tileentitybeehive_hivebee.occupant.minTicksInHive / 2; // Not strictly Vanilla behaviour in cases where bees cannot spawn but still reasonable ++ tileentitybeehive_hivebee.exitTickCounter = tileentitybeehive_hivebee.occupant.minTicksInHive / 2; // Not strictly Vanilla behaviour in cases where bees cannot spawn but still reasonable // Paper - Fix bees aging inside hives; use exitTickCounter to keep actual bee life + // CraftBukkit end + } + } +@@ -474,15 +474,18 @@ public class BeehiveBlockEntity extends BlockEntity { + private static class BeeData { + + private final BeehiveBlockEntity.Occupant occupant; ++ private int exitTickCounter; // Paper - Fix bees aging inside hives; separate counter for checking if bee should exit to reduce exit attempts + private int ticksInHive; + + BeeData(BeehiveBlockEntity.Occupant data) { + this.occupant = data; + this.ticksInHive = data.ticksInHive(); ++ this.exitTickCounter = this.ticksInHive; // Paper - Fix bees aging inside hives + } + + public boolean tick() { +- return this.ticksInHive++ > this.occupant.minTicksInHive; ++ this.ticksInHive++; // Paper - Fix bees aging inside hives ++ return this.exitTickCounter++ > this.occupant.minTicksInHive; // Paper - Fix bees aging inside hives + } + + public BeehiveBlockEntity.Occupant toOccupant() { diff --git a/patches/server/0971-Disable-memory-reserve-allocating.patch b/patches/server/0971-Disable-memory-reserve-allocating.patch new file mode 100644 index 0000000000..30eabae9e8 --- /dev/null +++ b/patches/server/0971-Disable-memory-reserve-allocating.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Warrior <50800980+Warriorrrr@users.noreply.github.com> +Date: Thu, 18 Jan 2024 23:25:09 +0100 +Subject: [PATCH] Disable memory reserve allocating + + +diff --git a/src/main/java/net/minecraft/CrashReport.java b/src/main/java/net/minecraft/CrashReport.java +index 06c514b2d0674cd7df6692981f020437ea0f2f91..bcb6a3b3cd17ce5db9aaf6bd3ec7a0ec1b44b979 100644 +--- a/src/main/java/net/minecraft/CrashReport.java ++++ b/src/main/java/net/minecraft/CrashReport.java +@@ -250,7 +250,7 @@ public class CrashReport { + } + + public static void preload() { +- MemoryReserve.allocate(); ++ // MemoryReserve.allocate(); // Paper - Disable memory reserve allocating + (new CrashReport("Don't panic!", new Throwable())).getFriendlyReport(); + } + } diff --git a/patches/server/0971-Fix-bees-aging-inside-hives.patch b/patches/server/0971-Fix-bees-aging-inside-hives.patch deleted file mode 100644 index ae6a0ff63d..0000000000 --- a/patches/server/0971-Fix-bees-aging-inside-hives.patch +++ /dev/null @@ -1,41 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Sat, 21 Aug 2021 21:54:16 -0700 -Subject: [PATCH] Fix bees aging inside hives - -Fixes bees incorrectly being aged up due to upstream's -resetting the ticks inside hive on a failed release - -diff --git a/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java -index 6fb7ec36f6f7a3021fac4b9e31fd333dfd5ea5e5..7b263fab4f0014400b3b8e7e33db32f9a125f6ba 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java -@@ -314,7 +314,7 @@ public class BeehiveBlockEntity extends BlockEntity { - iterator.remove(); - // CraftBukkit start - } else { -- tileentitybeehive_hivebee.ticksInHive = tileentitybeehive_hivebee.occupant.minTicksInHive / 2; // Not strictly Vanilla behaviour in cases where bees cannot spawn but still reasonable -+ tileentitybeehive_hivebee.exitTickCounter = tileentitybeehive_hivebee.occupant.minTicksInHive / 2; // Not strictly Vanilla behaviour in cases where bees cannot spawn but still reasonable // Paper - Fix bees aging inside hives; use exitTickCounter to keep actual bee life - // CraftBukkit end - } - } -@@ -474,15 +474,18 @@ public class BeehiveBlockEntity extends BlockEntity { - private static class BeeData { - - private final BeehiveBlockEntity.Occupant occupant; -+ private int exitTickCounter; // Paper - Fix bees aging inside hives; separate counter for checking if bee should exit to reduce exit attempts - private int ticksInHive; - - BeeData(BeehiveBlockEntity.Occupant data) { - this.occupant = data; - this.ticksInHive = data.ticksInHive(); -+ this.exitTickCounter = this.ticksInHive; // Paper - Fix bees aging inside hives - } - - public boolean tick() { -- return this.ticksInHive++ > this.occupant.minTicksInHive; -+ this.ticksInHive++; // Paper - Fix bees aging inside hives -+ return this.exitTickCounter++ > this.occupant.minTicksInHive; // Paper - Fix bees aging inside hives - } - - public BeehiveBlockEntity.Occupant toOccupant() { diff --git a/patches/server/0972-Disable-memory-reserve-allocating.patch b/patches/server/0972-Disable-memory-reserve-allocating.patch deleted file mode 100644 index 30eabae9e8..0000000000 --- a/patches/server/0972-Disable-memory-reserve-allocating.patch +++ /dev/null @@ -1,19 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Warrior <50800980+Warriorrrr@users.noreply.github.com> -Date: Thu, 18 Jan 2024 23:25:09 +0100 -Subject: [PATCH] Disable memory reserve allocating - - -diff --git a/src/main/java/net/minecraft/CrashReport.java b/src/main/java/net/minecraft/CrashReport.java -index 06c514b2d0674cd7df6692981f020437ea0f2f91..bcb6a3b3cd17ce5db9aaf6bd3ec7a0ec1b44b979 100644 ---- a/src/main/java/net/minecraft/CrashReport.java -+++ b/src/main/java/net/minecraft/CrashReport.java -@@ -250,7 +250,7 @@ public class CrashReport { - } - - public static void preload() { -- MemoryReserve.allocate(); -+ // MemoryReserve.allocate(); // Paper - Disable memory reserve allocating - (new CrashReport("Don't panic!", new Throwable())).getFriendlyReport(); - } - } diff --git a/patches/server/0972-Fire-EntityDamageByEntityEvent-for-unowned-wither-sk.patch b/patches/server/0972-Fire-EntityDamageByEntityEvent-for-unowned-wither-sk.patch new file mode 100644 index 0000000000..5c5bb40964 --- /dev/null +++ b/patches/server/0972-Fire-EntityDamageByEntityEvent-for-unowned-wither-sk.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: RodneyMKay <36546810+RodneyMKay@users.noreply.github.com> +Date: Sun, 11 Feb 2024 20:05:11 +0100 +Subject: [PATCH] Fire EntityDamageByEntityEvent for unowned wither skulls + + +diff --git a/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java b/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java +index 729aa0125479ab839792a3f35c981046899421b5..02931861de955352e71d6f5ffc720a17304815fe 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java +@@ -72,7 +72,7 @@ public class WitherSkull extends AbstractHurtingProjectile { + } + } + } else { +- flag = entity.hurt(this.damageSources().magic(), 5.0F); ++ flag = entity.hurt(this.damageSources().magic().customCausingEntity(this), 5.0F); // Paper - Fire EntityDamageByEntityEvent for unowned wither skulls + } + + if (flag && entity instanceof LivingEntity) { diff --git a/patches/server/0973-Fire-EntityDamageByEntityEvent-for-unowned-wither-sk.patch b/patches/server/0973-Fire-EntityDamageByEntityEvent-for-unowned-wither-sk.patch deleted file mode 100644 index 5c5bb40964..0000000000 --- a/patches/server/0973-Fire-EntityDamageByEntityEvent-for-unowned-wither-sk.patch +++ /dev/null @@ -1,19 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: RodneyMKay <36546810+RodneyMKay@users.noreply.github.com> -Date: Sun, 11 Feb 2024 20:05:11 +0100 -Subject: [PATCH] Fire EntityDamageByEntityEvent for unowned wither skulls - - -diff --git a/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java b/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java -index 729aa0125479ab839792a3f35c981046899421b5..02931861de955352e71d6f5ffc720a17304815fe 100644 ---- a/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java -+++ b/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java -@@ -72,7 +72,7 @@ public class WitherSkull extends AbstractHurtingProjectile { - } - } - } else { -- flag = entity.hurt(this.damageSources().magic(), 5.0F); -+ flag = entity.hurt(this.damageSources().magic().customCausingEntity(this), 5.0F); // Paper - Fire EntityDamageByEntityEvent for unowned wither skulls - } - - if (flag && entity instanceof LivingEntity) { diff --git a/patches/server/0973-Fix-DamageSource-API.patch b/patches/server/0973-Fix-DamageSource-API.patch new file mode 100644 index 0000000000..0e5c644bc1 --- /dev/null +++ b/patches/server/0973-Fix-DamageSource-API.patch @@ -0,0 +1,204 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Sat, 9 Mar 2024 14:13:04 -0800 +Subject: [PATCH] Fix DamageSource API + +Uses the correct entity in the EntityDamageByEntity event +Returns the correct entity for API's DamageSource#getCausingEntity + +diff --git a/src/main/java/net/minecraft/world/damagesource/DamageSource.java b/src/main/java/net/minecraft/world/damagesource/DamageSource.java +index a78fd4f9ce97ebece45979908382de8f5fc14c1b..160dc3216e8f5db5f9b3cce5e2d655f2b35b208a 100644 +--- a/src/main/java/net/minecraft/world/damagesource/DamageSource.java ++++ b/src/main/java/net/minecraft/world/damagesource/DamageSource.java +@@ -28,7 +28,8 @@ public class DamageSource { + private boolean withSweep = false; + private boolean melting = false; + private boolean poison = false; +- private Entity customCausingEntity = null; // This field is a helper for when causing entity damage is not set by vanilla ++ @Nullable ++ private Entity customEventDamager = null; // This field is a helper for when causing entity damage is not set by vanilla // Paper - fix DamageSource API + + public DamageSource sweep() { + this.withSweep = true; +@@ -57,18 +58,18 @@ public class DamageSource { + return this.poison; + } + +- public Entity getCausingEntity() { +- return (this.customCausingEntity != null) ? this.customCausingEntity : this.causingEntity; ++ // Paper start - fix DamageSource API ++ public @Nullable Entity getCustomEventDamager() { ++ return (this.customEventDamager != null) ? this.customEventDamager : this.directEntity; + } + +- public DamageSource customCausingEntity(Entity entity) { +- // This method is not intended for change the causing entity if is already set +- // also is only necessary if the entity passed is not the direct entity or different from the current causingEntity +- if (this.customCausingEntity != null || this.directEntity == entity || this.causingEntity == entity) { +- return this; ++ public DamageSource customEventDamager(Entity entity) { ++ if (this.directEntity != null) { ++ throw new IllegalStateException("Cannot set custom event damager when direct entity is already set (report a bug to Paper)"); + } + DamageSource damageSource = this.cloneInstance(); +- damageSource.customCausingEntity = entity; ++ damageSource.customEventDamager = entity; ++ // Paper end - fix DamageSource API + return damageSource; + } + +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 4e96b6c05bf8b8235e91bdd26e5615c5299fd9aa..3c1bcf8d1a07b35a8688160c9f05f792451338a3 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -3195,7 +3195,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return; + } + +- if (!this.hurt(this.damageSources().lightningBolt().customCausingEntity(lightning), 5.0F)) { ++ if (!this.hurt(this.damageSources().lightningBolt().customEventDamager(lightning), 5.0F)) { // Paper - fix DamageSource API + return; + } + // CraftBukkit end +diff --git a/src/main/java/net/minecraft/world/entity/animal/Turtle.java b/src/main/java/net/minecraft/world/entity/animal/Turtle.java +index 3eabf1e94c0a1086f471e30bf523581907169c58..ffffa53fcaa6ec8681b283fa20bb5a3320ad9c11 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java +@@ -334,7 +334,7 @@ public class Turtle extends Animal { + + @Override + public void thunderHit(ServerLevel world, LightningBolt lightning) { +- this.hurt(this.damageSources().lightningBolt().customCausingEntity(lightning), Float.MAX_VALUE); // CraftBukkit ++ this.hurt(this.damageSources().lightningBolt().customEventDamager(lightning), Float.MAX_VALUE); // CraftBukkit // Paper - fix DamageSource API + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/decoration/HangingEntity.java b/src/main/java/net/minecraft/world/entity/decoration/HangingEntity.java +index 0ffff5329fa2c7833f9ec71528cb7f951cf78109..bf2d91bbb4bf401696f5f5d14a67e3920a179084 100644 +--- a/src/main/java/net/minecraft/world/entity/decoration/HangingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/decoration/HangingEntity.java +@@ -203,7 +203,7 @@ public abstract class HangingEntity extends Entity { + } else { + if (!this.isRemoved() && !this.level().isClientSide) { + // CraftBukkit start - fire break events +- Entity damager = (source.isIndirect()) ? source.getEntity() : source.getDirectEntity(); ++ Entity damager = (source.isIndirect() && source.getEntity() != null) ? source.getEntity() : source.getDirectEntity(); // Paper - fix DamageSource API + HangingBreakEvent event; + if (damager != null) { + event = new HangingBreakByEntityEvent((Hanging) this.getBukkitEntity(), damager.getBukkitEntity(), source.is(DamageTypeTags.IS_EXPLOSION) ? HangingBreakEvent.RemoveCause.EXPLOSION : HangingBreakEvent.RemoveCause.ENTITY); +diff --git a/src/main/java/net/minecraft/world/entity/monster/Creeper.java b/src/main/java/net/minecraft/world/entity/monster/Creeper.java +index 4b94e21d05d6deae75f0c2fb711e43a4c7d06f90..cbcb2bfa8f91099e5c374f590f48885390bdf7a7 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Creeper.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Creeper.java +@@ -271,7 +271,7 @@ public class Creeper extends Monster implements PowerableMob { + if (!event.isCancelled()) { + // CraftBukkit end + this.dead = true; +- this.level().explode(this, net.minecraft.world.level.Explosion.getDefaultDamageSource(this.level(), this).customCausingEntity(this.entityIgniter), null, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.MOB); // CraftBukkit ++ this.level().explode(this, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.MOB); // CraftBukkit // Paper - fix DamageSource API + this.discard(EntityRemoveEvent.Cause.EXPLODE); // CraftBukkit - add Bukkit remove cause + this.spawnLingeringCloud(); + // CraftBukkit start +diff --git a/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java b/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java +index eb764829bde34a835a151934267bfcf36d5239fa..f8aa723f18e28b02d4b7055404922ff8d7a73f8d 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java +@@ -133,7 +133,7 @@ public class EvokerFangs extends Entity implements TraceableEntity { + + if (target.isAlive() && !target.isInvulnerable() && target != entityliving1) { + if (entityliving1 == null) { +- target.hurt(this.damageSources().magic().customCausingEntity(this), 6.0F); // CraftBukkit ++ target.hurt(this.damageSources().magic().customEventDamager(this), 6.0F); // CraftBukkit // Paper - fix DamageSource API + } else { + if (entityliving1.isAlliedTo((Entity) target)) { + return; +diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +index 28690877c443ceb2bdf20e6d251c9d32f667814c..1fb1e729d6879568d8b4943071fa940325b2e5b0 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +@@ -86,7 +86,7 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + + entityplayer.connection.teleport(teleEvent.getTo()); + entity.resetFallDistance(); +- entity.hurt(this.damageSources().fall().customCausingEntity(this), 5.0F); // CraftBukkit ++ entity.hurt(this.damageSources().fall().customEventDamager(this), 5.0F); // CraftBukkit // Paper - fix DamageSource API + } + // CraftBukkit end + this.level().playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_TELEPORT, SoundSource.PLAYERS); +diff --git a/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java b/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java +index 02931861de955352e71d6f5ffc720a17304815fe..55b4b5ad5f084c9a271a716d076672478d6486ba 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java +@@ -72,7 +72,7 @@ public class WitherSkull extends AbstractHurtingProjectile { + } + } + } else { +- flag = entity.hurt(this.damageSources().magic().customCausingEntity(this), 5.0F); // Paper - Fire EntityDamageByEntityEvent for unowned wither skulls ++ flag = entity.hurt(this.damageSources().magic().customEventDamager(this), 5.0F); // Paper - Fire EntityDamageByEntityEvent for unowned wither skulls // Paper - fix DamageSource API + } + + if (flag && entity instanceof LivingEntity) { +diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java +index fbd1f4f3a8c94a4842f91b7cc6c21f0cf7edd7ac..550c0cf4d84afc0cd6b2675bdc6aa2282d87e312 100644 +--- a/src/main/java/net/minecraft/world/level/Explosion.java ++++ b/src/main/java/net/minecraft/world/level/Explosion.java +@@ -104,7 +104,7 @@ public class Explosion { + this.z = z; + this.fire = createFire; + this.blockInteraction = destructionType; +- this.damageSource = damageSource == null ? world.damageSources().explosion(this).customCausingEntity(entity) : damageSource.customCausingEntity(entity); // CraftBukkit - handle source entity ++ this.damageSource = damageSource == null ? world.damageSources().explosion(this) : damageSource; // CraftBukkit - handle source entity // Paper - revert to fix DamageSource API + this.damageCalculator = behavior == null ? this.makeDamageCalculator(entity) : behavior; + this.smallExplosionParticles = particle; + this.largeExplosionParticles = emitterParticle; +diff --git a/src/main/java/org/bukkit/craftbukkit/damage/CraftDamageSource.java b/src/main/java/org/bukkit/craftbukkit/damage/CraftDamageSource.java +index 6ae1ad21807c039726021f8f26f92042acce2fda..b7e2327c50195e8d3ca3ca3b47c7c0f9ea8e289c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/damage/CraftDamageSource.java ++++ b/src/main/java/org/bukkit/craftbukkit/damage/CraftDamageSource.java +@@ -41,7 +41,7 @@ public class CraftDamageSource implements DamageSource { + + @Override + public org.bukkit.entity.Entity getCausingEntity() { +- net.minecraft.world.entity.Entity entity = this.getHandle().getCausingEntity(); ++ net.minecraft.world.entity.Entity entity = this.getHandle().getEntity(); // Paper - fix DamageSource API + return (entity != null) ? entity.getBukkitEntity() : null; + } + +@@ -65,7 +65,7 @@ public class CraftDamageSource implements DamageSource { + + @Override + public boolean isIndirect() { +- return this.getHandle().getCausingEntity() != this.getHandle().getDirectEntity(); ++ return this.getHandle().isIndirect(); // Paper - fix DamageSource API + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/damage/CraftDamageSourceBuilder.java b/src/main/java/org/bukkit/craftbukkit/damage/CraftDamageSourceBuilder.java +index 4c6e15535fa40aad8cf1920f392589404f9ba79c..35eb95ef6fb6a0f7ea63351e90741c489fdd15f9 100644 +--- a/src/main/java/org/bukkit/craftbukkit/damage/CraftDamageSourceBuilder.java ++++ b/src/main/java/org/bukkit/craftbukkit/damage/CraftDamageSourceBuilder.java +@@ -41,6 +41,11 @@ public class CraftDamageSourceBuilder implements DamageSource.Builder { + + @Override + public DamageSource build() { ++ // Paper start - fix DamageCause API ++ if (this.causingEntity != null && this.directEntity == null) { ++ throw new IllegalArgumentException("Direct entity must be set if causing entity is set"); ++ } ++ // Paper end - fix DamageCause API + return CraftDamageSource.buildFromBukkit(this.damageType, this.causingEntity, this.directEntity, this.damageLocation); + } + } +diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +index 444287fea080182df533198f5c659533de86c5dc..544567e576023709f86abf57963c37a5490ab9f0 100644 +--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java ++++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +@@ -1082,7 +1082,7 @@ public class CraftEventFactory { + + private static EntityDamageEvent handleEntityDamageEvent(Entity entity, DamageSource source, Map modifiers, Map> modifierFunctions, boolean cancelled) { + CraftDamageSource bukkitDamageSource = new CraftDamageSource(source); +- Entity damager = (bukkitDamageSource.isIndirect() && source.getDirectEntity() != null) ? source.getDirectEntity() : source.getCausingEntity(); ++ final Entity damager = source.getCustomEventDamager(); // Paper - fix DamageSource API + if (source.is(DamageTypeTags.IS_EXPLOSION)) { + if (damager == null) { + return CraftEventFactory.callEntityDamageEvent(source.getDirectBlock(), entity, DamageCause.BLOCK_EXPLOSION, bukkitDamageSource, modifiers, modifierFunctions, cancelled, source.explodedBlockState); // Paper - Include BlockState for damage diff --git a/patches/server/0974-Fix-DamageSource-API.patch b/patches/server/0974-Fix-DamageSource-API.patch deleted file mode 100644 index 0e5c644bc1..0000000000 --- a/patches/server/0974-Fix-DamageSource-API.patch +++ /dev/null @@ -1,204 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Sat, 9 Mar 2024 14:13:04 -0800 -Subject: [PATCH] Fix DamageSource API - -Uses the correct entity in the EntityDamageByEntity event -Returns the correct entity for API's DamageSource#getCausingEntity - -diff --git a/src/main/java/net/minecraft/world/damagesource/DamageSource.java b/src/main/java/net/minecraft/world/damagesource/DamageSource.java -index a78fd4f9ce97ebece45979908382de8f5fc14c1b..160dc3216e8f5db5f9b3cce5e2d655f2b35b208a 100644 ---- a/src/main/java/net/minecraft/world/damagesource/DamageSource.java -+++ b/src/main/java/net/minecraft/world/damagesource/DamageSource.java -@@ -28,7 +28,8 @@ public class DamageSource { - private boolean withSweep = false; - private boolean melting = false; - private boolean poison = false; -- private Entity customCausingEntity = null; // This field is a helper for when causing entity damage is not set by vanilla -+ @Nullable -+ private Entity customEventDamager = null; // This field is a helper for when causing entity damage is not set by vanilla // Paper - fix DamageSource API - - public DamageSource sweep() { - this.withSweep = true; -@@ -57,18 +58,18 @@ public class DamageSource { - return this.poison; - } - -- public Entity getCausingEntity() { -- return (this.customCausingEntity != null) ? this.customCausingEntity : this.causingEntity; -+ // Paper start - fix DamageSource API -+ public @Nullable Entity getCustomEventDamager() { -+ return (this.customEventDamager != null) ? this.customEventDamager : this.directEntity; - } - -- public DamageSource customCausingEntity(Entity entity) { -- // This method is not intended for change the causing entity if is already set -- // also is only necessary if the entity passed is not the direct entity or different from the current causingEntity -- if (this.customCausingEntity != null || this.directEntity == entity || this.causingEntity == entity) { -- return this; -+ public DamageSource customEventDamager(Entity entity) { -+ if (this.directEntity != null) { -+ throw new IllegalStateException("Cannot set custom event damager when direct entity is already set (report a bug to Paper)"); - } - DamageSource damageSource = this.cloneInstance(); -- damageSource.customCausingEntity = entity; -+ damageSource.customEventDamager = entity; -+ // Paper end - fix DamageSource API - return damageSource; - } - -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 4e96b6c05bf8b8235e91bdd26e5615c5299fd9aa..3c1bcf8d1a07b35a8688160c9f05f792451338a3 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -3195,7 +3195,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - return; - } - -- if (!this.hurt(this.damageSources().lightningBolt().customCausingEntity(lightning), 5.0F)) { -+ if (!this.hurt(this.damageSources().lightningBolt().customEventDamager(lightning), 5.0F)) { // Paper - fix DamageSource API - return; - } - // CraftBukkit end -diff --git a/src/main/java/net/minecraft/world/entity/animal/Turtle.java b/src/main/java/net/minecraft/world/entity/animal/Turtle.java -index 3eabf1e94c0a1086f471e30bf523581907169c58..ffffa53fcaa6ec8681b283fa20bb5a3320ad9c11 100644 ---- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java -+++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java -@@ -334,7 +334,7 @@ public class Turtle extends Animal { - - @Override - public void thunderHit(ServerLevel world, LightningBolt lightning) { -- this.hurt(this.damageSources().lightningBolt().customCausingEntity(lightning), Float.MAX_VALUE); // CraftBukkit -+ this.hurt(this.damageSources().lightningBolt().customEventDamager(lightning), Float.MAX_VALUE); // CraftBukkit // Paper - fix DamageSource API - } - - @Override -diff --git a/src/main/java/net/minecraft/world/entity/decoration/HangingEntity.java b/src/main/java/net/minecraft/world/entity/decoration/HangingEntity.java -index 0ffff5329fa2c7833f9ec71528cb7f951cf78109..bf2d91bbb4bf401696f5f5d14a67e3920a179084 100644 ---- a/src/main/java/net/minecraft/world/entity/decoration/HangingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/decoration/HangingEntity.java -@@ -203,7 +203,7 @@ public abstract class HangingEntity extends Entity { - } else { - if (!this.isRemoved() && !this.level().isClientSide) { - // CraftBukkit start - fire break events -- Entity damager = (source.isIndirect()) ? source.getEntity() : source.getDirectEntity(); -+ Entity damager = (source.isIndirect() && source.getEntity() != null) ? source.getEntity() : source.getDirectEntity(); // Paper - fix DamageSource API - HangingBreakEvent event; - if (damager != null) { - event = new HangingBreakByEntityEvent((Hanging) this.getBukkitEntity(), damager.getBukkitEntity(), source.is(DamageTypeTags.IS_EXPLOSION) ? HangingBreakEvent.RemoveCause.EXPLOSION : HangingBreakEvent.RemoveCause.ENTITY); -diff --git a/src/main/java/net/minecraft/world/entity/monster/Creeper.java b/src/main/java/net/minecraft/world/entity/monster/Creeper.java -index 4b94e21d05d6deae75f0c2fb711e43a4c7d06f90..cbcb2bfa8f91099e5c374f590f48885390bdf7a7 100644 ---- a/src/main/java/net/minecraft/world/entity/monster/Creeper.java -+++ b/src/main/java/net/minecraft/world/entity/monster/Creeper.java -@@ -271,7 +271,7 @@ public class Creeper extends Monster implements PowerableMob { - if (!event.isCancelled()) { - // CraftBukkit end - this.dead = true; -- this.level().explode(this, net.minecraft.world.level.Explosion.getDefaultDamageSource(this.level(), this).customCausingEntity(this.entityIgniter), null, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.MOB); // CraftBukkit -+ this.level().explode(this, this.getX(), this.getY(), this.getZ(), event.getRadius(), event.getFire(), Level.ExplosionInteraction.MOB); // CraftBukkit // Paper - fix DamageSource API - this.discard(EntityRemoveEvent.Cause.EXPLODE); // CraftBukkit - add Bukkit remove cause - this.spawnLingeringCloud(); - // CraftBukkit start -diff --git a/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java b/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java -index eb764829bde34a835a151934267bfcf36d5239fa..f8aa723f18e28b02d4b7055404922ff8d7a73f8d 100644 ---- a/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java -+++ b/src/main/java/net/minecraft/world/entity/projectile/EvokerFangs.java -@@ -133,7 +133,7 @@ public class EvokerFangs extends Entity implements TraceableEntity { - - if (target.isAlive() && !target.isInvulnerable() && target != entityliving1) { - if (entityliving1 == null) { -- target.hurt(this.damageSources().magic().customCausingEntity(this), 6.0F); // CraftBukkit -+ target.hurt(this.damageSources().magic().customEventDamager(this), 6.0F); // CraftBukkit // Paper - fix DamageSource API - } else { - if (entityliving1.isAlliedTo((Entity) target)) { - return; -diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java -index 28690877c443ceb2bdf20e6d251c9d32f667814c..1fb1e729d6879568d8b4943071fa940325b2e5b0 100644 ---- a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java -+++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java -@@ -86,7 +86,7 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { - - entityplayer.connection.teleport(teleEvent.getTo()); - entity.resetFallDistance(); -- entity.hurt(this.damageSources().fall().customCausingEntity(this), 5.0F); // CraftBukkit -+ entity.hurt(this.damageSources().fall().customEventDamager(this), 5.0F); // CraftBukkit // Paper - fix DamageSource API - } - // CraftBukkit end - this.level().playSound((Player) null, this.getX(), this.getY(), this.getZ(), SoundEvents.PLAYER_TELEPORT, SoundSource.PLAYERS); -diff --git a/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java b/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java -index 02931861de955352e71d6f5ffc720a17304815fe..55b4b5ad5f084c9a271a716d076672478d6486ba 100644 ---- a/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java -+++ b/src/main/java/net/minecraft/world/entity/projectile/WitherSkull.java -@@ -72,7 +72,7 @@ public class WitherSkull extends AbstractHurtingProjectile { - } - } - } else { -- flag = entity.hurt(this.damageSources().magic().customCausingEntity(this), 5.0F); // Paper - Fire EntityDamageByEntityEvent for unowned wither skulls -+ flag = entity.hurt(this.damageSources().magic().customEventDamager(this), 5.0F); // Paper - Fire EntityDamageByEntityEvent for unowned wither skulls // Paper - fix DamageSource API - } - - if (flag && entity instanceof LivingEntity) { -diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java -index fbd1f4f3a8c94a4842f91b7cc6c21f0cf7edd7ac..550c0cf4d84afc0cd6b2675bdc6aa2282d87e312 100644 ---- a/src/main/java/net/minecraft/world/level/Explosion.java -+++ b/src/main/java/net/minecraft/world/level/Explosion.java -@@ -104,7 +104,7 @@ public class Explosion { - this.z = z; - this.fire = createFire; - this.blockInteraction = destructionType; -- this.damageSource = damageSource == null ? world.damageSources().explosion(this).customCausingEntity(entity) : damageSource.customCausingEntity(entity); // CraftBukkit - handle source entity -+ this.damageSource = damageSource == null ? world.damageSources().explosion(this) : damageSource; // CraftBukkit - handle source entity // Paper - revert to fix DamageSource API - this.damageCalculator = behavior == null ? this.makeDamageCalculator(entity) : behavior; - this.smallExplosionParticles = particle; - this.largeExplosionParticles = emitterParticle; -diff --git a/src/main/java/org/bukkit/craftbukkit/damage/CraftDamageSource.java b/src/main/java/org/bukkit/craftbukkit/damage/CraftDamageSource.java -index 6ae1ad21807c039726021f8f26f92042acce2fda..b7e2327c50195e8d3ca3ca3b47c7c0f9ea8e289c 100644 ---- a/src/main/java/org/bukkit/craftbukkit/damage/CraftDamageSource.java -+++ b/src/main/java/org/bukkit/craftbukkit/damage/CraftDamageSource.java -@@ -41,7 +41,7 @@ public class CraftDamageSource implements DamageSource { - - @Override - public org.bukkit.entity.Entity getCausingEntity() { -- net.minecraft.world.entity.Entity entity = this.getHandle().getCausingEntity(); -+ net.minecraft.world.entity.Entity entity = this.getHandle().getEntity(); // Paper - fix DamageSource API - return (entity != null) ? entity.getBukkitEntity() : null; - } - -@@ -65,7 +65,7 @@ public class CraftDamageSource implements DamageSource { - - @Override - public boolean isIndirect() { -- return this.getHandle().getCausingEntity() != this.getHandle().getDirectEntity(); -+ return this.getHandle().isIndirect(); // Paper - fix DamageSource API - } - - @Override -diff --git a/src/main/java/org/bukkit/craftbukkit/damage/CraftDamageSourceBuilder.java b/src/main/java/org/bukkit/craftbukkit/damage/CraftDamageSourceBuilder.java -index 4c6e15535fa40aad8cf1920f392589404f9ba79c..35eb95ef6fb6a0f7ea63351e90741c489fdd15f9 100644 ---- a/src/main/java/org/bukkit/craftbukkit/damage/CraftDamageSourceBuilder.java -+++ b/src/main/java/org/bukkit/craftbukkit/damage/CraftDamageSourceBuilder.java -@@ -41,6 +41,11 @@ public class CraftDamageSourceBuilder implements DamageSource.Builder { - - @Override - public DamageSource build() { -+ // Paper start - fix DamageCause API -+ if (this.causingEntity != null && this.directEntity == null) { -+ throw new IllegalArgumentException("Direct entity must be set if causing entity is set"); -+ } -+ // Paper end - fix DamageCause API - return CraftDamageSource.buildFromBukkit(this.damageType, this.causingEntity, this.directEntity, this.damageLocation); - } - } -diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java -index 444287fea080182df533198f5c659533de86c5dc..544567e576023709f86abf57963c37a5490ab9f0 100644 ---- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java -+++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java -@@ -1082,7 +1082,7 @@ public class CraftEventFactory { - - private static EntityDamageEvent handleEntityDamageEvent(Entity entity, DamageSource source, Map modifiers, Map> modifierFunctions, boolean cancelled) { - CraftDamageSource bukkitDamageSource = new CraftDamageSource(source); -- Entity damager = (bukkitDamageSource.isIndirect() && source.getDirectEntity() != null) ? source.getDirectEntity() : source.getCausingEntity(); -+ final Entity damager = source.getCustomEventDamager(); // Paper - fix DamageSource API - if (source.is(DamageTypeTags.IS_EXPLOSION)) { - if (damager == null) { - return CraftEventFactory.callEntityDamageEvent(source.getDirectBlock(), entity, DamageCause.BLOCK_EXPLOSION, bukkitDamageSource, modifiers, modifierFunctions, cancelled, source.explodedBlockState); // Paper - Include BlockState for damage diff --git a/patches/server/0974-Fix-creation-of-invalid-block-entity-during-world-ge.patch b/patches/server/0974-Fix-creation-of-invalid-block-entity-during-world-ge.patch new file mode 100644 index 0000000000..a745081446 --- /dev/null +++ b/patches/server/0974-Fix-creation-of-invalid-block-entity-during-world-ge.patch @@ -0,0 +1,40 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Pierpaolo Coletta +Date: Sat, 30 Mar 2024 21:06:10 +0100 +Subject: [PATCH] Fix creation of invalid block entity during world generation + + +diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +index a59eece9c7a8c33cb8ce963906e993c3462684fb..333a02e08cccf5cb0efa2076582cbd69e95ff0c0 100644 +--- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java ++++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +@@ -339,6 +339,7 @@ public class WorldGenRegion implements WorldGenLevel { + ichunkaccess.removeBlockEntity(pos); + } + } else { ++ ichunkaccess.removeBlockEntity(pos); // Paper - Clear the block entity before setting up a DUMMY block entity + CompoundTag nbttagcompound = new CompoundTag(); + + nbttagcompound.putInt("x", pos.getX()); +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +index 744db9eec4f7bdeb32f83300960a7fce63b393d8..8de6ad8b131061b2dae440dff71e2e6e7af2de39 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -1071,9 +1071,14 @@ public class LevelChunk extends ChunkAccess { + if (this.blockEntity.getType().isValid(iblockdata)) { + this.ticker.tick(LevelChunk.this.level, this.blockEntity.getBlockPos(), iblockdata, this.blockEntity); + this.loggedInvalidBlockState = false; +- } else if (!this.loggedInvalidBlockState) { +- this.loggedInvalidBlockState = true; +- LevelChunk.LOGGER.warn("Block entity {} @ {} state {} invalid for ticking:", new Object[]{LogUtils.defer(this::getType), LogUtils.defer(this::getPos), iblockdata}); ++ // Paper start - Remove the Block Entity if it's invalid ++ } else { ++ LevelChunk.this.removeBlockEntity(this.getPos()); ++ if (!this.loggedInvalidBlockState) { ++ this.loggedInvalidBlockState = true; ++ LevelChunk.LOGGER.warn("Block entity {} @ {} state {} invalid for ticking:", new Object[]{LogUtils.defer(this::getType), LogUtils.defer(this::getPos), iblockdata}); ++ } ++ // Paper end - Remove the Block Entity if it's invalid + } + + gameprofilerfiller.pop(); diff --git a/patches/server/0975-Fix-creation-of-invalid-block-entity-during-world-ge.patch b/patches/server/0975-Fix-creation-of-invalid-block-entity-during-world-ge.patch deleted file mode 100644 index a745081446..0000000000 --- a/patches/server/0975-Fix-creation-of-invalid-block-entity-during-world-ge.patch +++ /dev/null @@ -1,40 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Pierpaolo Coletta -Date: Sat, 30 Mar 2024 21:06:10 +0100 -Subject: [PATCH] Fix creation of invalid block entity during world generation - - -diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java -index a59eece9c7a8c33cb8ce963906e993c3462684fb..333a02e08cccf5cb0efa2076582cbd69e95ff0c0 100644 ---- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java -+++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java -@@ -339,6 +339,7 @@ public class WorldGenRegion implements WorldGenLevel { - ichunkaccess.removeBlockEntity(pos); - } - } else { -+ ichunkaccess.removeBlockEntity(pos); // Paper - Clear the block entity before setting up a DUMMY block entity - CompoundTag nbttagcompound = new CompoundTag(); - - nbttagcompound.putInt("x", pos.getX()); -diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -index 744db9eec4f7bdeb32f83300960a7fce63b393d8..8de6ad8b131061b2dae440dff71e2e6e7af2de39 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -@@ -1071,9 +1071,14 @@ public class LevelChunk extends ChunkAccess { - if (this.blockEntity.getType().isValid(iblockdata)) { - this.ticker.tick(LevelChunk.this.level, this.blockEntity.getBlockPos(), iblockdata, this.blockEntity); - this.loggedInvalidBlockState = false; -- } else if (!this.loggedInvalidBlockState) { -- this.loggedInvalidBlockState = true; -- LevelChunk.LOGGER.warn("Block entity {} @ {} state {} invalid for ticking:", new Object[]{LogUtils.defer(this::getType), LogUtils.defer(this::getPos), iblockdata}); -+ // Paper start - Remove the Block Entity if it's invalid -+ } else { -+ LevelChunk.this.removeBlockEntity(this.getPos()); -+ if (!this.loggedInvalidBlockState) { -+ this.loggedInvalidBlockState = true; -+ LevelChunk.LOGGER.warn("Block entity {} @ {} state {} invalid for ticking:", new Object[]{LogUtils.defer(this::getType), LogUtils.defer(this::getPos), iblockdata}); -+ } -+ // Paper end - Remove the Block Entity if it's invalid - } - - gameprofilerfiller.pop(); diff --git a/patches/server/0975-Fix-possible-StackOverflowError-for-some-dispenses.patch b/patches/server/0975-Fix-possible-StackOverflowError-for-some-dispenses.patch new file mode 100644 index 0000000000..21910b922f --- /dev/null +++ b/patches/server/0975-Fix-possible-StackOverflowError-for-some-dispenses.patch @@ -0,0 +1,108 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Sat, 29 Oct 2022 17:02:42 -0700 +Subject: [PATCH] Fix possible StackOverflowError for some dispenses + +For saddles, carpets, horse armor, and chests for horse-likes +a BlockDispenseEvent handler that always mutated the item without +changing the type would result in a SO error because when it went +to find the replacement dispense behavior (since the item "changed") +it didn't properly handle if the replacement was the same instance +of dispense behavior. + +Additionally equippable mob heads, wither skulls, and carved pumpkins +are subject to the same possible error. + +diff --git a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +index 4fa838bf97ede6e1c893ba64f53aa7af5db0405a..32084df594649d8da04052bbfa111896d8ea1f91 100644 +--- a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +@@ -238,7 +238,7 @@ public interface DispenseItemBehavior { + // Chain to handler for new item + ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); + DispenseItemBehavior idispensebehavior = (DispenseItemBehavior) DispenserBlock.DISPENSER_REGISTRY.get(eventStack.getItem()); +- if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != ArmorItem.DISPENSE_ITEM_BEHAVIOR) { ++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) { // Paper - fix possible StackOverflowError + idispensebehavior.dispense(pointer, eventStack); + return stack; + } +@@ -294,7 +294,7 @@ public interface DispenseItemBehavior { + // Chain to handler for new item + ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); + DispenseItemBehavior idispensebehavior = (DispenseItemBehavior) DispenserBlock.DISPENSER_REGISTRY.get(eventStack.getItem()); +- if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != ArmorItem.DISPENSE_ITEM_BEHAVIOR) { ++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) { // Paper - fix possible StackOverflowError + idispensebehavior.dispense(pointer, eventStack); + return stack; + } +@@ -368,7 +368,7 @@ public interface DispenseItemBehavior { + // Chain to handler for new item + ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); + DispenseItemBehavior idispensebehavior = (DispenseItemBehavior) DispenserBlock.DISPENSER_REGISTRY.get(eventStack.getItem()); +- if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != ArmorItem.DISPENSE_ITEM_BEHAVIOR) { ++ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) { // Paper - fix possible StackOverflowError + idispensebehavior.dispense(pointer, eventStack); + return stack; + } +@@ -710,7 +710,7 @@ public interface DispenseItemBehavior { + OptionalDispenseItemBehavior dispensebehaviormaybe1 = new OptionalDispenseItemBehavior() { + @Override + protected ItemStack execute(BlockSource pointer, ItemStack stack) { +- this.setSuccess(ArmorItem.dispenseArmor(pointer, stack)); ++ this.setSuccess(ArmorItem.dispenseArmor(pointer, stack, this)); // Paper - fix possible StackOverflowError + return stack; + } + }; +@@ -764,7 +764,7 @@ public interface DispenseItemBehavior { + stack.shrink(1); + this.setSuccess(true); + } else { +- this.setSuccess(ArmorItem.dispenseArmor(pointer, stack)); ++ this.setSuccess(ArmorItem.dispenseArmor(pointer, stack, this)); // Paper - fix possible StackOverflowError + } + + return stack; +@@ -810,7 +810,7 @@ public interface DispenseItemBehavior { + stack.shrink(1); + this.setSuccess(true); + } else { +- this.setSuccess(ArmorItem.dispenseArmor(pointer, stack)); ++ this.setSuccess(ArmorItem.dispenseArmor(pointer, stack, this)); // Paper - fix possible StackOverflowError + } + + return stack; +diff --git a/src/main/java/net/minecraft/world/item/ArmorItem.java b/src/main/java/net/minecraft/world/item/ArmorItem.java +index e766397aae3f73548b290b0809b9d1ca0967ea39..786e4a8700cb84b16dd9b8892a0d1d5803924d81 100644 +--- a/src/main/java/net/minecraft/world/item/ArmorItem.java ++++ b/src/main/java/net/minecraft/world/item/ArmorItem.java +@@ -48,14 +48,20 @@ public class ArmorItem extends Item implements Equipable { + public static final DispenseItemBehavior DISPENSE_ITEM_BEHAVIOR = new DefaultDispenseItemBehavior() { + @Override + protected ItemStack execute(BlockSource pointer, ItemStack stack) { +- return ArmorItem.dispenseArmor(pointer, stack) ? stack : super.execute(pointer, stack); ++ return ArmorItem.dispenseArmor(pointer, stack, this) ? stack : super.execute(pointer, stack); // Paper - fix possible StackOverflowError + } + }; + protected final ArmorItem.Type type; + protected final Holder material; + private final Supplier defaultModifiers; + ++ @Deprecated @io.papermc.paper.annotation.DoNotUse // Paper + public static boolean dispenseArmor(BlockSource pointer, ItemStack armor) { ++ // Paper start ++ return dispenseArmor(pointer, armor, null); ++ } ++ public static boolean dispenseArmor(BlockSource pointer, ItemStack armor, @javax.annotation.Nullable DispenseItemBehavior currentBehavior) { ++ // Paper end + BlockPos blockposition = pointer.pos().relative((Direction) pointer.state().getValue(DispenserBlock.FACING)); + List list = pointer.level().getEntitiesOfClass(LivingEntity.class, new AABB(blockposition), EntitySelector.NO_SPECTATORS.and(new EntitySelector.MobCanWearArmorEntitySelector(armor))); + +@@ -86,7 +92,7 @@ public class ArmorItem extends Item implements Equipable { + // Chain to handler for new item + ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); + DispenseItemBehavior idispensebehavior = (DispenseItemBehavior) DispenserBlock.DISPENSER_REGISTRY.get(eventStack.getItem()); +- if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != ArmorItem.DISPENSE_ITEM_BEHAVIOR) { ++ if (idispensebehavior != DispenseItemBehavior.NOOP && (currentBehavior == null || idispensebehavior != currentBehavior)) { // Paper - fix possible StackOverflowError + idispensebehavior.dispense(pointer, eventStack); + return true; + } diff --git a/patches/server/0976-Fix-possible-StackOverflowError-for-some-dispenses.patch b/patches/server/0976-Fix-possible-StackOverflowError-for-some-dispenses.patch deleted file mode 100644 index 21910b922f..0000000000 --- a/patches/server/0976-Fix-possible-StackOverflowError-for-some-dispenses.patch +++ /dev/null @@ -1,108 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Sat, 29 Oct 2022 17:02:42 -0700 -Subject: [PATCH] Fix possible StackOverflowError for some dispenses - -For saddles, carpets, horse armor, and chests for horse-likes -a BlockDispenseEvent handler that always mutated the item without -changing the type would result in a SO error because when it went -to find the replacement dispense behavior (since the item "changed") -it didn't properly handle if the replacement was the same instance -of dispense behavior. - -Additionally equippable mob heads, wither skulls, and carved pumpkins -are subject to the same possible error. - -diff --git a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java -index 4fa838bf97ede6e1c893ba64f53aa7af5db0405a..32084df594649d8da04052bbfa111896d8ea1f91 100644 ---- a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java -+++ b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java -@@ -238,7 +238,7 @@ public interface DispenseItemBehavior { - // Chain to handler for new item - ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); - DispenseItemBehavior idispensebehavior = (DispenseItemBehavior) DispenserBlock.DISPENSER_REGISTRY.get(eventStack.getItem()); -- if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != ArmorItem.DISPENSE_ITEM_BEHAVIOR) { -+ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) { // Paper - fix possible StackOverflowError - idispensebehavior.dispense(pointer, eventStack); - return stack; - } -@@ -294,7 +294,7 @@ public interface DispenseItemBehavior { - // Chain to handler for new item - ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); - DispenseItemBehavior idispensebehavior = (DispenseItemBehavior) DispenserBlock.DISPENSER_REGISTRY.get(eventStack.getItem()); -- if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != ArmorItem.DISPENSE_ITEM_BEHAVIOR) { -+ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) { // Paper - fix possible StackOverflowError - idispensebehavior.dispense(pointer, eventStack); - return stack; - } -@@ -368,7 +368,7 @@ public interface DispenseItemBehavior { - // Chain to handler for new item - ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); - DispenseItemBehavior idispensebehavior = (DispenseItemBehavior) DispenserBlock.DISPENSER_REGISTRY.get(eventStack.getItem()); -- if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != ArmorItem.DISPENSE_ITEM_BEHAVIOR) { -+ if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != this) { // Paper - fix possible StackOverflowError - idispensebehavior.dispense(pointer, eventStack); - return stack; - } -@@ -710,7 +710,7 @@ public interface DispenseItemBehavior { - OptionalDispenseItemBehavior dispensebehaviormaybe1 = new OptionalDispenseItemBehavior() { - @Override - protected ItemStack execute(BlockSource pointer, ItemStack stack) { -- this.setSuccess(ArmorItem.dispenseArmor(pointer, stack)); -+ this.setSuccess(ArmorItem.dispenseArmor(pointer, stack, this)); // Paper - fix possible StackOverflowError - return stack; - } - }; -@@ -764,7 +764,7 @@ public interface DispenseItemBehavior { - stack.shrink(1); - this.setSuccess(true); - } else { -- this.setSuccess(ArmorItem.dispenseArmor(pointer, stack)); -+ this.setSuccess(ArmorItem.dispenseArmor(pointer, stack, this)); // Paper - fix possible StackOverflowError - } - - return stack; -@@ -810,7 +810,7 @@ public interface DispenseItemBehavior { - stack.shrink(1); - this.setSuccess(true); - } else { -- this.setSuccess(ArmorItem.dispenseArmor(pointer, stack)); -+ this.setSuccess(ArmorItem.dispenseArmor(pointer, stack, this)); // Paper - fix possible StackOverflowError - } - - return stack; -diff --git a/src/main/java/net/minecraft/world/item/ArmorItem.java b/src/main/java/net/minecraft/world/item/ArmorItem.java -index e766397aae3f73548b290b0809b9d1ca0967ea39..786e4a8700cb84b16dd9b8892a0d1d5803924d81 100644 ---- a/src/main/java/net/minecraft/world/item/ArmorItem.java -+++ b/src/main/java/net/minecraft/world/item/ArmorItem.java -@@ -48,14 +48,20 @@ public class ArmorItem extends Item implements Equipable { - public static final DispenseItemBehavior DISPENSE_ITEM_BEHAVIOR = new DefaultDispenseItemBehavior() { - @Override - protected ItemStack execute(BlockSource pointer, ItemStack stack) { -- return ArmorItem.dispenseArmor(pointer, stack) ? stack : super.execute(pointer, stack); -+ return ArmorItem.dispenseArmor(pointer, stack, this) ? stack : super.execute(pointer, stack); // Paper - fix possible StackOverflowError - } - }; - protected final ArmorItem.Type type; - protected final Holder material; - private final Supplier defaultModifiers; - -+ @Deprecated @io.papermc.paper.annotation.DoNotUse // Paper - public static boolean dispenseArmor(BlockSource pointer, ItemStack armor) { -+ // Paper start -+ return dispenseArmor(pointer, armor, null); -+ } -+ public static boolean dispenseArmor(BlockSource pointer, ItemStack armor, @javax.annotation.Nullable DispenseItemBehavior currentBehavior) { -+ // Paper end - BlockPos blockposition = pointer.pos().relative((Direction) pointer.state().getValue(DispenserBlock.FACING)); - List list = pointer.level().getEntitiesOfClass(LivingEntity.class, new AABB(blockposition), EntitySelector.NO_SPECTATORS.and(new EntitySelector.MobCanWearArmorEntitySelector(armor))); - -@@ -86,7 +92,7 @@ public class ArmorItem extends Item implements Equipable { - // Chain to handler for new item - ItemStack eventStack = CraftItemStack.asNMSCopy(event.getItem()); - DispenseItemBehavior idispensebehavior = (DispenseItemBehavior) DispenserBlock.DISPENSER_REGISTRY.get(eventStack.getItem()); -- if (idispensebehavior != DispenseItemBehavior.NOOP && idispensebehavior != ArmorItem.DISPENSE_ITEM_BEHAVIOR) { -+ if (idispensebehavior != DispenseItemBehavior.NOOP && (currentBehavior == null || idispensebehavior != currentBehavior)) { // Paper - fix possible StackOverflowError - idispensebehavior.dispense(pointer, eventStack); - return true; - } diff --git a/patches/server/0976-Improve-tag-parser-handling.patch b/patches/server/0976-Improve-tag-parser-handling.patch new file mode 100644 index 0000000000..6f346ac4f2 --- /dev/null +++ b/patches/server/0976-Improve-tag-parser-handling.patch @@ -0,0 +1,202 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Nassim Jahnke +Date: Mon, 5 Feb 2024 11:54:04 +0100 +Subject: [PATCH] Improve tag parser handling + + +diff --git a/src/main/java/com/mojang/brigadier/CommandDispatcher.java b/src/main/java/com/mojang/brigadier/CommandDispatcher.java +index 92848b64a78fce7a92e1657c2da6fc5ee53eea44..4b4f812eb13d5f03bcf3f8724d8aa8dbbc724e8b 100644 +--- a/src/main/java/com/mojang/brigadier/CommandDispatcher.java ++++ b/src/main/java/com/mojang/brigadier/CommandDispatcher.java +@@ -304,9 +304,15 @@ public class CommandDispatcher { + } + final CommandContextBuilder context = contextSoFar.copy(); + final StringReader reader = new StringReader(originalReader); ++ boolean stop = false; // Paper - Handle non-recoverable exceptions + try { + try { + child.parse(reader, context); ++ // Paper start - Handle non-recoverable exceptions ++ } catch (final io.papermc.paper.brigadier.TagParseCommandSyntaxException e) { ++ stop = true; ++ throw e; ++ // Paper end - Handle non-recoverable exceptions + } catch (final RuntimeException ex) { + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherParseException().createWithContext(reader, ex.getMessage()); + } +@@ -321,6 +327,7 @@ public class CommandDispatcher { + } + errors.put(child, ex); + reader.setCursor(cursor); ++ if (stop) return new ParseResults<>(contextSoFar, originalReader, errors); // Paper - Handle non-recoverable exceptions + continue; + } + +diff --git a/src/main/java/io/papermc/paper/brigadier/TagParseCommandSyntaxException.java b/src/main/java/io/papermc/paper/brigadier/TagParseCommandSyntaxException.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a375ad4ba9db990b24a2b9ff366fcba66b753815 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/brigadier/TagParseCommandSyntaxException.java +@@ -0,0 +1,15 @@ ++package io.papermc.paper.brigadier; ++ ++import com.mojang.brigadier.LiteralMessage; ++import com.mojang.brigadier.exceptions.CommandSyntaxException; ++import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; ++import net.minecraft.network.chat.Component; ++ ++public final class TagParseCommandSyntaxException extends CommandSyntaxException { ++ ++ private static final SimpleCommandExceptionType EXCEPTION_TYPE = new SimpleCommandExceptionType(new LiteralMessage("Error parsing NBT")); ++ ++ public TagParseCommandSyntaxException(final String message) { ++ super(EXCEPTION_TYPE, Component.literal(message)); ++ } ++} +diff --git a/src/main/java/net/minecraft/nbt/TagParser.java b/src/main/java/net/minecraft/nbt/TagParser.java +index da101bca71f4710812621b98f0a0d8cab180346a..3cd112584accb8e8f050ac99738eed11c902e60e 100644 +--- a/src/main/java/net/minecraft/nbt/TagParser.java ++++ b/src/main/java/net/minecraft/nbt/TagParser.java +@@ -49,6 +49,7 @@ public class TagParser { + }, CompoundTag::toString); + public static final Codec LENIENT_CODEC = Codec.withAlternative(AS_CODEC, CompoundTag.CODEC); + private final StringReader reader; ++ private int depth; // Paper + + public static CompoundTag parseTag(String string) throws CommandSyntaxException { + return new TagParser(new StringReader(string)).readSingleStruct(); +@@ -159,6 +160,7 @@ public class TagParser { + + public CompoundTag readStruct() throws CommandSyntaxException { + this.expect('{'); ++ this.increaseDepth(); // Paper + CompoundTag compoundTag = new CompoundTag(); + this.reader.skipWhitespace(); + +@@ -182,6 +184,7 @@ public class TagParser { + } + + this.expect('}'); ++ this.depth--; // Paper + return compoundTag; + } + +@@ -191,6 +194,7 @@ public class TagParser { + if (!this.reader.canRead()) { + throw ERROR_EXPECTED_VALUE.createWithContext(this.reader); + } else { ++ this.increaseDepth(); // Paper + ListTag listTag = new ListTag(); + TagType tagType = null; + +@@ -216,6 +220,7 @@ public class TagParser { + } + + this.expect(']'); ++ this.depth--; // Paper + return listTag; + } + } +@@ -288,4 +293,11 @@ public class TagParser { + this.reader.skipWhitespace(); + this.reader.expect(c); + } ++ ++ private void increaseDepth() throws CommandSyntaxException { ++ this.depth++; ++ if (this.depth > 512) { ++ throw new io.papermc.paper.brigadier.TagParseCommandSyntaxException("NBT tag is too complex, depth > 512"); ++ } ++ } + } +diff --git a/src/main/java/net/minecraft/network/chat/contents/TranslatableContents.java b/src/main/java/net/minecraft/network/chat/contents/TranslatableContents.java +index 56e641bc5f6edc657647993ea2efbb7bb9c2f732..4aa6232bf0f72fcde32d257100bd15b1c5192aaa 100644 +--- a/src/main/java/net/minecraft/network/chat/contents/TranslatableContents.java ++++ b/src/main/java/net/minecraft/network/chat/contents/TranslatableContents.java +@@ -181,6 +181,15 @@ public class TranslatableContents implements ComponentContents { + + @Override + public Optional visit(FormattedText.ContentConsumer visitor) { ++ // Paper start - Count visited parts ++ try { ++ return this.visit(new TranslatableContentConsumer<>(visitor)); ++ } catch (IllegalArgumentException ignored) { ++ return visitor.accept("..."); ++ } ++ } ++ private Optional visit(TranslatableContentConsumer visitor) { ++ // Paper end - Count visited parts + this.decompose(); + + for (FormattedText formattedText : this.decomposedParts) { +@@ -192,6 +201,25 @@ public class TranslatableContents implements ComponentContents { + + return Optional.empty(); + } ++ // Paper start - Count visited parts ++ private static final class TranslatableContentConsumer implements FormattedText.ContentConsumer { ++ private static final IllegalArgumentException EX = new IllegalArgumentException("Too long"); ++ private final FormattedText.ContentConsumer visitor; ++ private int visited; ++ ++ private TranslatableContentConsumer(FormattedText.ContentConsumer visitor) { ++ this.visitor = visitor; ++ } ++ ++ @Override ++ public Optional accept(final String asString) { ++ if (visited++ > 32) { ++ throw EX; ++ } ++ return this.visitor.accept(asString); ++ } ++ } ++ // Paper end - Count visited parts + + @Override + public MutableComponent resolve(@Nullable CommandSourceStack source, @Nullable Entity sender, int depth) throws CommandSyntaxException { +diff --git a/src/main/java/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java b/src/main/java/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java +index 898b19887ed34c87003fc63cb5905df2ba6234a5..b47eeb23055b135d5567552ba983bfbc3e1fab67 100644 +--- a/src/main/java/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java ++++ b/src/main/java/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java +@@ -19,7 +19,7 @@ public class ServerboundCommandSuggestionPacket implements Packet 64 && ((index = packet.getCommand().indexOf(' ')) == -1 || index >= 64)) { ++ this.disconnect(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); ++ return; ++ } ++ // Paper end + // Paper start - AsyncTabCompleteEvent + TAB_COMPLETE_EXECUTOR.execute(() -> this.handleCustomCommandSuggestions0(packet)); + } +@@ -815,6 +822,13 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + private void sendServerSuggestions(final ServerboundCommandSuggestionPacket packet, final StringReader stringreader) { + // Paper end - AsyncTabCompleteEvent + ParseResults parseresults = this.server.getCommands().getDispatcher().parse(stringreader, this.player.createCommandSourceStack()); ++ // Paper start - Handle non-recoverable exceptions ++ if (!parseresults.getExceptions().isEmpty() ++ && parseresults.getExceptions().values().stream().anyMatch(e -> e instanceof io.papermc.paper.brigadier.TagParseCommandSyntaxException)) { ++ this.disconnect(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); ++ return; ++ } ++ // Paper end - Handle non-recoverable exceptions + + this.server.getCommands().getDispatcher().getCompletionSuggestions(parseresults).thenAccept((suggestions) -> { + // Paper start - Don't tab-complete namespaced commands if send-namespaced is false diff --git a/patches/server/0977-Improve-tag-parser-handling.patch b/patches/server/0977-Improve-tag-parser-handling.patch deleted file mode 100644 index 6f346ac4f2..0000000000 --- a/patches/server/0977-Improve-tag-parser-handling.patch +++ /dev/null @@ -1,202 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Nassim Jahnke -Date: Mon, 5 Feb 2024 11:54:04 +0100 -Subject: [PATCH] Improve tag parser handling - - -diff --git a/src/main/java/com/mojang/brigadier/CommandDispatcher.java b/src/main/java/com/mojang/brigadier/CommandDispatcher.java -index 92848b64a78fce7a92e1657c2da6fc5ee53eea44..4b4f812eb13d5f03bcf3f8724d8aa8dbbc724e8b 100644 ---- a/src/main/java/com/mojang/brigadier/CommandDispatcher.java -+++ b/src/main/java/com/mojang/brigadier/CommandDispatcher.java -@@ -304,9 +304,15 @@ public class CommandDispatcher { - } - final CommandContextBuilder context = contextSoFar.copy(); - final StringReader reader = new StringReader(originalReader); -+ boolean stop = false; // Paper - Handle non-recoverable exceptions - try { - try { - child.parse(reader, context); -+ // Paper start - Handle non-recoverable exceptions -+ } catch (final io.papermc.paper.brigadier.TagParseCommandSyntaxException e) { -+ stop = true; -+ throw e; -+ // Paper end - Handle non-recoverable exceptions - } catch (final RuntimeException ex) { - throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherParseException().createWithContext(reader, ex.getMessage()); - } -@@ -321,6 +327,7 @@ public class CommandDispatcher { - } - errors.put(child, ex); - reader.setCursor(cursor); -+ if (stop) return new ParseResults<>(contextSoFar, originalReader, errors); // Paper - Handle non-recoverable exceptions - continue; - } - -diff --git a/src/main/java/io/papermc/paper/brigadier/TagParseCommandSyntaxException.java b/src/main/java/io/papermc/paper/brigadier/TagParseCommandSyntaxException.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a375ad4ba9db990b24a2b9ff366fcba66b753815 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/brigadier/TagParseCommandSyntaxException.java -@@ -0,0 +1,15 @@ -+package io.papermc.paper.brigadier; -+ -+import com.mojang.brigadier.LiteralMessage; -+import com.mojang.brigadier.exceptions.CommandSyntaxException; -+import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; -+import net.minecraft.network.chat.Component; -+ -+public final class TagParseCommandSyntaxException extends CommandSyntaxException { -+ -+ private static final SimpleCommandExceptionType EXCEPTION_TYPE = new SimpleCommandExceptionType(new LiteralMessage("Error parsing NBT")); -+ -+ public TagParseCommandSyntaxException(final String message) { -+ super(EXCEPTION_TYPE, Component.literal(message)); -+ } -+} -diff --git a/src/main/java/net/minecraft/nbt/TagParser.java b/src/main/java/net/minecraft/nbt/TagParser.java -index da101bca71f4710812621b98f0a0d8cab180346a..3cd112584accb8e8f050ac99738eed11c902e60e 100644 ---- a/src/main/java/net/minecraft/nbt/TagParser.java -+++ b/src/main/java/net/minecraft/nbt/TagParser.java -@@ -49,6 +49,7 @@ public class TagParser { - }, CompoundTag::toString); - public static final Codec LENIENT_CODEC = Codec.withAlternative(AS_CODEC, CompoundTag.CODEC); - private final StringReader reader; -+ private int depth; // Paper - - public static CompoundTag parseTag(String string) throws CommandSyntaxException { - return new TagParser(new StringReader(string)).readSingleStruct(); -@@ -159,6 +160,7 @@ public class TagParser { - - public CompoundTag readStruct() throws CommandSyntaxException { - this.expect('{'); -+ this.increaseDepth(); // Paper - CompoundTag compoundTag = new CompoundTag(); - this.reader.skipWhitespace(); - -@@ -182,6 +184,7 @@ public class TagParser { - } - - this.expect('}'); -+ this.depth--; // Paper - return compoundTag; - } - -@@ -191,6 +194,7 @@ public class TagParser { - if (!this.reader.canRead()) { - throw ERROR_EXPECTED_VALUE.createWithContext(this.reader); - } else { -+ this.increaseDepth(); // Paper - ListTag listTag = new ListTag(); - TagType tagType = null; - -@@ -216,6 +220,7 @@ public class TagParser { - } - - this.expect(']'); -+ this.depth--; // Paper - return listTag; - } - } -@@ -288,4 +293,11 @@ public class TagParser { - this.reader.skipWhitespace(); - this.reader.expect(c); - } -+ -+ private void increaseDepth() throws CommandSyntaxException { -+ this.depth++; -+ if (this.depth > 512) { -+ throw new io.papermc.paper.brigadier.TagParseCommandSyntaxException("NBT tag is too complex, depth > 512"); -+ } -+ } - } -diff --git a/src/main/java/net/minecraft/network/chat/contents/TranslatableContents.java b/src/main/java/net/minecraft/network/chat/contents/TranslatableContents.java -index 56e641bc5f6edc657647993ea2efbb7bb9c2f732..4aa6232bf0f72fcde32d257100bd15b1c5192aaa 100644 ---- a/src/main/java/net/minecraft/network/chat/contents/TranslatableContents.java -+++ b/src/main/java/net/minecraft/network/chat/contents/TranslatableContents.java -@@ -181,6 +181,15 @@ public class TranslatableContents implements ComponentContents { - - @Override - public Optional visit(FormattedText.ContentConsumer visitor) { -+ // Paper start - Count visited parts -+ try { -+ return this.visit(new TranslatableContentConsumer<>(visitor)); -+ } catch (IllegalArgumentException ignored) { -+ return visitor.accept("..."); -+ } -+ } -+ private Optional visit(TranslatableContentConsumer visitor) { -+ // Paper end - Count visited parts - this.decompose(); - - for (FormattedText formattedText : this.decomposedParts) { -@@ -192,6 +201,25 @@ public class TranslatableContents implements ComponentContents { - - return Optional.empty(); - } -+ // Paper start - Count visited parts -+ private static final class TranslatableContentConsumer implements FormattedText.ContentConsumer { -+ private static final IllegalArgumentException EX = new IllegalArgumentException("Too long"); -+ private final FormattedText.ContentConsumer visitor; -+ private int visited; -+ -+ private TranslatableContentConsumer(FormattedText.ContentConsumer visitor) { -+ this.visitor = visitor; -+ } -+ -+ @Override -+ public Optional accept(final String asString) { -+ if (visited++ > 32) { -+ throw EX; -+ } -+ return this.visitor.accept(asString); -+ } -+ } -+ // Paper end - Count visited parts - - @Override - public MutableComponent resolve(@Nullable CommandSourceStack source, @Nullable Entity sender, int depth) throws CommandSyntaxException { -diff --git a/src/main/java/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java b/src/main/java/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java -index 898b19887ed34c87003fc63cb5905df2ba6234a5..b47eeb23055b135d5567552ba983bfbc3e1fab67 100644 ---- a/src/main/java/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java -+++ b/src/main/java/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java -@@ -19,7 +19,7 @@ public class ServerboundCommandSuggestionPacket implements Packet 64 && ((index = packet.getCommand().indexOf(' ')) == -1 || index >= 64)) { -+ this.disconnect(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); -+ return; -+ } -+ // Paper end - // Paper start - AsyncTabCompleteEvent - TAB_COMPLETE_EXECUTOR.execute(() -> this.handleCustomCommandSuggestions0(packet)); - } -@@ -815,6 +822,13 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - private void sendServerSuggestions(final ServerboundCommandSuggestionPacket packet, final StringReader stringreader) { - // Paper end - AsyncTabCompleteEvent - ParseResults parseresults = this.server.getCommands().getDispatcher().parse(stringreader, this.player.createCommandSourceStack()); -+ // Paper start - Handle non-recoverable exceptions -+ if (!parseresults.getExceptions().isEmpty() -+ && parseresults.getExceptions().values().stream().anyMatch(e -> e instanceof io.papermc.paper.brigadier.TagParseCommandSyntaxException)) { -+ this.disconnect(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); -+ return; -+ } -+ // Paper end - Handle non-recoverable exceptions - - this.server.getCommands().getDispatcher().getCompletionSuggestions(parseresults).thenAccept((suggestions) -> { - // Paper start - Don't tab-complete namespaced commands if send-namespaced is false diff --git a/patches/server/0977-Item-Mutation-Fixes.patch b/patches/server/0977-Item-Mutation-Fixes.patch new file mode 100644 index 0000000000..736d493c99 --- /dev/null +++ b/patches/server/0977-Item-Mutation-Fixes.patch @@ -0,0 +1,38 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> +Date: Wed, 20 Mar 2024 20:41:35 -0400 +Subject: [PATCH] Item Mutation Fixes + + +diff --git a/src/main/java/net/minecraft/world/inventory/EnchantmentMenu.java b/src/main/java/net/minecraft/world/inventory/EnchantmentMenu.java +index 46beb8e45788950b8ca863aaf07c6d0587d8f693..5b3e33807e0e13480e3359c0cf067719e5749237 100644 +--- a/src/main/java/net/minecraft/world/inventory/EnchantmentMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/EnchantmentMenu.java +@@ -227,7 +227,7 @@ public class EnchantmentMenu extends AbstractContainerMenu { + return false; + } else if (this.costs[id] > 0 && !itemstack.isEmpty() && (player.experienceLevel >= j && player.experienceLevel >= this.costs[id] || player.getAbilities().instabuild)) { + this.access.execute((world, blockposition) -> { +- ItemStack itemstack2 = itemstack; ++ ItemStack itemstack2 = itemstack; // Paper - diff on change + List list = this.getEnchantmentList(world.enabledFeatures(), itemstack, id, this.costs[id]); + + // CraftBukkit start +@@ -249,10 +249,16 @@ public class EnchantmentMenu extends AbstractContainerMenu { + return; + } + // CraftBukkit end +- if (itemstack.is(Items.BOOK)) { +- itemstack2 = itemstack.transmuteCopy(Items.ENCHANTED_BOOK, 1); ++ // Paper start ++ itemstack2 = org.bukkit.craftbukkit.inventory.CraftItemStack.getOrCloneOnMutation(item, event.getItem()); ++ if (itemstack2 != itemstack) { + this.enchantSlots.setItem(0, itemstack2); + } ++ if (itemstack2.is(Items.BOOK)) { ++ itemstack2 = itemstack2.transmuteCopy(Items.ENCHANTED_BOOK, 1); ++ this.enchantSlots.setItem(0, itemstack2); ++ } ++ // Paper end + + // CraftBukkit start + for (Map.Entry entry : event.getEnchantsToAdd().entrySet()) { diff --git a/patches/server/0978-Item-Mutation-Fixes.patch b/patches/server/0978-Item-Mutation-Fixes.patch deleted file mode 100644 index 736d493c99..0000000000 --- a/patches/server/0978-Item-Mutation-Fixes.patch +++ /dev/null @@ -1,38 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> -Date: Wed, 20 Mar 2024 20:41:35 -0400 -Subject: [PATCH] Item Mutation Fixes - - -diff --git a/src/main/java/net/minecraft/world/inventory/EnchantmentMenu.java b/src/main/java/net/minecraft/world/inventory/EnchantmentMenu.java -index 46beb8e45788950b8ca863aaf07c6d0587d8f693..5b3e33807e0e13480e3359c0cf067719e5749237 100644 ---- a/src/main/java/net/minecraft/world/inventory/EnchantmentMenu.java -+++ b/src/main/java/net/minecraft/world/inventory/EnchantmentMenu.java -@@ -227,7 +227,7 @@ public class EnchantmentMenu extends AbstractContainerMenu { - return false; - } else if (this.costs[id] > 0 && !itemstack.isEmpty() && (player.experienceLevel >= j && player.experienceLevel >= this.costs[id] || player.getAbilities().instabuild)) { - this.access.execute((world, blockposition) -> { -- ItemStack itemstack2 = itemstack; -+ ItemStack itemstack2 = itemstack; // Paper - diff on change - List list = this.getEnchantmentList(world.enabledFeatures(), itemstack, id, this.costs[id]); - - // CraftBukkit start -@@ -249,10 +249,16 @@ public class EnchantmentMenu extends AbstractContainerMenu { - return; - } - // CraftBukkit end -- if (itemstack.is(Items.BOOK)) { -- itemstack2 = itemstack.transmuteCopy(Items.ENCHANTED_BOOK, 1); -+ // Paper start -+ itemstack2 = org.bukkit.craftbukkit.inventory.CraftItemStack.getOrCloneOnMutation(item, event.getItem()); -+ if (itemstack2 != itemstack) { - this.enchantSlots.setItem(0, itemstack2); - } -+ if (itemstack2.is(Items.BOOK)) { -+ itemstack2 = itemstack2.transmuteCopy(Items.ENCHANTED_BOOK, 1); -+ this.enchantSlots.setItem(0, itemstack2); -+ } -+ // Paper end - - // CraftBukkit start - for (Map.Entry entry : event.getEnchantsToAdd().entrySet()) { diff --git a/patches/server/0978-Per-world-ticks-per-spawn-settings.patch b/patches/server/0978-Per-world-ticks-per-spawn-settings.patch new file mode 100644 index 0000000000..ebdf89762d --- /dev/null +++ b/patches/server/0978-Per-world-ticks-per-spawn-settings.patch @@ -0,0 +1,35 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Sat, 13 Nov 2021 12:36:26 -0800 +Subject: [PATCH] Per world ticks per spawn settings + + +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 51c4877ffb77ece41e90d9c6ac06c04dc941e35c..975fcd4b8f93cb8c602ddeb165c485214eac10a4 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -185,6 +185,15 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + return this.getChunkIfLoaded(chunkX, chunkZ) != null; + } + // Paper end - Use getChunkIfLoadedImmediately ++ // Paper start - per world ticks per spawn ++ private int getTicksPerSpawn(SpawnCategory spawnCategory) { ++ final int perWorld = this.paperConfig().entities.spawning.ticksPerSpawn.getInt(CraftSpawnCategory.toNMS(spawnCategory)); ++ if (perWorld >= 0) { ++ return perWorld; ++ } ++ return this.getCraftServer().getTicksPerSpawns(spawnCategory); ++ } ++ // Paper end + + public abstract ResourceKey getTypeKey(); + +@@ -197,7 +206,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + // CraftBukkit Ticks things + for (SpawnCategory spawnCategory : SpawnCategory.values()) { + if (CraftSpawnCategory.isValidForLimits(spawnCategory)) { +- this.ticksPerSpawnCategory.put(spawnCategory, (long) this.getCraftServer().getTicksPerSpawns(spawnCategory)); ++ this.ticksPerSpawnCategory.put(spawnCategory, this.getTicksPerSpawn(spawnCategory)); // Paper + } + } + diff --git a/patches/server/0979-Per-world-ticks-per-spawn-settings.patch b/patches/server/0979-Per-world-ticks-per-spawn-settings.patch deleted file mode 100644 index ebdf89762d..0000000000 --- a/patches/server/0979-Per-world-ticks-per-spawn-settings.patch +++ /dev/null @@ -1,35 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Sat, 13 Nov 2021 12:36:26 -0800 -Subject: [PATCH] Per world ticks per spawn settings - - -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index 51c4877ffb77ece41e90d9c6ac06c04dc941e35c..975fcd4b8f93cb8c602ddeb165c485214eac10a4 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -185,6 +185,15 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - return this.getChunkIfLoaded(chunkX, chunkZ) != null; - } - // Paper end - Use getChunkIfLoadedImmediately -+ // Paper start - per world ticks per spawn -+ private int getTicksPerSpawn(SpawnCategory spawnCategory) { -+ final int perWorld = this.paperConfig().entities.spawning.ticksPerSpawn.getInt(CraftSpawnCategory.toNMS(spawnCategory)); -+ if (perWorld >= 0) { -+ return perWorld; -+ } -+ return this.getCraftServer().getTicksPerSpawns(spawnCategory); -+ } -+ // Paper end - - public abstract ResourceKey getTypeKey(); - -@@ -197,7 +206,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - // CraftBukkit Ticks things - for (SpawnCategory spawnCategory : SpawnCategory.values()) { - if (CraftSpawnCategory.isValidForLimits(spawnCategory)) { -- this.ticksPerSpawnCategory.put(spawnCategory, (long) this.getCraftServer().getTicksPerSpawns(spawnCategory)); -+ this.ticksPerSpawnCategory.put(spawnCategory, this.getTicksPerSpawn(spawnCategory)); // Paper - } - } - diff --git a/patches/server/0979-Properly-track-the-changed-item-from-dispense-events.patch b/patches/server/0979-Properly-track-the-changed-item-from-dispense-events.patch new file mode 100644 index 0000000000..9957c97ca3 --- /dev/null +++ b/patches/server/0979-Properly-track-the-changed-item-from-dispense-events.patch @@ -0,0 +1,103 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Mon, 12 Dec 2022 12:14:03 -0800 +Subject: [PATCH] Properly track the changed item from dispense events + + +diff --git a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +index 32084df594649d8da04052bbfa111896d8ea1f91..5dab1e10303177e5a4d97a91ee46ede66f30ae35 100644 +--- a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +@@ -133,10 +133,14 @@ public interface DispenseItemBehavior { + idispensebehavior.dispense(pointer, eventStack); + return stack; + } ++ // Paper start - track changed items in the dispense event ++ itemstack1 = CraftItemStack.unwrap(event.getItem()); // unwrap is safe because the stack won't be modified ++ entitytypes = ((SpawnEggItem) itemstack1.getItem()).getType(itemstack1); ++ // Paper end - track changed item from dispense event + } + + try { +- entitytypes.spawn(pointer.level(), stack, (Player) null, pointer.pos().relative(enumdirection), MobSpawnType.DISPENSER, enumdirection != Direction.UP, false); ++ entitytypes.spawn(pointer.level(), itemstack1, (Player) null, pointer.pos().relative(enumdirection), MobSpawnType.DISPENSER, enumdirection != Direction.UP, false); // Paper - track changed item in dispense event + } catch (Exception exception) { + DispenseItemBehavior.LOGGER.error("Error while dispensing spawn egg from dispenser at {}", pointer.pos(), exception); // CraftBukkit - decompile error + return ItemStack.EMPTY; +@@ -191,9 +195,10 @@ public interface DispenseItemBehavior { + } + // CraftBukkit end + ++ final ItemStack newStack = CraftItemStack.unwrap(event.getItem()); // Paper - use event itemstack (unwrap is fine here because the stack won't be modified) + Consumer consumer = EntityType.appendDefaultStackConfig((entityarmorstand) -> { + entityarmorstand.setYRot(enumdirection.toYRot()); +- }, worldserver, stack, (Player) null); ++ }, worldserver, newStack, (Player) null); // Paper - track changed items in the dispense event + ArmorStand entityarmorstand = (ArmorStand) EntityType.ARMOR_STAND.spawn(worldserver, consumer, blockposition, MobSpawnType.DISPENSER, false, false); + + if (entityarmorstand != null) { +@@ -413,6 +418,7 @@ public interface DispenseItemBehavior { + int y = blockposition.getY(); + int z = blockposition.getZ(); + BlockState iblockdata = worldserver.getBlockState(blockposition); ++ ItemStack dispensedItem = stack; // Paper - track changed item from the dispense event + // Paper start - correctly check if the bucket place will succeed + /* Taken from SolidBucketItem#emptyContents */ + boolean willEmptyContentsSolidBucketItem = dispensiblecontaineritem instanceof net.minecraft.world.item.SolidBucketItem && worldserver.isInWorldBounds(blockposition) && iblockdata.isAir(); +@@ -442,12 +448,15 @@ public interface DispenseItemBehavior { + } + } + +- dispensiblecontaineritem = (DispensibleContainerItem) CraftItemStack.asNMSCopy(event.getItem()).getItem(); ++ // Paper start - track changed item from dispense event ++ dispensedItem = CraftItemStack.unwrap(event.getItem()); // unwrap is safe here as the stack isn't mutated ++ dispensiblecontaineritem = (DispensibleContainerItem) dispensedItem.getItem(); ++ // Paper end - track changed item from dispense event + } + // CraftBukkit end + + if (dispensiblecontaineritem.emptyContents((Player) null, worldserver, blockposition, (BlockHitResult) null)) { +- dispensiblecontaineritem.checkExtraContent((Player) null, worldserver, stack, blockposition); ++ dispensiblecontaineritem.checkExtraContent((Player) null, worldserver, dispensedItem, blockposition); // Paper - track changed item from dispense event + // CraftBukkit start - Handle stacked buckets + Item item = Items.BUCKET; + stack.shrink(1); +diff --git a/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java b/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java +index 985954030654d521291cccbfc3ca49b67ee4357d..e37d2d29f3ba67cfe28abe4847a3dca07121f0be 100644 +--- a/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java +@@ -36,7 +36,7 @@ public class ProjectileDispenseBehavior extends DefaultDispenseItemBehavior { + ServerLevel worldserver = pointer.level(); + Direction enumdirection = (Direction) pointer.state().getValue(DispenserBlock.FACING); + Position iposition = this.dispenseConfig.positionFunction().getDispensePosition(pointer, enumdirection); +- Projectile iprojectile = this.projectileItem.asProjectile(worldserver, iposition, stack, enumdirection); ++ // Paper - move down + + // CraftBukkit start + // this.projectileItem.shoot(iprojectile, (double) enumdirection.getStepX(), (double) enumdirection.getStepY(), (double) enumdirection.getStepZ(), this.dispenseConfig.power(), this.dispenseConfig.uncertainty()); +@@ -66,6 +66,7 @@ public class ProjectileDispenseBehavior extends DefaultDispenseItemBehavior { + } + } + ++ Projectile iprojectile = this.projectileItem.asProjectile(worldserver, iposition, CraftItemStack.unwrap(event.getItem()), enumdirection); // Paper - move from above and track changed items in the dispense event; unwrap is safe here because all uses of the stack make their own copies + this.projectileItem.shoot(iprojectile, event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ(), this.dispenseConfig.power(), this.dispenseConfig.uncertainty()); + ((Entity) iprojectile).projectileSource = new org.bukkit.craftbukkit.projectiles.CraftBlockProjectileSource(pointer.blockEntity()); + // CraftBukkit end +diff --git a/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java +index 6f2adf2334e35e8a617a4ced0c1af2abf32bbd8d..cb308808906a8cdb127df8284e106e00553473ca 100644 +--- a/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java ++++ b/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java +@@ -57,7 +57,12 @@ public class ShulkerBoxDispenseBehavior extends OptionalDispenseItemBehavior { + // CraftBukkit end + + try { +- this.setSuccess(((BlockItem) item).place(new DirectionalPlaceContext(pointer.level(), blockposition, enumdirection, stack, enumdirection1)).consumesAction()); ++ // Paper start - track changed items in the dispense event ++ this.setSuccess(((BlockItem) item).place(new DirectionalPlaceContext(pointer.level(), blockposition, enumdirection, CraftItemStack.asNMSCopy(event.getItem()), enumdirection1)).consumesAction()); ++ if (this.isSuccess()) { ++ stack.shrink(1); // vanilla shrink is in the place function above, manually handle it here ++ } ++ // Paper end - track changed items in the dispense event + } catch (Exception exception) { + ShulkerBoxDispenseBehavior.LOGGER.error("Error trying to place shulker box at {}", blockposition, exception); + } diff --git a/patches/server/0980-Properly-track-the-changed-item-from-dispense-events.patch b/patches/server/0980-Properly-track-the-changed-item-from-dispense-events.patch deleted file mode 100644 index 9957c97ca3..0000000000 --- a/patches/server/0980-Properly-track-the-changed-item-from-dispense-events.patch +++ /dev/null @@ -1,103 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Mon, 12 Dec 2022 12:14:03 -0800 -Subject: [PATCH] Properly track the changed item from dispense events - - -diff --git a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java -index 32084df594649d8da04052bbfa111896d8ea1f91..5dab1e10303177e5a4d97a91ee46ede66f30ae35 100644 ---- a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java -+++ b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java -@@ -133,10 +133,14 @@ public interface DispenseItemBehavior { - idispensebehavior.dispense(pointer, eventStack); - return stack; - } -+ // Paper start - track changed items in the dispense event -+ itemstack1 = CraftItemStack.unwrap(event.getItem()); // unwrap is safe because the stack won't be modified -+ entitytypes = ((SpawnEggItem) itemstack1.getItem()).getType(itemstack1); -+ // Paper end - track changed item from dispense event - } - - try { -- entitytypes.spawn(pointer.level(), stack, (Player) null, pointer.pos().relative(enumdirection), MobSpawnType.DISPENSER, enumdirection != Direction.UP, false); -+ entitytypes.spawn(pointer.level(), itemstack1, (Player) null, pointer.pos().relative(enumdirection), MobSpawnType.DISPENSER, enumdirection != Direction.UP, false); // Paper - track changed item in dispense event - } catch (Exception exception) { - DispenseItemBehavior.LOGGER.error("Error while dispensing spawn egg from dispenser at {}", pointer.pos(), exception); // CraftBukkit - decompile error - return ItemStack.EMPTY; -@@ -191,9 +195,10 @@ public interface DispenseItemBehavior { - } - // CraftBukkit end - -+ final ItemStack newStack = CraftItemStack.unwrap(event.getItem()); // Paper - use event itemstack (unwrap is fine here because the stack won't be modified) - Consumer consumer = EntityType.appendDefaultStackConfig((entityarmorstand) -> { - entityarmorstand.setYRot(enumdirection.toYRot()); -- }, worldserver, stack, (Player) null); -+ }, worldserver, newStack, (Player) null); // Paper - track changed items in the dispense event - ArmorStand entityarmorstand = (ArmorStand) EntityType.ARMOR_STAND.spawn(worldserver, consumer, blockposition, MobSpawnType.DISPENSER, false, false); - - if (entityarmorstand != null) { -@@ -413,6 +418,7 @@ public interface DispenseItemBehavior { - int y = blockposition.getY(); - int z = blockposition.getZ(); - BlockState iblockdata = worldserver.getBlockState(blockposition); -+ ItemStack dispensedItem = stack; // Paper - track changed item from the dispense event - // Paper start - correctly check if the bucket place will succeed - /* Taken from SolidBucketItem#emptyContents */ - boolean willEmptyContentsSolidBucketItem = dispensiblecontaineritem instanceof net.minecraft.world.item.SolidBucketItem && worldserver.isInWorldBounds(blockposition) && iblockdata.isAir(); -@@ -442,12 +448,15 @@ public interface DispenseItemBehavior { - } - } - -- dispensiblecontaineritem = (DispensibleContainerItem) CraftItemStack.asNMSCopy(event.getItem()).getItem(); -+ // Paper start - track changed item from dispense event -+ dispensedItem = CraftItemStack.unwrap(event.getItem()); // unwrap is safe here as the stack isn't mutated -+ dispensiblecontaineritem = (DispensibleContainerItem) dispensedItem.getItem(); -+ // Paper end - track changed item from dispense event - } - // CraftBukkit end - - if (dispensiblecontaineritem.emptyContents((Player) null, worldserver, blockposition, (BlockHitResult) null)) { -- dispensiblecontaineritem.checkExtraContent((Player) null, worldserver, stack, blockposition); -+ dispensiblecontaineritem.checkExtraContent((Player) null, worldserver, dispensedItem, blockposition); // Paper - track changed item from dispense event - // CraftBukkit start - Handle stacked buckets - Item item = Items.BUCKET; - stack.shrink(1); -diff --git a/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java b/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java -index 985954030654d521291cccbfc3ca49b67ee4357d..e37d2d29f3ba67cfe28abe4847a3dca07121f0be 100644 ---- a/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java -+++ b/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java -@@ -36,7 +36,7 @@ public class ProjectileDispenseBehavior extends DefaultDispenseItemBehavior { - ServerLevel worldserver = pointer.level(); - Direction enumdirection = (Direction) pointer.state().getValue(DispenserBlock.FACING); - Position iposition = this.dispenseConfig.positionFunction().getDispensePosition(pointer, enumdirection); -- Projectile iprojectile = this.projectileItem.asProjectile(worldserver, iposition, stack, enumdirection); -+ // Paper - move down - - // CraftBukkit start - // this.projectileItem.shoot(iprojectile, (double) enumdirection.getStepX(), (double) enumdirection.getStepY(), (double) enumdirection.getStepZ(), this.dispenseConfig.power(), this.dispenseConfig.uncertainty()); -@@ -66,6 +66,7 @@ public class ProjectileDispenseBehavior extends DefaultDispenseItemBehavior { - } - } - -+ Projectile iprojectile = this.projectileItem.asProjectile(worldserver, iposition, CraftItemStack.unwrap(event.getItem()), enumdirection); // Paper - move from above and track changed items in the dispense event; unwrap is safe here because all uses of the stack make their own copies - this.projectileItem.shoot(iprojectile, event.getVelocity().getX(), event.getVelocity().getY(), event.getVelocity().getZ(), this.dispenseConfig.power(), this.dispenseConfig.uncertainty()); - ((Entity) iprojectile).projectileSource = new org.bukkit.craftbukkit.projectiles.CraftBlockProjectileSource(pointer.blockEntity()); - // CraftBukkit end -diff --git a/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java -index 6f2adf2334e35e8a617a4ced0c1af2abf32bbd8d..cb308808906a8cdb127df8284e106e00553473ca 100644 ---- a/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java -+++ b/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java -@@ -57,7 +57,12 @@ public class ShulkerBoxDispenseBehavior extends OptionalDispenseItemBehavior { - // CraftBukkit end - - try { -- this.setSuccess(((BlockItem) item).place(new DirectionalPlaceContext(pointer.level(), blockposition, enumdirection, stack, enumdirection1)).consumesAction()); -+ // Paper start - track changed items in the dispense event -+ this.setSuccess(((BlockItem) item).place(new DirectionalPlaceContext(pointer.level(), blockposition, enumdirection, CraftItemStack.asNMSCopy(event.getItem()), enumdirection1)).consumesAction()); -+ if (this.isSuccess()) { -+ stack.shrink(1); // vanilla shrink is in the place function above, manually handle it here -+ } -+ // Paper end - track changed items in the dispense event - } catch (Exception exception) { - ShulkerBoxDispenseBehavior.LOGGER.error("Error trying to place shulker box at {}", blockposition, exception); - } diff --git a/patches/server/0980-Rewrite-dataconverter-system.patch b/patches/server/0980-Rewrite-dataconverter-system.patch new file mode 100644 index 0000000000..bbb4bda157 --- /dev/null +++ b/patches/server/0980-Rewrite-dataconverter-system.patch @@ -0,0 +1,28922 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sat, 19 Jun 2021 10:43:01 -0700 +Subject: [PATCH] Rewrite dataconverter system + +Please see https://github.com/PaperMC/DataConverter +for details. + +diff --git a/src/main/java/ca/spottedleaf/dataconverter/converters/DataConverter.java b/src/main/java/ca/spottedleaf/dataconverter/converters/DataConverter.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1863c606be715683d53863a0c9293525d199c9cf +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/converters/DataConverter.java +@@ -0,0 +1,54 @@ ++package ca.spottedleaf.dataconverter.converters; ++ ++import java.util.Comparator; ++ ++public abstract class DataConverter { ++ ++ public static final Comparator> LOWEST_VERSION_COMPARATOR = (x, y) -> { ++ return Long.compare(x.getEncodedVersion(), y.getEncodedVersion()); ++ }; ++ ++ protected final int toVersion; ++ protected final int versionStep; ++ ++ public DataConverter(final int toVersion) { ++ this.toVersion = toVersion; ++ this.versionStep = 0; ++ } ++ ++ public DataConverter(final int toVersion, final int versionStep) { ++ this.toVersion = toVersion; ++ this.versionStep = versionStep; ++ } ++ ++ public final int getToVersion() { ++ return this.toVersion; ++ } ++ ++ public final int getVersionStep() { ++ return this.versionStep; ++ } ++ ++ public final long getEncodedVersion() { ++ return encodeVersions(this.toVersion, this.versionStep); ++ } ++ ++ public abstract R convert(final T data, final long sourceVersion, final long toVersion); ++ ++ // step must be in the lower bits, so that encodeVersions(version, step) < encodeVersions(version, step + 1) ++ public static long encodeVersions(final int version, final int step) { ++ return ((long)version << 32) | (step & 0xFFFFFFFFL); ++ } ++ ++ public static int getVersion(final long encoded) { ++ return (int)(encoded >>> 32); ++ } ++ ++ public static int getStep(final long encoded) { ++ return (int)encoded; ++ } ++ ++ public static String encodedToString(final long encoded) { ++ return getVersion(encoded) + "." + getStep(encoded); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataHook.java b/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataHook.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0b92c5c66ad3a5198873f98287a5ced71c231d09 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataHook.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.dataconverter.converters.datatypes; ++ ++public interface DataHook { ++ ++ public R preHook(final T data, final long fromVersion, final long toVersion); ++ ++ public R postHook(final T data, final long fromVersion, final long toVersion); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataType.java b/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataType.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b56a7f9ace3b947fed49101b6e9936721fb99ea5 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataType.java +@@ -0,0 +1,7 @@ ++package ca.spottedleaf.dataconverter.converters.datatypes; ++ ++public abstract class DataType { ++ ++ public abstract R convert(final T data, final long fromVersion, final long toVersion); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataWalker.java b/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataWalker.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ca55b3f7e7208e629e88d4c7bfa9517384a26fef +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataWalker.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.dataconverter.converters.datatypes; ++ ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public interface DataWalker { ++ ++ public T walk(final T data, final long fromVersion, final long toVersion); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCDataConverter.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCDataConverter.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a27d3d41109271834b6c37fa22d4b80d9e4b88c8 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCDataConverter.java +@@ -0,0 +1,79 @@ ++package ca.spottedleaf.dataconverter.minecraft; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.converters.datatypes.DataType; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCDataType; ++import ca.spottedleaf.dataconverter.minecraft.versions.V99; ++import ca.spottedleaf.dataconverter.types.json.JsonMapType; ++import ca.spottedleaf.dataconverter.types.nbt.NBTMapType; ++import com.google.gson.JsonObject; ++import it.unimi.dsi.fastutil.longs.LongArrayList; ++import net.minecraft.nbt.CompoundTag; ++ ++public final class MCDataConverter { ++ ++ private static final LongArrayList BREAKPOINTS = MCVersionRegistry.getBreakpoints(); ++ ++ public static T copy(final T type) { ++ if (type instanceof CompoundTag) { ++ return (T)((CompoundTag)type).copy(); ++ } else if (type instanceof JsonObject) { ++ return (T)((JsonObject)type).deepCopy(); ++ } ++ ++ return type; ++ } ++ ++ public static CompoundTag convertTag(final MCDataType type, final CompoundTag data, final int fromVersion, final int toVersion) { ++ final NBTMapType wrapped = new NBTMapType(data); ++ ++ final NBTMapType replaced = (NBTMapType)convert(type, wrapped, fromVersion, toVersion); ++ ++ return replaced == null ? wrapped.getTag() : replaced.getTag(); ++ } ++ ++ public static JsonObject convertJson(final MCDataType type, final JsonObject data, final boolean compressed, final int fromVersion, final int toVersion) { ++ final JsonMapType wrapped = new JsonMapType(data, compressed); ++ ++ final JsonMapType replaced = (JsonMapType)convert(type, wrapped, fromVersion, toVersion); ++ ++ return replaced == null ? wrapped.getJson() : replaced.getJson(); ++ } ++ ++ public static R convert(final DataType type, final T data, int fromVersion, final int toVersion) { ++ Object ret = data; ++ ++ long currentVersion = DataConverter.encodeVersions(fromVersion < V99.VERSION ? V99.VERSION : fromVersion, Integer.MAX_VALUE); ++ final long nextVersion = DataConverter.encodeVersions(toVersion, Integer.MAX_VALUE); ++ ++ for (int i = 0, len = BREAKPOINTS.size(); i < len; ++i) { ++ final long breakpoint = BREAKPOINTS.getLong(i); ++ ++ if (currentVersion >= breakpoint) { ++ continue; ++ } ++ ++ final Object converted = type.convert((T)ret, currentVersion, Math.min(nextVersion, breakpoint - 1)); ++ if (converted != null) { ++ ret = converted; ++ } ++ ++ currentVersion = Math.min(nextVersion, breakpoint - 1); ++ ++ if (currentVersion == nextVersion) { ++ break; ++ } ++ } ++ ++ if (currentVersion != nextVersion) { ++ final Object converted = type.convert((T)ret, currentVersion, nextVersion); ++ if (converted != null) { ++ ret = converted; ++ } ++ } ++ ++ return (R)ret; ++ } ++ ++ private MCDataConverter() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCVersionRegistry.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCVersionRegistry.java +new file mode 100644 +index 0000000000000000000000000000000000000000..057a2e1e160282c53336802580410ca23ea3222a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCVersionRegistry.java +@@ -0,0 +1,424 @@ ++package ca.spottedleaf.dataconverter.minecraft; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import com.mojang.logging.LogUtils; ++import it.unimi.dsi.fastutil.ints.Int2ObjectLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.ints.IntArrayList; ++import it.unimi.dsi.fastutil.ints.IntLinkedOpenHashSet; ++import it.unimi.dsi.fastutil.ints.IntRBTreeSet; ++import it.unimi.dsi.fastutil.longs.LongArrayList; ++import it.unimi.dsi.fastutil.longs.LongComparator; ++import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; ++import org.slf4j.Logger; ++import java.lang.reflect.Field; ++import java.util.Arrays; ++import java.util.Locale; ++ ++public final class MCVersionRegistry { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ private static final Int2ObjectLinkedOpenHashMap VERSION_NAMES = new Int2ObjectLinkedOpenHashMap<>(); ++ private static final IntArrayList VERSION_LIST; ++ private static final LongArrayList DATA_VERSION_LIST; ++ ++ private static final IntArrayList DATACONVERTER_VERSIONS_LIST; ++ private static final IntLinkedOpenHashSet DATACONVERTER_VERSIONS_MAJOR = new IntLinkedOpenHashSet(); ++ private static final LongLinkedOpenHashSet DATACONVERTER_VERSIONS = new LongLinkedOpenHashSet(); ++ private static final Int2ObjectLinkedOpenHashMap SUBVERSIONS = new Int2ObjectLinkedOpenHashMap<>(); ++ private static final LongArrayList BREAKPOINTS = new LongArrayList(); ++ static { ++ // Note: Some of these are nameless. ++ // Unless a data version is specified here, it will NOT have converters ran for it. Please add them on update! ++ final int[] converterVersions = new int[] { ++ 99, ++ 100, ++ 101, ++ 102, ++ 105, ++ 106, ++ 107, ++ 108, ++ 109, ++ 110, ++ 111, ++ 113, ++ 135, ++ 143, ++ 147, ++ 165, ++ 501, ++ 502, ++ 505, ++ 700, ++ 701, ++ 702, ++ 703, ++ 704, ++ 705, ++ 804, ++ 806, ++ 808, ++ 808, ++ 813, ++ 816, ++ 820, ++ 1022, ++ 1125, ++ 1344, ++ 1446, ++ 1450, ++ 1451, ++ 1451, ++ 1451, ++ 1451, ++ 1451, ++ 1451, ++ 1451, ++ 1451, ++ 1451, ++ 1456, ++ 1458, ++ 1460, ++ 1466, ++ 1470, ++ 1474, ++ 1475, ++ 1480, ++ 1481, ++ 1483, ++ 1484, ++ 1486, ++ 1487, ++ 1488, ++ 1490, ++ 1492, ++ 1494, ++ 1496, ++ 1500, ++ 1501, ++ 1502, ++ 1506, ++ 1510, ++ 1514, ++ 1515, ++ 1624, ++ 1800, ++ 1801, ++ 1802, ++ 1803, ++ 1904, ++ 1905, ++ 1906, ++ 1909, ++ 1911, ++ 1914, ++ 1917, ++ 1918, ++ 1920, ++ 1925, ++ 1928, ++ 1929, ++ 1931, ++ 1936, ++ 1946, ++ 1948, ++ 1953, ++ 1955, ++ 1961, ++ 1963, ++ 2100, ++ 2202, ++ 2209, ++ 2211, ++ 2218, ++ 2501, ++ 2502, ++ 2503, ++ 2505, ++ 2508, ++ 2509, ++ 2511, ++ 2514, ++ 2516, ++ 2518, ++ 2519, ++ 2522, ++ 2523, ++ 2527, ++ 2528, ++ 2529, ++ 2531, ++ 2533, ++ 2535, ++ 2538, ++ 2550, ++ 2551, ++ 2552, ++ 2553, ++ 2558, ++ 2568, ++ 2671, ++ 2679, ++ 2680, ++ 2684, ++ 2686, ++ 2688, ++ 2690, ++ 2691, ++ 2693, ++ 2696, ++ 2700, ++ 2701, ++ 2702, ++ 2704, ++ 2707, ++ 2710, ++ 2717, ++ 2825, ++ 2831, ++ 2832, ++ 2833, ++ 2838, ++ 2841, ++ 2842, ++ 2843, ++ 2846, ++ 2852, ++ 2967, ++ 2970, ++ 3077, ++ 3078, ++ 3081, ++ 3082, ++ 3083, ++ 3084, ++ 3086, ++ 3087, ++ 3088, ++ 3090, ++ 3093, ++ 3094, ++ 3097, ++ 3108, ++ 3201, ++ 3203, ++ 3204, ++ 3209, ++ 3214, ++ 3319, ++ 3322, ++ 3438, ++ 3439, ++ 3440, ++ 3441, ++ 3447, ++ 3448, ++ 3450, ++ 3451, ++ 3459, ++ 3564, ++ 3565, ++ 3566, ++ 3568, ++ 3683, ++ 3685, ++ 3692, ++ 3800, ++ 3803, ++ 3807, ++ 3808, ++ 3809, ++ 3812, ++ 3813, ++ 3814, ++ 3818, ++ 3820, ++ 3825, ++ 3828, ++ 3833 ++ // All up to 1.20.5 ++ }; ++ Arrays.sort(converterVersions); ++ ++ DATACONVERTER_VERSIONS_MAJOR.addAll(DATACONVERTER_VERSIONS_LIST = new IntArrayList(converterVersions)); ++ ++ // add sub versions ++ registerSubVersion(MCVersions.V16W38A + 1, 1); ++ ++ registerSubVersion(MCVersions.V17W47A, 1); ++ registerSubVersion(MCVersions.V17W47A, 2); ++ registerSubVersion(MCVersions.V17W47A, 3); ++ registerSubVersion(MCVersions.V17W47A, 4); ++ registerSubVersion(MCVersions.V17W47A, 5); ++ registerSubVersion(MCVersions.V17W47A, 6); ++ registerSubVersion(MCVersions.V17W47A, 7); ++ ++ registerSubVersion(MCVersions.V24W04A + 1, 1); ++ registerSubVersion(MCVersions.V24W04A + 2, 1); ++ ++ registerSubVersion(MCVersions.V24W07A + 1, 1); ++ registerSubVersion(MCVersions.V24W07A + 1, 2); ++ registerSubVersion(MCVersions.V24W07A + 1, 4); ++ registerSubVersion(MCVersions.V24W07A + 1, 5); ++ registerSubVersion(MCVersions.V24W07A + 1, 6); ++ ++ // register breakpoints here ++ // for all major releases after 1.16, add them. this reduces the work required to determine if a breakpoint ++ // is needed for new converters ++ ++ // Too much changed in this version. ++ registerBreakpoint(MCVersions.V17W47A); ++ registerBreakpointAfter(MCVersions.V17W47A, Integer.MAX_VALUE); ++ ++ // final release of major version ++ registerBreakpointAfter(MCVersions.V1_17_1, Integer.MAX_VALUE); ++ ++ // final release of major version ++ registerBreakpointAfter(MCVersions.V1_18_2, Integer.MAX_VALUE); ++ ++ // final release of major version ++ registerBreakpointAfter(MCVersions.V1_19_4, Integer.MAX_VALUE); ++ ++ // Too much changed in this version. ++ registerBreakpoint(MCVersions.V24W07A + 1, 5); ++ registerBreakpointAfter(MCVersions.V24W07A + 1, Integer.MAX_VALUE); ++ } ++ ++ static { ++ final Field[] fields = MCVersions.class.getDeclaredFields(); ++ for (final Field field : fields) { ++ final String name = field.getName(); ++ final int value; ++ try { ++ value = field.getInt(null); ++ } catch (final Exception ex) { ++ throw new RuntimeException(ex); ++ } ++ ++ if (VERSION_NAMES.containsKey(value) && value != MCVersions.V15W33B) { // Mojang registered 15w33a and 15w33b under the same id. ++ LOGGER.warn("Error registering version \"" + name + "\", version number '" + value + "' is already associated with \"" + VERSION_NAMES.get(value) + "\""); ++ } ++ ++ VERSION_NAMES.put(value, name.substring(1).replace("_PRE", "-PRE").replace("_RC", "-RC").replace('_', '.').toLowerCase(Locale.ROOT)); ++ } ++ ++ for (final int version : DATACONVERTER_VERSIONS_MAJOR) { ++ if (VERSION_NAMES.containsKey(version)) { ++ continue; ++ } ++ ++ // find closest greatest version above this one ++ int closest = Integer.MAX_VALUE; ++ String closestName = null; ++ for (final int v : VERSION_NAMES.keySet()) { ++ if (v > version && v < closest) { ++ closest = v; ++ closestName = VERSION_NAMES.get(v); ++ } ++ } ++ ++ if (closestName == null) { ++ VERSION_NAMES.put(version, "unregistered_v" + version); ++ } else { ++ VERSION_NAMES.put(version, closestName + "-dev" + (closest - version)); ++ } ++ } ++ ++ // Explicit override for V99, as 99 is very special. ++ VERSION_NAMES.put(99, "pre_converter"); ++ ++ VERSION_LIST = new IntArrayList(new IntRBTreeSet(VERSION_NAMES.keySet())); ++ ++ DATA_VERSION_LIST = new LongArrayList(); ++ for (final int version : VERSION_LIST) { ++ DATA_VERSION_LIST.add(DataConverter.encodeVersions(version, 0)); ++ ++ final IntArrayList subVersions = SUBVERSIONS.get(version); ++ if (subVersions == null) { ++ continue; ++ } ++ ++ for (final int step : subVersions) { ++ DATA_VERSION_LIST.add(DataConverter.encodeVersions(version, step)); ++ } ++ } ++ ++ DATA_VERSION_LIST.sort((LongComparator)null); ++ ++ for (final int version : DATACONVERTER_VERSIONS_MAJOR) { ++ DATACONVERTER_VERSIONS.add(DataConverter.encodeVersions(version, 0)); ++ ++ final IntArrayList subVersions = SUBVERSIONS.get(version); ++ if (subVersions == null) { ++ continue; ++ } ++ ++ for (final int step : subVersions) { ++ DATACONVERTER_VERSIONS.add(DataConverter.encodeVersions(version, step)); ++ } ++ } ++ } ++ ++ private static void registerSubVersion(final int version, final int step) { ++ if (DATA_VERSION_LIST != null) { ++ throw new IllegalStateException("Added too late!"); ++ } ++ SUBVERSIONS.computeIfAbsent(version, (final int keyInMap) -> { ++ return new IntArrayList(); ++ }).add(step); ++ } ++ ++ private static void registerBreakpoint(final int version) { ++ registerBreakpoint(version, 0); ++ } ++ ++ private static void registerBreakpoint(final int version, final int step) { ++ BREAKPOINTS.add(DataConverter.encodeVersions(version, step)); ++ } ++ ++ private static void registerBreakpointAfter(final int version) { ++ registerBreakpointAfter(version, 0); ++ } ++ ++ private static void registerBreakpointAfter(final int version, final int step) { ++ BREAKPOINTS.add(DataConverter.encodeVersions(version, step) + 1L); ++ } ++ ++ // returns only versions that have dataconverters ++ public static boolean hasDataConverters(final int version) { ++ return DATACONVERTER_VERSIONS_MAJOR.contains(version); ++ } ++ ++ public String getVersionName(final int version) { ++ return VERSION_NAMES.get(version); ++ } ++ ++ public boolean isRegisteredVersion(final int version) { ++ return VERSION_NAMES.containsKey(version); ++ } ++ ++ public static IntArrayList getVersionList() { ++ return VERSION_LIST; ++ } ++ ++ public static LongArrayList getDataVersionList() { ++ return DATA_VERSION_LIST; ++ } ++ ++ public static int getMaxVersion() { ++ return VERSION_LIST.getInt(VERSION_LIST.size() - 1); ++ } ++ ++ public static LongArrayList getBreakpoints() { ++ return BREAKPOINTS; ++ } ++ ++ public static void checkVersion(final long version) { ++ if (!DATACONVERTER_VERSIONS.contains(version)) { ++ throw new IllegalStateException("Version " + DataConverter.encodedToString(version) + " is not registered to have dataconverters, yet has a dataconverter"); ++ } ++ } ++ ++ private MCVersionRegistry() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCVersions.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCVersions.java +new file mode 100644 +index 0000000000000000000000000000000000000000..dbd232869253c4b06c09a52c2d7ad64fbbc3b71c +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCVersions.java +@@ -0,0 +1,525 @@ ++package ca.spottedleaf.dataconverter.minecraft; ++ ++@SuppressWarnings("unused") ++public final class MCVersions { ++ ++ /* https://minecraft.wiki/wiki/Data_version */ ++ ++ public static final int V15W32A = 100; ++ public static final int V15W32B = 103; ++ public static final int V15W32C = 104; ++ public static final int V15W33A = 111; ++ public static final int V15W33B = 111; ++ public static final int V15W33C = 112; ++ public static final int V15W34A = 114; ++ public static final int V15W34B = 115; ++ public static final int V15W34C = 116; ++ public static final int V15W34D = 117; ++ public static final int V15W35A = 118; ++ public static final int V15W35B = 119; ++ public static final int V15W35C = 120; ++ public static final int V15W35D = 121; ++ public static final int V15W35E = 122; ++ public static final int V15W36A = 123; ++ public static final int V15W36B = 124; ++ public static final int V15W36C = 125; ++ public static final int V15W36D = 126; ++ public static final int V15W37A = 127; ++ public static final int V15W38A = 128; ++ public static final int V15W38B = 129; ++ public static final int V15W39A = 130; ++ public static final int V15W39B = 131; ++ public static final int V15W39C = 132; ++ public static final int V15W40A = 133; ++ public static final int V15W40B = 134; ++ public static final int V15W41A = 136; ++ public static final int V15W41B = 137; ++ public static final int V15W42A = 138; ++ public static final int V15W43A = 139; ++ public static final int V15W43B = 140; ++ public static final int V15W43C = 141; ++ public static final int V15W44A = 142; ++ public static final int V15W44B = 143; ++ public static final int V15W45A = 145; ++ public static final int V15W46A = 146; ++ public static final int V15W47A = 148; ++ public static final int V15W47B = 149; ++ public static final int V15W47C = 150; ++ public static final int V15W49A = 151; ++ public static final int V15W49B = 152; ++ public static final int V15W50A = 153; ++ public static final int V15W51A = 154; ++ public static final int V15W51B = 155; ++ public static final int V16W02A = 156; ++ public static final int V16W03A = 157; ++ public static final int V16W04A = 158; ++ public static final int V16W05A = 159; ++ public static final int V16W05B = 160; ++ public static final int V16W06A = 161; ++ public static final int V16W07A = 162; ++ public static final int V16W07B = 163; ++ public static final int V1_9_PRE1 = 164; ++ public static final int V1_9_PRE2 = 165; ++ public static final int V1_9_PRE3 = 167; ++ public static final int V1_9_PRE4 = 168; ++ public static final int V1_9 = 169; ++ public static final int V1_9_1_PRE1 = 170; ++ public static final int V1_9_1_PRE2 = 171; ++ public static final int V1_9_1_PRE3 = 172; ++ public static final int V1_9_1 = 175; ++ public static final int V1_9_2 = 176; ++ public static final int V16W14A = 177; ++ public static final int V16W15A = 178; ++ public static final int V16W15B = 179; ++ public static final int V1_9_3_PRE1 = 180; ++ public static final int V1_9_3_PRE2 = 181; ++ public static final int V1_9_3_PRE3 = 182; ++ public static final int V1_9_3 = 183; ++ public static final int V1_9_4 = 184; ++ public static final int V16W20A = 501; ++ public static final int V16W21A = 503; ++ public static final int V16W21B = 504; ++ public static final int V1_10_PRE1 = 506; ++ public static final int V1_10_PRE2 = 507; ++ public static final int V1_10 = 510; ++ public static final int V1_10_1 = 511; ++ public static final int V1_10_2 = 512; ++ public static final int V16W32A = 800; ++ public static final int V16W32B = 801; ++ public static final int V16W33A = 802; ++ public static final int V16W35A = 803; ++ public static final int V16W36A = 805; ++ public static final int V16W38A = 807; ++ public static final int V16W39A = 809; ++ public static final int V16W39B = 811; ++ public static final int V16W39C = 812; ++ public static final int V16W40A = 813; ++ public static final int V16W41A = 814; ++ public static final int V16W42A = 815; ++ public static final int V16W43A = 816; ++ public static final int V16W44A = 817; ++ public static final int V1_11_PRE1 = 818; ++ public static final int V1_11 = 819; ++ public static final int V16W50A = 920; ++ public static final int V1_11_1 = 921; ++ public static final int V1_11_2 = 922; ++ public static final int V17W06A = 1022; ++ public static final int V17W13A = 1122; ++ public static final int V17W13B = 1123; ++ public static final int V17W14A = 1124; ++ public static final int V17W15A = 1125; ++ public static final int V17W16A = 1126; ++ public static final int V17W16B = 1127; ++ public static final int V17W17A = 1128; ++ public static final int V17W17B = 1129; ++ public static final int V17W18A = 1130; ++ public static final int V17W18B = 1131; ++ public static final int V1_12_PRE1 = 1132; ++ public static final int V1_12_PRE2 = 1133; ++ public static final int V1_12_PRE3 = 1134; ++ public static final int V1_12_PRE4 = 1135; ++ public static final int V1_12_PRE5 = 1136; ++ public static final int V1_12_PRE6 = 1137; ++ public static final int V1_12_PRE7 = 1138; ++ public static final int V1_12 = 1139; ++ public static final int V17W31A = 1239; ++ public static final int V1_12_1_PRE1 = 1240; ++ public static final int V1_12_1 = 1241; ++ public static final int V1_12_2_PRE1 = 1341; ++ public static final int V1_12_2_PRE2 = 1342; ++ public static final int V1_12_2 = 1343; ++ public static final int V17W43A = 1444; ++ public static final int V17W43B = 1445; ++ public static final int V17W45A = 1447; ++ public static final int V17W45B = 1448; ++ public static final int V17W46A = 1449; ++ public static final int V17W47A = 1451; ++ public static final int V17W47B = 1452; ++ public static final int V17W48A = 1453; ++ public static final int V17W49A = 1454; ++ public static final int V17W49B = 1455; ++ public static final int V17W50A = 1457; ++ public static final int V18W01A = 1459; ++ public static final int V18W02A = 1461; ++ public static final int V18W03A = 1462; ++ public static final int V18W03B = 1463; ++ public static final int V18W05A = 1464; ++ public static final int V18W06A = 1466; ++ public static final int V18W07A = 1467; ++ public static final int V18W07B = 1468; ++ public static final int V18W07C = 1469; ++ public static final int V18W08A = 1470; ++ public static final int V18W08B = 1471; ++ public static final int V18W09A = 1472; ++ public static final int V18W10A = 1473; ++ public static final int V18W10B = 1474; ++ public static final int V18W10C = 1476; ++ public static final int V18W10D = 1477; ++ public static final int V18W11A = 1478; ++ public static final int V18W14A = 1479; ++ public static final int V18W14B = 1481; ++ public static final int V18W15A = 1482; ++ public static final int V18W16A = 1483; ++ public static final int V18W19A = 1484; ++ public static final int V18W19B = 1485; ++ public static final int V18W20A = 1489; ++ public static final int V18W20B = 1491; ++ public static final int V18W20C = 1493; ++ public static final int V18W21A = 1495; ++ public static final int V18W21B = 1496; ++ public static final int V18W22A = 1497; ++ public static final int V18W22B = 1498; ++ public static final int V18W22C = 1499; ++ public static final int V1_13_PRE1 = 1501; ++ public static final int V1_13_PRE2 = 1502; ++ public static final int V1_13_PRE3 = 1503; ++ public static final int V1_13_PRE4 = 1504; ++ public static final int V1_13_PRE5 = 1511; ++ public static final int V1_13_PRE6 = 1512; ++ public static final int V1_13_PRE7 = 1513; ++ public static final int V1_13_PRE8 = 1516; ++ public static final int V1_13_PRE9 = 1517; ++ public static final int V1_13_PRE10 = 1518; ++ public static final int V1_13 = 1519; ++ public static final int V18W30A = 1620; ++ public static final int V18W30B = 1621; ++ public static final int V18W31A = 1622; ++ public static final int V18W32A = 1623; ++ public static final int V18W33A = 1625; ++ public static final int V1_13_1_PRE1 = 1626; ++ public static final int V1_13_1_PRE2 = 1627; ++ public static final int V1_13_1 = 1628; ++ public static final int V1_13_2_PRE1 = 1629; ++ public static final int V1_13_2_PRE2 = 1630; ++ public static final int V1_13_2 = 1631; ++ public static final int V18W43A = 1901; ++ public static final int V18W43B = 1902; ++ public static final int V18W43C = 1903; ++ public static final int V18W44A = 1907; ++ public static final int V18W45A = 1908; ++ public static final int V18W46A = 1910; ++ public static final int V18W47A = 1912; ++ public static final int V18W47B = 1913; ++ public static final int V18W48A = 1914; ++ public static final int V18W48B = 1915; ++ public static final int V18W49A = 1916; ++ public static final int V18W50A = 1919; ++ public static final int V19W02A = 1921; ++ public static final int V19W03A = 1922; ++ public static final int V19W03B = 1923; ++ public static final int V19W03C = 1924; ++ public static final int V19W04A = 1926; ++ public static final int V19W04B = 1927; ++ public static final int V19W05A = 1930; ++ public static final int V19W06A = 1931; ++ public static final int V19W07A = 1932; ++ public static final int V19W08A = 1933; ++ public static final int V19W08B = 1934; ++ public static final int V19W09A = 1935; ++ public static final int V19W11A = 1937; ++ public static final int V19W11B = 1938; ++ public static final int V19W12A = 1940; ++ public static final int V19W12B = 1941; ++ public static final int V19W13A = 1942; ++ public static final int V19W13B = 1943; ++ public static final int V19W14A = 1944; ++ public static final int V19W14B = 1945; ++ public static final int V1_14_PRE1 = 1947; ++ public static final int V1_14_PRE2 = 1948; ++ public static final int V1_14_PRE3 = 1949; ++ public static final int V1_14_PRE4 = 1950; ++ public static final int V1_14_PRE5 = 1951; ++ public static final int V1_14 = 1952; ++ public static final int V1_14_1_PRE1 = 1955; ++ public static final int V1_14_1_PRE2 = 1956; ++ public static final int V1_14_1 = 1957; ++ public static final int V1_14_2_PRE1 = 1958; ++ public static final int V1_14_2_PRE2 = 1959; ++ public static final int V1_14_2_PRE3 = 1960; ++ public static final int V1_14_2_PRE4 = 1962; ++ public static final int V1_14_2 = 1963; ++ public static final int V1_14_3_PRE1 = 1964; ++ public static final int V1_14_3_PRE2 = 1965; ++ public static final int V1_14_3_PRE3 = 1966; ++ public static final int V1_14_3_PRE4 = 1967; ++ public static final int V1_14_3 = 1968; ++ public static final int V1_14_4_PRE1 = 1969; ++ public static final int V1_14_4_PRE2 = 1970; ++ public static final int V1_14_4_PRE3 = 1971; ++ public static final int V1_14_4_PRE4 = 1972; ++ public static final int V1_14_4_PRE5 = 1973; ++ public static final int V1_14_4_PRE6 = 1974; ++ public static final int V1_14_4_PRE7 = 1975; ++ public static final int V1_14_4 = 1976; ++ public static final int V19W34A = 2200; ++ public static final int V19W35A = 2201; ++ public static final int V19W36A = 2203; ++ public static final int V19W37A = 2204; ++ public static final int V19W38A = 2205; ++ public static final int V19W38B = 2206; ++ public static final int V19W39A = 2207; ++ public static final int V19W40A = 2208; ++ public static final int V19W41A = 2210; ++ public static final int V19W42A = 2212; ++ public static final int V19W44A = 2213; ++ public static final int V19W45A = 2214; ++ public static final int V19W45B = 2215; ++ public static final int V19W46A = 2216; ++ public static final int V19W46B = 2217; ++ public static final int V1_15_PRE1 = 2218; ++ public static final int V1_15_PRE2 = 2219; ++ public static final int V1_15_PRE3 = 2220; ++ public static final int V1_15_PRE4 = 2221; ++ public static final int V1_15_PRE5 = 2222; ++ public static final int V1_15_PRE6 = 2223; ++ public static final int V1_15_PRE7 = 2224; ++ public static final int V1_15 = 2225; ++ public static final int V1_15_1_PRE1 = 2226; ++ public static final int V1_15_1 = 2227; ++ public static final int V1_15_2_PRE1 = 2228; ++ public static final int V1_15_2_PRE2 = 2229; ++ public static final int V1_15_2 = 2230; ++ public static final int V20W06A = 2504; ++ public static final int V20W07A = 2506; ++ public static final int V20W08A = 2507; ++ public static final int V20W09A = 2510; ++ public static final int V20W10A = 2512; ++ public static final int V20W11A = 2513; ++ public static final int V20W12A = 2515; ++ public static final int V20W13A = 2520; ++ public static final int V20W13B = 2521; ++ public static final int V20W14A = 2524; ++ public static final int V20W15A = 2525; ++ public static final int V20W16A = 2526; ++ public static final int V20W17A = 2529; ++ public static final int V20W18A = 2532; ++ public static final int V20W19A = 2534; ++ public static final int V20W20A = 2536; ++ public static final int V20W20B = 2537; ++ public static final int V20W21A = 2554; ++ public static final int V20W22A = 2555; ++ public static final int V1_16_PRE1 = 2556; ++ public static final int V1_16_PRE2 = 2557; ++ public static final int V1_16_PRE3 = 2559; ++ public static final int V1_16_PRE4 = 2560; ++ public static final int V1_16_PRE5 = 2561; ++ public static final int V1_16_PRE6 = 2562; ++ public static final int V1_16_PRE7 = 2563; ++ public static final int V1_16_PRE8 = 2564; ++ public static final int V1_16_RC1 = 2565; ++ public static final int V1_16 = 2566; ++ public static final int V1_16_1 = 2567; ++ public static final int V20W27A = 2569; ++ public static final int V20W28A = 2570; ++ public static final int V20W29A = 2571; ++ public static final int V20W30A = 2572; ++ public static final int V1_16_2_PRE1 = 2573; ++ public static final int V1_16_2_PRE2 = 2574; ++ public static final int V1_16_2_PRE3 = 2575; ++ public static final int V1_16_2_RC1 = 2576; ++ public static final int V1_16_2_RC2 = 2577; ++ public static final int V1_16_2 = 2578; ++ public static final int V1_16_3_RC1 = 2579; ++ public static final int V1_16_3 = 2580; ++ public static final int V1_16_4_PRE1 = 2581; ++ public static final int V1_16_4_PRE2 = 2582; ++ public static final int V1_16_4_RC1 = 2583; ++ public static final int V1_16_4 = 2584; ++ public static final int V1_16_5_RC1 = 2585; ++ public static final int V1_16_5 = 2586; ++ public static final int V20W45A = 2681; ++ public static final int V20W46A = 2682; ++ public static final int V20W48A = 2683; ++ public static final int V20W49A = 2685; ++ public static final int V20W51A = 2687; ++ public static final int V21W03A = 2689; ++ public static final int V21W05A = 2690; ++ public static final int V21W05B = 2692; ++ public static final int V21W06A = 2694; ++ public static final int V21W07A = 2695; ++ public static final int V21W08A = 2697; ++ public static final int V21W08B = 2698; ++ public static final int V21W10A = 2699; ++ public static final int V21W11A = 2703; ++ public static final int V21W13A = 2705; ++ public static final int V21W14A = 2706; ++ public static final int V21W15A = 2709; ++ public static final int V21W16A = 2711; ++ public static final int V21W17A = 2712; ++ public static final int V21W18A = 2713; ++ public static final int V21W19A = 2714; ++ public static final int V21W20A = 2715; ++ public static final int V1_17_PRE1 = 2716; ++ public static final int V1_17_PRE2 = 2718; ++ public static final int V1_17_PRE3 = 2719; ++ public static final int V1_17_PRE4 = 2720; ++ public static final int V1_17_PRE5 = 2721; ++ public static final int V1_17_RC1 = 2722; ++ public static final int V1_17_RC2 = 2723; ++ public static final int V1_17 = 2724; ++ public static final int V1_17_1_PRE1 = 2725; ++ public static final int V1_17_1_PRE2 = 2726; ++ public static final int V1_17_1_PRE3 = 2727; ++ public static final int V1_17_1_RC1 = 2728; ++ public static final int V1_17_1_RC2 = 2729; ++ public static final int V1_17_1 = 2730; ++ public static final int V21W37A = 2834; ++ public static final int V21W38A = 2835; ++ public static final int V21W39A = 2836; ++ public static final int V21W40A = 2838; ++ public static final int V21W41A = 2839; ++ public static final int V21W42A = 2840; ++ public static final int V21W43A = 2844; ++ public static final int V21W44A = 2845; ++ public static final int V1_18_PRE1 = 2847; ++ public static final int V1_18_PRE2 = 2848; ++ public static final int V1_18_PRE3 = 2849; ++ public static final int V1_18_PRE4 = 2850; ++ public static final int V1_18_PRE5 = 2851; ++ public static final int V1_18_PRE6 = 2853; ++ public static final int V1_18_PRE7 = 2854; ++ public static final int V1_18_PRE8 = 2855; ++ public static final int V1_18_RC1 = 2856; ++ public static final int V1_18_RC2 = 2857; ++ public static final int V1_18_RC3 = 2858; ++ public static final int V1_18_RC4 = 2859; ++ public static final int V1_18 = 2860; ++ public static final int V1_18_1_PRE1 = 2861; ++ public static final int V1_18_1_RC1 = 2862; ++ public static final int V1_18_1_RC2 = 2863; ++ public static final int V1_18_1_RC3 = 2864; ++ public static final int V1_18_1 = 2865; ++ public static final int V22W03A = 2966; ++ public static final int V22W05A = 2967; ++ public static final int V22W06A = 2968; ++ public static final int V22W07A = 2969; ++ public static final int V1_18_2_PRE1 = 2971; ++ public static final int V1_18_2_PRE2 = 2972; ++ public static final int V1_18_2_PRE3 = 2973; ++ public static final int V1_18_2_RC1 = 2974; ++ public static final int V1_18_2 = 2975; ++ public static final int V22W11A = 3080; ++ public static final int V22W12A = 3082; ++ public static final int V22W13A = 3085; ++ public static final int V22W14A = 3088; ++ public static final int V22W15A = 3089; ++ public static final int V22W16A = 3091; ++ public static final int V22W16B = 3092; ++ public static final int V22W17A = 3093; ++ public static final int V22W18A = 3095; ++ public static final int V22W19A = 3096; ++ public static final int V1_19_PRE1 = 3098; ++ public static final int V1_19_PRE2 = 3099; ++ public static final int V1_19_PRE3 = 3100; ++ public static final int V1_19_PRE4 = 3101; ++ public static final int V1_19_PRE5 = 3102; ++ public static final int V1_19_RC1 = 3103; ++ public static final int V1_19_RC2 = 3104; ++ public static final int V1_19 = 3105; ++ public static final int V22W24A = 3106; ++ public static final int V1_19_1_PRE1 = 3107; ++ public static final int V1_19_1_RC1 = 3109; ++ public static final int V1_19_1_PRE2 = 3110; ++ public static final int V1_19_1_PRE3 = 3111; ++ public static final int V1_19_1_PRE4 = 3112; ++ public static final int V1_19_1_PRE5 = 3113; ++ public static final int V1_19_1_PRE6 = 3114; ++ public static final int V1_19_1_RC2 = 3115; ++ public static final int V1_19_1_RC3 = 3116; ++ public static final int V1_19_1 = 3117; ++ public static final int V1_19_2_RC1 = 3118; ++ public static final int V1_19_2_RC2 = 3119; ++ public static final int V1_19_2 = 3120; ++ public static final int V22W42A = 3205; ++ public static final int V22W43A = 3206; ++ public static final int V22W44A = 3207; ++ public static final int V22W45A = 3208; ++ public static final int V22W46A = 3210; ++ public static final int V1_19_3_PRE1 = 3211; ++ public static final int V1_19_3_PRE2 = 3212; ++ public static final int V1_19_3_PRE3 = 3213; ++ public static final int V1_19_3_RC1 = 3215; ++ public static final int V1_19_3 = 3218; ++ public static final int V23W03A = 3320; ++ public static final int V23W04A = 3321; ++ public static final int V23W05A = 3323; ++ public static final int V23W06A = 3326; ++ public static final int V23W07A = 3329; ++ public static final int V1_19_4_PRE1 = 3330; ++ public static final int V1_19_4_PRE2 = 3331; ++ public static final int V1_19_4_PRE3 = 3332; ++ public static final int V1_19_4_PRE4 = 3333; ++ public static final int V1_19_4_RC1 = 3334; ++ public static final int V1_19_4_RC2 = 3335; ++ public static final int V1_19_4_RC3 = 3336; ++ public static final int V1_19_4 = 3337; ++ public static final int V23W12A = 3442; ++ public static final int V23W13A = 3443; ++ public static final int V23W14A = 3445; ++ public static final int V23W16A = 3449; ++ public static final int V23W17A = 3452; ++ public static final int V23W19A = 3453; ++ public static final int V1_20_PRE1 = 3454; ++ public static final int V1_20_PRE2 = 3455; ++ public static final int V1_20_PRE3 = 3456; ++ public static final int V1_20_PRE4 = 3457; ++ public static final int V1_20_PRE5 = 3458; ++ public static final int V1_20_PRE6 = 3460; ++ public static final int V1_20_PRE7 = 3461; ++ public static final int V1_20_RC1 = 3462; ++ public static final int V1_20 = 3463; ++ public static final int V1_20_1_RC1 = 3464; ++ public static final int V1_20_1 = 3465; ++ public static final int V23W31A = 3567; ++ public static final int V23W32A = 3569; ++ public static final int V23W33A = 3570; ++ public static final int V23W35A = 3571; ++ public static final int V1_20_2_PRE1 = 3572; ++ public static final int V1_20_2_PRE2 = 3573; ++ public static final int V1_20_2_PRE3 = 3574; ++ public static final int V1_20_2_PRE4 = 3575; ++ public static final int V1_20_2_RC1 = 3576; ++ public static final int V1_20_2_RC2 = 3577; ++ public static final int V1_20_2 = 3578; ++ public static final int V23W40A = 3679; ++ public static final int V23W41A = 3681; ++ public static final int V23W42A = 3684; ++ public static final int V23W43A = 3686; ++ public static final int V23W43B = 3687; ++ public static final int V23W44A = 3688; ++ public static final int V23W45A = 3690; ++ public static final int V23W46A = 3691; ++ public static final int V1_20_3_PRE1 = 3693; ++ public static final int V1_20_3_PRE2 = 3694; ++ public static final int V1_20_3_PRE3 = 3695; ++ public static final int V1_20_3_PRE4 = 3696; ++ public static final int V1_20_3_RC1 = 3697; ++ public static final int V1_20_3 = 3698; ++ public static final int V1_20_4_RC1 = 3699; ++ public static final int V1_20_4 = 3700; ++ public static final int V23W51A = 3801; ++ public static final int V23W51B = 3802; ++ public static final int V24W03A = 3804; ++ public static final int V24W03B = 3805; ++ public static final int V24W04A = 3806; ++ public static final int V24W05A = 3809; ++ public static final int V24W05B = 3811; ++ public static final int V24W06A = 3815; ++ public static final int V24W07A = 3817; ++ public static final int V24W09A = 3819; ++ public static final int V24W10A = 3821; ++ public static final int V24W11A = 3823; ++ public static final int V24W12A = 3824; ++ public static final int V24W13A = 3826; ++ public static final int V24W14A = 3827; ++ public static final int V1_20_5_PRE1 = 3829; ++ public static final int V1_20_5_PRE2 = 3830; ++ public static final int V1_20_5_PRE3 = 3831; ++ public static final int V1_20_5_PRE4 = 3832; ++ public static final int V1_20_5_RC1 = 3834; ++ public static final int V1_20_5_RC2 = 3835; ++ public static final int V1_20_5_RC3 = 3836; ++ public static final int V1_20_5 = 3837; ++ ++ private MCVersions() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterAbstractAdvancementsRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterAbstractAdvancementsRename.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ae3aed21c1fccb688e9a1665e2d317a77508d157 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterAbstractAdvancementsRename.java +@@ -0,0 +1,28 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.advancements; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import java.util.ArrayList; ++import java.util.function.Function; ++ ++public final class ConverterAbstractAdvancementsRename { ++ ++ private ConverterAbstractAdvancementsRename() {} ++ ++ public static void register(final int version, final Function renamer) { ++ register(version, 0, renamer); ++ } ++ ++ public static void register(final int version, final int subVersion, final Function renamer) { ++ MCTypeRegistry.ADVANCEMENTS.addStructureConverter(new DataConverter<>(version, subVersion) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ RenameHelper.renameKeys(data, renamer); ++ return null; ++ } ++ }); ++ } ++ ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterCriteriaRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterCriteriaRename.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b2a4d16e6a2f9d71dbfa692922671581c2bec136 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterCriteriaRename.java +@@ -0,0 +1,42 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.advancements; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.types.MapType; ++import java.util.function.Function; ++ ++public final class ConverterCriteriaRename extends DataConverter, MapType> { ++ ++ public final String path; ++ public final Function conversion; ++ ++ public ConverterCriteriaRename(final int toVersion, final String path, final Function conversion) { ++ super(toVersion); ++ this.path = path; ++ this.conversion = conversion; ++ } ++ ++ public ConverterCriteriaRename(final int toVersion, final int versionStep, final String path, final Function conversion) { ++ super(toVersion, versionStep); ++ this.path = path; ++ this.conversion = conversion; ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType advancement = data.getMap(this.path); ++ if (advancement == null) { ++ return null; ++ } ++ ++ final MapType criteria = advancement.getMap("criteria"); ++ if (criteria == null) { ++ return null; ++ } ++ ++ RenameHelper.renameKeys(criteria, this.conversion); ++ ++ return null; ++ } ++ ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractAttributesRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractAttributesRename.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c465036923656e8c974cc447bb57169b1a882e88 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractAttributesRename.java +@@ -0,0 +1,57 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.attributes; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import java.util.function.Function; ++ ++public final class ConverterAbstractAttributesRename { ++ ++ public static void register(final int version, final Function renamer) { ++ register(version, 0, renamer); ++ } ++ ++ public static void register(final int version, final int versionStep, final Function renamer) { ++ final DataConverter, MapType> entityConverter = new DataConverter<>(version, versionStep) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final ListType attributes = data.getList("Attributes", ObjectType.MAP); ++ ++ if (attributes == null) { ++ return null; ++ } ++ ++ for (int i = 0, len = attributes.size(); i < len; ++i) { ++ RenameHelper.renameString(attributes.getMap(i), "Name", renamer); ++ } ++ ++ return null; ++ } ++ }; ++ ++ MCTypeRegistry.ENTITY.addStructureConverter(entityConverter); ++ MCTypeRegistry.PLAYER.addStructureConverter(entityConverter); ++ ++ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(version, versionStep) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final ListType attributes = data.getList("AttributeModifiers", ObjectType.MAP); ++ ++ if (attributes == null) { ++ return null; ++ } ++ ++ for (int i = 0, len = attributes.size(); i < len; ++i) { ++ RenameHelper.renameString(attributes.getMap(i), "AttributeName", renamer); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private ConverterAbstractAttributesRename() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/blockname/ConverterAbstractBlockRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/blockname/ConverterAbstractBlockRename.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7b47879a7c2e8c21fae43bf5247585c716d75565 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/blockname/ConverterAbstractBlockRename.java +@@ -0,0 +1,64 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.blockname; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import java.util.function.Function; ++ ++public final class ConverterAbstractBlockRename { ++ ++ private ConverterAbstractBlockRename() {} ++ ++ public static void register(final int version, final Function renamer) { ++ register(version, 0, renamer); ++ } ++ ++ public static void register(final int version, final int subVersion, final Function renamer) { ++ ConverterAbstractStringValueTypeRename.register(version, subVersion, MCTypeRegistry.BLOCK_NAME, renamer); ++ MCTypeRegistry.BLOCK_STATE.addStructureConverter(new DataConverter<>(version, subVersion) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final String name = data.getString("Name"); ++ if (name != null) { ++ final String converted = renamer.apply(name); ++ if (converted != null) { ++ data.setString("Name", converted); ++ } ++ } ++ return null; ++ } ++ }); ++ MCTypeRegistry.FLAT_BLOCK_STATE.addConverter(new DataConverter<>(version, subVersion) { ++ @Override ++ public Object convert(final Object data, final long sourceVersion, final long toVersion) { ++ if (!(data instanceof String string)) { ++ return null; ++ } ++ ++ if (string.isEmpty()) { ++ return null; ++ } ++ ++ final int nbtStart1 = string.indexOf('['); ++ final int nbtStart2 = string.indexOf('{'); ++ int stateNameEnd = string.length(); ++ if (nbtStart1 > 0) { ++ stateNameEnd = nbtStart1; ++ } ++ ++ if (nbtStart2 > 0) { ++ stateNameEnd = Math.min(stateNameEnd, nbtStart2); ++ } ++ ++ final String blockStateName = string.substring(0, stateNameEnd); ++ final String converted = renamer.apply(blockStateName); ++ if (converted == null) { ++ return null; ++ } ++ ++ return converted.concat(string.substring(stateNameEnd)); ++ } ++ }); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterAddBlendingData.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterAddBlendingData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d4cd5362e77eb71cb8eb45ffcc73185e01be1157 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterAddBlendingData.java +@@ -0,0 +1,65 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.chunk; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.Types; ++import ca.spottedleaf.dataconverter.util.NamespaceUtil; ++import java.util.Arrays; ++import java.util.HashSet; ++import java.util.Set; ++ ++public final class ConverterAddBlendingData extends DataConverter, MapType> { ++ ++ private static final Set STATUSES_TO_SKIP_BLENDING = new HashSet<>( ++ Arrays.asList( ++ "minecraft:empty", ++ "minecraft:structure_starts", ++ "minecraft:structure_references", ++ "minecraft:biomes" ++ ) ++ ); ++ ++ public ConverterAddBlendingData(final int toVersion) { ++ super(toVersion); ++ } ++ ++ public ConverterAddBlendingData(final int toVersion, final int versionStep) { ++ super(toVersion, versionStep); ++ } ++ ++ private static MapType createBlendingData(final int height, final int minY) { ++ final MapType ret = Types.NBT.createEmptyMap(); ++ ++ ret.setInt("min_section", minY >> 4); ++ ret.setInt("max_section", (minY + height) >> 4); ++ ++ return ret; ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ data.remove("blending_data"); ++ final MapType context = data.getMap("__context"); ++ if (!"minecraft:overworld".equals(context == null ? null : context.getString("dimension"))) { ++ return null; ++ } ++ ++ final String status = NamespaceUtil.correctNamespace(data.getString("Status")); ++ if (status == null) { ++ return null; ++ } ++ ++ final MapType belowZeroRetrogen = data.getMap("below_zero_retrogen"); ++ ++ if (!STATUSES_TO_SKIP_BLENDING.contains(status)) { ++ data.setMap("blending_data", createBlendingData(384, -64)); ++ } else if (belowZeroRetrogen != null) { ++ final String realStatus = NamespaceUtil.correctNamespace(belowZeroRetrogen.getString("target_status", "empty")); ++ if (!STATUSES_TO_SKIP_BLENDING.contains(realStatus)) { ++ data.setMap("blending_data", createBlendingData(256, 0)); ++ } ++ } ++ ++ return null; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterFlattenChunk.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterFlattenChunk.java +new file mode 100644 +index 0000000000000000000000000000000000000000..300c2d14818b1e0cfe7341aba573ec75d0581b26 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterFlattenChunk.java +@@ -0,0 +1,1016 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.chunk; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.HelperBlockFlatteningV1450; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.HelperItemNameV102; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.Types; ++import com.mojang.datafixers.DataFixUtils; ++import com.mojang.logging.LogUtils; ++import it.unimi.dsi.fastutil.ints.Int2ObjectLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.ints.Int2ObjectMap; ++import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.ints.IntArrayList; ++import it.unimi.dsi.fastutil.ints.IntIterator; ++import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; ++import net.minecraft.util.datafix.PackedBitStorage; ++import org.slf4j.Logger; ++import java.util.Arrays; ++import java.util.BitSet; ++import java.util.HashMap; ++import java.util.Iterator; ++import java.util.Map; ++import java.util.Objects; ++ ++import static it.unimi.dsi.fastutil.HashCommon.arraySize; ++ ++public final class ConverterFlattenChunk extends DataConverter, MapType> { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ static final BitSet VIRTUAL_SET = new BitSet(256); ++ static final BitSet IDS_NEEDING_FIX_SET = new BitSet(256); ++ ++ static { ++ IDS_NEEDING_FIX_SET.set(2); ++ IDS_NEEDING_FIX_SET.set(3); ++ IDS_NEEDING_FIX_SET.set(110); ++ IDS_NEEDING_FIX_SET.set(140); ++ IDS_NEEDING_FIX_SET.set(144); ++ IDS_NEEDING_FIX_SET.set(25); ++ IDS_NEEDING_FIX_SET.set(86); ++ IDS_NEEDING_FIX_SET.set(26); ++ IDS_NEEDING_FIX_SET.set(176); ++ IDS_NEEDING_FIX_SET.set(177); ++ IDS_NEEDING_FIX_SET.set(175); ++ IDS_NEEDING_FIX_SET.set(64); ++ IDS_NEEDING_FIX_SET.set(71); ++ IDS_NEEDING_FIX_SET.set(193); ++ IDS_NEEDING_FIX_SET.set(194); ++ IDS_NEEDING_FIX_SET.set(195); ++ IDS_NEEDING_FIX_SET.set(196); ++ IDS_NEEDING_FIX_SET.set(197); ++ ++ VIRTUAL_SET.set(54); ++ VIRTUAL_SET.set(146); ++ VIRTUAL_SET.set(25); ++ VIRTUAL_SET.set(26); ++ VIRTUAL_SET.set(51); ++ VIRTUAL_SET.set(53); ++ VIRTUAL_SET.set(67); ++ VIRTUAL_SET.set(108); ++ VIRTUAL_SET.set(109); ++ VIRTUAL_SET.set(114); ++ VIRTUAL_SET.set(128); ++ VIRTUAL_SET.set(134); ++ VIRTUAL_SET.set(135); ++ VIRTUAL_SET.set(136); ++ VIRTUAL_SET.set(156); ++ VIRTUAL_SET.set(163); ++ VIRTUAL_SET.set(164); ++ VIRTUAL_SET.set(180); ++ VIRTUAL_SET.set(203); ++ VIRTUAL_SET.set(55); ++ VIRTUAL_SET.set(85); ++ VIRTUAL_SET.set(113); ++ VIRTUAL_SET.set(188); ++ VIRTUAL_SET.set(189); ++ VIRTUAL_SET.set(190); ++ VIRTUAL_SET.set(191); ++ VIRTUAL_SET.set(192); ++ VIRTUAL_SET.set(93); ++ VIRTUAL_SET.set(94); ++ VIRTUAL_SET.set(101); ++ VIRTUAL_SET.set(102); ++ VIRTUAL_SET.set(160); ++ VIRTUAL_SET.set(106); ++ VIRTUAL_SET.set(107); ++ VIRTUAL_SET.set(183); ++ VIRTUAL_SET.set(184); ++ VIRTUAL_SET.set(185); ++ VIRTUAL_SET.set(186); ++ VIRTUAL_SET.set(187); ++ VIRTUAL_SET.set(132); ++ VIRTUAL_SET.set(139); ++ VIRTUAL_SET.set(199); ++ } ++ ++ static final boolean[] VIRTUAL = toBooleanArray(VIRTUAL_SET); ++ static final boolean[] IDS_NEEDING_FIX = toBooleanArray(IDS_NEEDING_FIX_SET); ++ ++ private static boolean[] toBooleanArray(final BitSet set) { ++ final boolean[] ret = new boolean[4096]; ++ for (int i = 0; i < 4096; ++i) { ++ ret[i] = set.get(i); ++ } ++ ++ return ret; ++ } ++ ++ static final MapType PUMPKIN = HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:pumpkin'}"); ++ static final MapType SNOWY_PODZOL = HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:podzol',Properties:{snowy:'true'}}"); ++ static final MapType SNOWY_GRASS = HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:grass_block',Properties:{snowy:'true'}}"); ++ static final MapType SNOWY_MYCELIUM = HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:mycelium',Properties:{snowy:'true'}}"); ++ static final MapType UPPER_SUNFLOWER = HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:sunflower',Properties:{half:'upper'}}"); ++ static final MapType UPPER_LILAC = HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:lilac',Properties:{half:'upper'}}"); ++ static final MapType UPPER_TALL_GRASS = HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:tall_grass',Properties:{half:'upper'}}"); ++ static final MapType UPPER_LARGE_FERN = HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:large_fern',Properties:{half:'upper'}}"); ++ static final MapType UPPER_ROSE_BUSH = HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:rose_bush',Properties:{half:'upper'}}"); ++ static final MapType UPPER_PEONY = HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:peony',Properties:{half:'upper'}}"); ++ ++ static final Map> FLOWER_POT_MAP = new HashMap<>(); ++ static { ++ FLOWER_POT_MAP.put("minecraft:air0", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:flower_pot'}")); ++ FLOWER_POT_MAP.put("minecraft:red_flower0", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_poppy'}")); ++ FLOWER_POT_MAP.put("minecraft:red_flower1", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_blue_orchid'}")); ++ FLOWER_POT_MAP.put("minecraft:red_flower2", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_allium'}")); ++ FLOWER_POT_MAP.put("minecraft:red_flower3", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_azure_bluet'}")); ++ FLOWER_POT_MAP.put("minecraft:red_flower4", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_red_tulip'}")); ++ FLOWER_POT_MAP.put("minecraft:red_flower5", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_orange_tulip'}")); ++ FLOWER_POT_MAP.put("minecraft:red_flower6", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_white_tulip'}")); ++ FLOWER_POT_MAP.put("minecraft:red_flower7", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_pink_tulip'}")); ++ FLOWER_POT_MAP.put("minecraft:red_flower8", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_oxeye_daisy'}")); ++ FLOWER_POT_MAP.put("minecraft:yellow_flower0", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_dandelion'}")); ++ FLOWER_POT_MAP.put("minecraft:sapling0", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_oak_sapling'}")); ++ FLOWER_POT_MAP.put("minecraft:sapling1", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_spruce_sapling'}")); ++ FLOWER_POT_MAP.put("minecraft:sapling2", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_birch_sapling'}")); ++ FLOWER_POT_MAP.put("minecraft:sapling3", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_jungle_sapling'}")); ++ FLOWER_POT_MAP.put("minecraft:sapling4", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_acacia_sapling'}")); ++ FLOWER_POT_MAP.put("minecraft:sapling5", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_dark_oak_sapling'}")); ++ FLOWER_POT_MAP.put("minecraft:red_mushroom0", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_red_mushroom'}")); ++ FLOWER_POT_MAP.put("minecraft:brown_mushroom0", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_brown_mushroom'}")); ++ FLOWER_POT_MAP.put("minecraft:deadbush0", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_dead_bush'}")); ++ FLOWER_POT_MAP.put("minecraft:tallgrass2", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_fern'}")); ++ FLOWER_POT_MAP.put("minecraft:cactus0", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_cactus'}")); // we change default to empty ++ } ++ ++ static final Map> SKULL_MAP = new HashMap<>(); ++ static { ++ mapSkull(SKULL_MAP, 0, "skeleton", "skull"); ++ mapSkull(SKULL_MAP, 1, "wither_skeleton", "skull"); ++ mapSkull(SKULL_MAP, 2, "zombie", "head"); ++ mapSkull(SKULL_MAP, 3, "player", "head"); ++ mapSkull(SKULL_MAP, 4, "creeper", "head"); ++ mapSkull(SKULL_MAP, 5, "dragon", "head"); ++ }; ++ ++ private static void mapSkull(final Map> into, final int oldId, final String newId, final String skullType) { ++ into.put(oldId + "north", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + newId + "_wall_" + skullType + "',Properties:{facing:'north'}}")); ++ into.put(oldId + "east", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + newId + "_wall_" + skullType + "',Properties:{facing:'east'}}")); ++ into.put(oldId + "south", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + newId + "_wall_" + skullType + "',Properties:{facing:'south'}}")); ++ into.put(oldId + "west", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + newId + "_wall_" + skullType + "',Properties:{facing:'west'}}")); ++ ++ for (int rotation = 0; rotation < 16; ++rotation) { ++ into.put(oldId + "" + rotation, ++ HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + newId + "_" + skullType + "',Properties:{rotation:'" + rotation + "'}}")); ++ } ++ } ++ ++ static final Map> DOOR_MAP = new HashMap<>(); ++ static { ++ mapDoor(DOOR_MAP, "oak_door", 1024); ++ mapDoor(DOOR_MAP, "iron_door", 1136); ++ mapDoor(DOOR_MAP, "spruce_door", 3088); ++ mapDoor(DOOR_MAP, "birch_door", 3104); ++ mapDoor(DOOR_MAP, "jungle_door", 3120); ++ mapDoor(DOOR_MAP, "acacia_door", 3136); ++ mapDoor(DOOR_MAP, "dark_oak_door", 3152); ++ }; ++ ++ private static void mapDoor(final Map> into, final String type, final int oldId) { ++ into.put("minecraft:" + type + "eastlowerleftfalsefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'false'}}")); ++ into.put("minecraft:" + type + "eastlowerleftfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'true'}}")); ++ into.put("minecraft:" + type + "eastlowerlefttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'false'}}")); ++ into.put("minecraft:" + type + "eastlowerlefttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'true'}}")); ++ into.put("minecraft:" + type + "eastlowerrightfalsefalse", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId))); ++ into.put("minecraft:" + type + "eastlowerrightfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'true'}}")); ++ into.put("minecraft:" + type + "eastlowerrighttruefalse", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 4))); ++ into.put("minecraft:" + type + "eastlowerrighttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'true'}}")); ++ into.put("minecraft:" + type + "eastupperleftfalsefalse", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 8))); ++ into.put("minecraft:" + type + "eastupperleftfalsetrue", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 10))); ++ into.put("minecraft:" + type + "eastupperlefttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'false'}}")); ++ into.put("minecraft:" + type + "eastupperlefttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'true'}}")); ++ into.put("minecraft:" + type + "eastupperrightfalsefalse", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 9))); ++ into.put("minecraft:" + type + "eastupperrightfalsetrue", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 11))); ++ into.put("minecraft:" + type + "eastupperrighttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'false'}}")); ++ into.put("minecraft:" + type + "eastupperrighttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'true'}}")); ++ into.put("minecraft:" + type + "northlowerleftfalsefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'false'}}")); ++ into.put("minecraft:" + type + "northlowerleftfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'true'}}")); ++ into.put("minecraft:" + type + "northlowerlefttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'false'}}")); ++ into.put("minecraft:" + type + "northlowerlefttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'true'}}")); ++ into.put("minecraft:" + type + "northlowerrightfalsefalse", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 3))); ++ into.put("minecraft:" + type + "northlowerrightfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'true'}}")); ++ into.put("minecraft:" + type + "northlowerrighttruefalse", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 7))); ++ into.put("minecraft:" + type + "northlowerrighttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'true'}}")); ++ into.put("minecraft:" + type + "northupperleftfalsefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'false'}}")); ++ into.put("minecraft:" + type + "northupperleftfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'true'}}")); ++ into.put("minecraft:" + type + "northupperlefttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'false'}}")); ++ into.put("minecraft:" + type + "northupperlefttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'true'}}")); ++ into.put("minecraft:" + type + "northupperrightfalsefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'false'}}")); ++ into.put("minecraft:" + type + "northupperrightfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'true'}}")); ++ into.put("minecraft:" + type + "northupperrighttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'false'}}")); ++ into.put("minecraft:" + type + "northupperrighttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'true'}}")); ++ into.put("minecraft:" + type + "southlowerleftfalsefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'false'}}")); ++ into.put("minecraft:" + type + "southlowerleftfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'true'}}")); ++ into.put("minecraft:" + type + "southlowerlefttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'false'}}")); ++ into.put("minecraft:" + type + "southlowerlefttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'true'}}")); ++ into.put("minecraft:" + type + "southlowerrightfalsefalse", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 1))); ++ into.put("minecraft:" + type + "southlowerrightfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'true'}}")); ++ into.put("minecraft:" + type + "southlowerrighttruefalse", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 5))); ++ into.put("minecraft:" + type + "southlowerrighttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'true'}}")); ++ into.put("minecraft:" + type + "southupperleftfalsefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'false'}}")); ++ into.put("minecraft:" + type + "southupperleftfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'true'}}")); ++ into.put("minecraft:" + type + "southupperlefttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'false'}}")); ++ into.put("minecraft:" + type + "southupperlefttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'true'}}")); ++ into.put("minecraft:" + type + "southupperrightfalsefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'false'}}")); ++ into.put("minecraft:" + type + "southupperrightfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'true'}}")); ++ into.put("minecraft:" + type + "southupperrighttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'false'}}")); ++ into.put("minecraft:" + type + "southupperrighttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'true'}}")); ++ into.put("minecraft:" + type + "westlowerleftfalsefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'false'}}")); ++ into.put("minecraft:" + type + "westlowerleftfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'true'}}")); ++ into.put("minecraft:" + type + "westlowerlefttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'false'}}")); ++ into.put("minecraft:" + type + "westlowerlefttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'true'}}")); ++ into.put("minecraft:" + type + "westlowerrightfalsefalse", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 2))); ++ into.put("minecraft:" + type + "westlowerrightfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'true'}}")); ++ into.put("minecraft:" + type + "westlowerrighttruefalse", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 6))); ++ into.put("minecraft:" + type + "westlowerrighttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'true'}}")); ++ into.put("minecraft:" + type + "westupperleftfalsefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'false'}}")); ++ into.put("minecraft:" + type + "westupperleftfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'true'}}")); ++ into.put("minecraft:" + type + "westupperlefttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'false'}}")); ++ into.put("minecraft:" + type + "westupperlefttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'true'}}")); ++ into.put("minecraft:" + type + "westupperrightfalsefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'false'}}")); ++ into.put("minecraft:" + type + "westupperrightfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'true'}}")); ++ into.put("minecraft:" + type + "westupperrighttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'false'}}")); ++ into.put("minecraft:" + type + "westupperrighttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'true'}}")); ++ } ++ ++ static final Map> NOTE_BLOCK_MAP = new HashMap<>(); ++ static { ++ for(int note = 0; note < 26; ++note) { ++ NOTE_BLOCK_MAP.put("true" + note, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:note_block',Properties:{powered:'true',note:'" + note + "'}}")); ++ NOTE_BLOCK_MAP.put("false" + note, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:note_block',Properties:{powered:'false',note:'" + note + "'}}")); ++ } ++ } ++ ++ static final Int2ObjectOpenHashMap DYE_COLOR_MAP = new Int2ObjectOpenHashMap<>(); ++ static { ++ DYE_COLOR_MAP.put(0, "white"); ++ DYE_COLOR_MAP.put(1, "orange"); ++ DYE_COLOR_MAP.put(2, "magenta"); ++ DYE_COLOR_MAP.put(3, "light_blue"); ++ DYE_COLOR_MAP.put(4, "yellow"); ++ DYE_COLOR_MAP.put(5, "lime"); ++ DYE_COLOR_MAP.put(6, "pink"); ++ DYE_COLOR_MAP.put(7, "gray"); ++ DYE_COLOR_MAP.put(8, "light_gray"); ++ DYE_COLOR_MAP.put(9, "cyan"); ++ DYE_COLOR_MAP.put(10, "purple"); ++ DYE_COLOR_MAP.put(11, "blue"); ++ DYE_COLOR_MAP.put(12, "brown"); ++ DYE_COLOR_MAP.put(13, "green"); ++ DYE_COLOR_MAP.put(14, "red"); ++ DYE_COLOR_MAP.put(15, "black"); ++ } ++ ++ static final Map> BED_BLOCK_MAP = new HashMap<>(); ++ ++ static { ++ for (final Int2ObjectMap.Entry entry : DYE_COLOR_MAP.int2ObjectEntrySet()) { ++ if (!Objects.equals(entry.getValue(), "red")) { ++ addBeds(BED_BLOCK_MAP, entry.getIntKey(), entry.getValue()); ++ } ++ } ++ } ++ ++ private static void addBeds(final Map> into, final int colourId, final String colourName) { ++ into.put("southfalsefoot" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'south',occupied:'false',part:'foot'}}")); ++ into.put("westfalsefoot" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'west',occupied:'false',part:'foot'}}")); ++ into.put("northfalsefoot" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'north',occupied:'false',part:'foot'}}")); ++ into.put("eastfalsefoot" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'east',occupied:'false',part:'foot'}}")); ++ into.put("southfalsehead" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'south',occupied:'false',part:'head'}}")); ++ into.put("westfalsehead" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'west',occupied:'false',part:'head'}}")); ++ into.put("northfalsehead" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'north',occupied:'false',part:'head'}}")); ++ into.put("eastfalsehead" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'east',occupied:'false',part:'head'}}")); ++ into.put("southtruehead" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'south',occupied:'true',part:'head'}}")); ++ into.put("westtruehead" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'west',occupied:'true',part:'head'}}")); ++ into.put("northtruehead" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'north',occupied:'true',part:'head'}}")); ++ into.put("easttruehead" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'east',occupied:'true',part:'head'}}")); ++ } ++ ++ static final Map> BANNER_BLOCK_MAP = new HashMap<>(); ++ ++ static { ++ for (final Int2ObjectMap.Entry entry : DYE_COLOR_MAP.int2ObjectEntrySet()) { ++ if (!Objects.equals(entry.getValue(), "white")) { ++ addBanners(BANNER_BLOCK_MAP, 15 - entry.getIntKey(), entry.getValue()); ++ } ++ } ++ } ++ ++ private static void addBanners(final Map> into, final int colourId, final String colourName) { ++ for(int rotation = 0; rotation < 16; ++rotation) { ++ into.put("" + rotation + "_" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_banner',Properties:{rotation:'" + rotation + "'}}")); ++ } ++ ++ into.put("north_" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_wall_banner',Properties:{facing:'north'}}")); ++ into.put("south_" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_wall_banner',Properties:{facing:'south'}}")); ++ into.put("west_" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_wall_banner',Properties:{facing:'west'}}")); ++ into.put("east_" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_wall_banner',Properties:{facing:'east'}}")); ++ } ++ ++ static final MapType AIR = Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(0)); ++ ++ public ConverterFlattenChunk() { ++ super(MCVersions.V17W47A, 1); ++ } ++ ++ static String getName(final MapType blockState) { ++ return blockState.getString("Name"); ++ } ++ ++ static String getProperty(final MapType blockState, final String propertyName) { ++ final MapType properties = blockState.getMap("Properties"); ++ if (properties == null) { ++ return ""; ++ } ++ ++ return properties.getString(propertyName, ""); ++ } ++ ++ static int getSideMask(final boolean noLeft, final boolean noRight, final boolean noBack, final boolean noForward) { ++ if (noBack) { ++ if (noRight) { ++ return 2; ++ } else if (noLeft) { ++ return 128; ++ } else { ++ return 1; ++ } ++ } else if (noForward) { ++ if (noLeft) { ++ return 32; ++ } else if (noRight) { ++ return 8; ++ } else { ++ return 16; ++ } ++ } else if (noRight) { ++ return 4; ++ } else if (noLeft) { ++ return 64; ++ } else { ++ return 0; ++ } ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType level = data.getMap("Level"); ++ if (level == null) { ++ return null; ++ } ++ ++ if (!level.hasKey("Sections", ObjectType.LIST)) { ++ return null; ++ } ++ ++ data.setMap("Level", new UpgradeChunk(level).writeBackToLevel()); ++ ++ return null; ++ } ++ ++ static enum Direction { ++ DOWN(AxisDirection.NEGATIVE, Axis.Y), ++ UP(AxisDirection.POSITIVE, Axis.Y), ++ NORTH(AxisDirection.NEGATIVE, Axis.Z), ++ SOUTH(AxisDirection.POSITIVE, Axis.Z), ++ WEST(AxisDirection.NEGATIVE, Axis.X), ++ EAST(AxisDirection.POSITIVE, Axis.X); ++ ++ private final Axis axis; ++ private final AxisDirection axisDirection; ++ ++ private Direction(final AxisDirection axisDirection, final Axis axis) { ++ this.axis = axis; ++ this.axisDirection = axisDirection; ++ } ++ ++ public AxisDirection getAxisDirection() { ++ return this.axisDirection; ++ } ++ ++ public Axis getAxis() { ++ return this.axis; ++ } ++ ++ public static enum AxisDirection { ++ POSITIVE(1), ++ NEGATIVE(-1); ++ ++ private final int step; ++ ++ private AxisDirection(final int step) { ++ this.step = step; ++ } ++ ++ public int getStep() { ++ return this.step; ++ } ++ } ++ ++ public static enum Axis { ++ X, Y, Z; ++ } ++ } ++ ++ static class DataLayer { ++ private final byte[] data; ++ ++ public DataLayer() { ++ this.data = new byte[2048]; ++ } ++ ++ public DataLayer(final byte[] data) { ++ this.data = data; ++ if (data.length != 2048) { ++ throw new IllegalArgumentException("ChunkNibbleArrays should be 2048 bytes not: " + data.length); ++ } ++ } ++ ++ public static DataLayer getOrNull(final byte[] data) { ++ return data == null ? null : new DataLayer(data); ++ } ++ ++ public static DataLayer getOrCreate(final byte[] data) { ++ return data == null ? new DataLayer() : new DataLayer(data); ++ } ++ ++ public int get(final int index) { ++ final byte value = this.data[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); ++ } ++ ++ public int get(final int x, final int y, final int z) { ++ final int index = y << 8 | z << 4 | x; ++ final byte value = this.data[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); ++ } ++ } ++ ++ static final class UpgradeChunk { ++ int sides; ++ ++ final Section[] sections = new Section[16]; ++ final MapType level; ++ final int blockX; ++ final int blockZ; ++ final Int2ObjectLinkedOpenHashMap> tileEntities = new Int2ObjectLinkedOpenHashMap<>(16); ++ ++ public UpgradeChunk(final MapType level) { ++ this.level = level; ++ this.blockX = level.getInt("xPos") << 4; ++ this.blockZ = level.getInt("zPos") << 4; ++ ++ final ListType tileEntities = level.getList("TileEntities", ObjectType.MAP); ++ if (tileEntities != null) { ++ for (int i = 0, len = tileEntities.size(); i < len; ++i) { ++ final MapType tileEntity = tileEntities.getMap(i); ++ ++ final int x = (tileEntity.getInt("x") - this.blockX) & 15; ++ final int y = tileEntity.getInt("y"); ++ final int z = (tileEntity.getInt("z") - this.blockZ) & 15; ++ final int index = (y << 8) | (z << 4) | x; ++ if (this.tileEntities.put(index, tileEntity) != null) { ++ LOGGER.warn("In chunk: {}x{} found a duplicate block entity at position (ConverterFlattenChunk): [{}, {}, {}]", this.blockX, this.blockZ, x, y, z); ++ } ++ } ++ } ++ ++ final boolean convertedFromAlphaFormat = level.getBoolean("convertedFromAlphaFormat"); ++ final ListType sections = level.getList("Sections", ObjectType.MAP); ++ if (sections != null) { ++ for (int i = 0, len = sections.size(); i < len; ++i) { ++ final MapType sectionData = sections.getMap(i); ++ final Section section = new Section(sectionData); ++ ++ if (section.y < 0 || section.y > 15) { ++ LOGGER.warn("In chunk: {}x{} found an invalid chunk section y (ConverterFlattenChunk): {}", this.blockX, this.blockZ, section.y); ++ continue; ++ } ++ ++ if (this.sections[section.y] != null) { ++ LOGGER.warn("In chunk: {}x{} found a duplicate chunk section (ConverterFlattenChunk): {}", this.blockX, this.blockZ, section.y); ++ } ++ ++ this.sides = section.upgrade(this.sides); ++ this.sections[section.y] = section; ++ } ++ } ++ ++ for (final Section section : this.sections) { ++ if (section == null) { ++ continue; ++ } ++ ++ final int yIndex = section.y << (8 + 4); ++ ++ for (final Iterator> iterator = section.toFix.int2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final Int2ObjectMap.Entry fixEntry = iterator.next(); ++ final IntIterator positionIterator = fixEntry.getValue().iterator(); ++ switch (fixEntry.getIntKey()) { ++ case 2: { // grass block ++ while (positionIterator.hasNext()) { ++ final int position = positionIterator.nextInt() | yIndex; ++ final MapType blockState = this.getBlock(position); ++ if (!"minecraft:grass_block".equals(getName(blockState))) { ++ continue; ++ } ++ ++ final String blockAbove = getName(getBlock(relative(position, Direction.UP))); ++ if ("minecraft:snow".equals(blockAbove) || "minecraft:snow_layer".equals(blockAbove)) { ++ this.setBlock(position, SNOWY_GRASS); ++ } ++ } ++ break; ++ } ++ case 3: { // dirt ++ while (positionIterator.hasNext()) { ++ final int position = positionIterator.nextInt() | yIndex; ++ final MapType blockState = this.getBlock(position); ++ if (!"minecraft:podzol".equals(getName(blockState))) { ++ continue; ++ } ++ ++ final String blockAbove = getName(getBlock(relative(position, Direction.UP))); ++ if ("minecraft:snow".equals(blockAbove) || "minecraft:snow_layer".equals(blockAbove)) { ++ this.setBlock(position, SNOWY_PODZOL); ++ } ++ } ++ break; ++ } ++ case 25: { // note block ++ while (positionIterator.hasNext()) { ++ final int position = positionIterator.nextInt() | yIndex; ++ final MapType tile = this.removeBlockEntity(position); ++ if (tile != null) { ++ final String state = Boolean.toString(tile.getBoolean("powered")) + (byte) Math.min(Math.max(tile.getInt("note"), 0), 24); ++ this.setBlock(position, NOTE_BLOCK_MAP.getOrDefault(state, NOTE_BLOCK_MAP.get("false0"))); ++ } ++ } ++ break; ++ } ++ case 26: { // bed ++ while (positionIterator.hasNext()) { ++ final int position = positionIterator.nextInt() | yIndex; ++ final MapType tile = this.getBlockEntity(position); ++ ++ if (tile == null) { ++ continue; ++ } ++ ++ final MapType blockState = this.getBlock(position); ++ ++ final int colour = tile.getInt("color"); ++ if (colour != 14 && colour >= 0 && colour < 16) { ++ final String state = getProperty(blockState, "facing") + getProperty(blockState, "occupied") + getProperty(blockState, "part") + colour; ++ ++ final MapType update = BED_BLOCK_MAP.get(state); ++ if (update != null) { ++ this.setBlock(position, update); ++ } ++ } ++ } ++ break; ++ } ++ case 64: // oak door ++ case 71: // iron door ++ case 193: // spruce door ++ case 194: // birch door ++ case 195: // jungle door ++ case 196: // acacia door ++ case 197: { // dark oak door ++ // aka the door updater ++ while (positionIterator.hasNext()) { ++ final int position = positionIterator.nextInt() | yIndex; ++ final MapType blockState = this.getBlock(position); ++ if (!getName(blockState).endsWith("_door")) { ++ continue; ++ } ++ ++ if (!"lower".equals(getProperty(blockState, "half"))) { ++ continue; ++ } ++ ++ final int positionAbove = relative(position, Direction.UP); ++ final MapType blockStateAbove = this.getBlock(positionAbove); ++ ++ final String name = getName(blockState); ++ if (name.equals(getName(blockStateAbove))) { ++ final String facingBelow = getProperty(blockState, "facing"); ++ final String openBelow = getProperty(blockState, "open"); ++ final String hingeAbove = convertedFromAlphaFormat ? "left" : getProperty(blockStateAbove, "hinge"); ++ final String poweredAbove = convertedFromAlphaFormat ? "false" : getProperty(blockStateAbove, "powered"); ++ ++ this.setBlock(position, DOOR_MAP.get(name + facingBelow + "lower" + hingeAbove + openBelow + poweredAbove)); ++ this.setBlock(positionAbove, DOOR_MAP.get(name + facingBelow + "upper" + hingeAbove + openBelow + poweredAbove)); ++ } ++ } ++ break; ++ } ++ case 86: { // pumpkin ++ while (positionIterator.hasNext()) { ++ final int position = positionIterator.nextInt() | yIndex; ++ final MapType blockState = this.getBlock(position); ++ ++ // I guess this is some terrible hack to convert carved pumpkins from world gen into ++ // regular pumpkins? ++ ++ if ("minecraft:carved_pumpkin".equals(getName(blockState))) { ++ final String downName = getName(this.getBlock(relative(position, Direction.DOWN))); ++ if ("minecraft:grass_block".equals(downName) || "minecraft:dirt".equals(downName)) { ++ this.setBlock(position, PUMPKIN); ++ } ++ } ++ } ++ break; ++ } ++ case 110: { // mycelium ++ while (positionIterator.hasNext()) { ++ final int position = positionIterator.nextInt() | yIndex; ++ final MapType blockState = this.getBlock(position); ++ if ("minecraft:mycelium".equals(getName(blockState))) { ++ final String nameAbove = getName(this.getBlock(relative(position, Direction.UP))); ++ if ("minecraft:snow".equals(nameAbove) || "minecraft:snow_layer".equals(nameAbove)) { ++ this.setBlock(position, SNOWY_MYCELIUM); ++ } ++ } ++ } ++ break; ++ } ++ case 140: { // flower pot ++ while (positionIterator.hasNext()) { ++ final int position = positionIterator.nextInt() | yIndex; ++ final MapType tile = this.removeBlockEntity(position); ++ if (tile == null) { ++ continue; ++ } ++ ++ final String item; ++ if (tile.hasKey("Item", ObjectType.NUMBER)) { ++ // the item name converter should have migrated to number, however no legacy converter ++ // ever did this. so we can get data with versions above v102 (old worlds, converted prior to DFU) ++ // that didn't convert. so just do it here. ++ item = HelperItemNameV102.getNameFromId(tile.getInt("Item")); ++ } else { ++ item = tile.getString("Item", ""); ++ } ++ ++ final String state = item + tile.getInt("Data"); ++ this.setBlock(position, FLOWER_POT_MAP.getOrDefault(state, FLOWER_POT_MAP.get("minecraft:air0"))); ++ } ++ break; ++ } ++ case 144: { // mob head ++ while (positionIterator.hasNext()) { ++ final int position = positionIterator.nextInt() | yIndex; ++ final MapType tile = this.getBlockEntity(position); ++ if (tile == null) { ++ continue; ++ } ++ ++ final String typeString = Integer.toString(tile.getInt("SkullType")); ++ final String facing = getProperty(this.getBlock(position), "facing"); ++ final String state; ++ if (!"up".equals(facing) && !"down".equals(facing)) { ++ state = typeString + facing; ++ } else { ++ state = typeString + tile.getInt("Rot"); ++ } ++ ++ tile.remove("SkullType"); ++ tile.remove("facing"); ++ tile.remove("Rot"); ++ ++ this.setBlock(position, SKULL_MAP.getOrDefault(state, SKULL_MAP.get("0north"))); ++ } ++ break; ++ } ++ case 175: { // sunflower ++ while (positionIterator.hasNext()) { ++ final int position = positionIterator.nextInt() | yIndex; ++ final MapType blockState = this.getBlock(position); ++ if (!"upper".equals(getProperty(blockState, "half"))) { ++ continue; ++ } ++ ++ final MapType blockStateBelow = this.getBlock(relative(position, Direction.DOWN)); ++ final String nameBelow = getName(blockStateBelow); ++ switch (nameBelow) { ++ case "minecraft:sunflower": ++ this.setBlock(position, UPPER_SUNFLOWER); ++ break; ++ case "minecraft:lilac": ++ this.setBlock(position, UPPER_LILAC); ++ break; ++ case "minecraft:tall_grass": ++ this.setBlock(position, UPPER_TALL_GRASS); ++ break; ++ case "minecraft:large_fern": ++ this.setBlock(position, UPPER_LARGE_FERN); ++ break; ++ case "minecraft:rose_bush": ++ this.setBlock(position, UPPER_ROSE_BUSH); ++ break; ++ case "minecraft:peony": ++ this.setBlock(position, UPPER_PEONY); ++ break; ++ } ++ } ++ break; ++ } ++ case 176: // free standing banner ++ case 177: { // wall mounted banner ++ while (positionIterator.hasNext()) { ++ final int position = positionIterator.nextInt() | yIndex; ++ final MapType tile = this.getBlockEntity(position); ++ ++ if (tile == null) { ++ continue; ++ } ++ ++ final MapType blockState = this.getBlock(position); ++ ++ final int base = tile.getInt("Base"); ++ if (base != 15 && base >= 0 && base < 16) { ++ final String state = getProperty(blockState, fixEntry.getIntKey() == 176 ? "rotation" : "facing") + "_" + base; ++ final MapType update = BANNER_BLOCK_MAP.get(state); ++ if (update != null) { ++ this.setBlock(position, update); ++ } ++ } ++ } ++ break; ++ } ++ } ++ } ++ } ++ } ++ ++ private MapType getBlockEntity(final int index) { ++ return this.tileEntities.get(index); ++ } ++ ++ private MapType removeBlockEntity(final int index) { ++ return this.tileEntities.remove(index); ++ } ++ ++ public static int relative(final int index, final Direction direction) { ++ switch (direction.getAxis()) { ++ case X: ++ int j = (index & 15) + direction.getAxisDirection().getStep(); ++ return j >= 0 && j <= 15 ? index & -16 | j : -1; ++ case Y: ++ int k = (index >> 8) + direction.getAxisDirection().getStep(); ++ return k >= 0 && k <= 255 ? index & 255 | k << 8 : -1; ++ case Z: ++ int l = (index >> 4 & 15) + direction.getAxisDirection().getStep(); ++ return l >= 0 && l <= 15 ? index & -241 | l << 4 : -1; ++ default: ++ return -1; ++ } ++ } ++ ++ private void setBlock(final int index, final MapType blockState) { ++ if (index >= 0 && index <= 65535) { ++ final Section section = this.getSection(index); ++ if (section != null) { ++ section.setBlock(index & 4095, blockState); ++ } ++ } ++ } ++ ++ private Section getSection(final int index) { ++ final int y = index >> 12; ++ return y < this.sections.length ? this.sections[y] : null; ++ } ++ ++ public MapType getBlock(int i) { ++ if (i >= 0 && i <= 65535) { ++ final Section section = this.getSection(i); ++ return section == null ? AIR : section.getBlock(i & 4095); ++ } else { ++ return AIR; ++ } ++ } ++ ++ public MapType writeBackToLevel() { ++ if (this.tileEntities.isEmpty()) { ++ this.level.remove("TileEntities"); ++ } else { ++ final ListType tileEntities = Types.NBT.createEmptyList(); ++ this.tileEntities.values().forEach(tileEntities::addMap); ++ this.level.setList("TileEntities", tileEntities); ++ } ++ ++ final MapType indices = Types.NBT.createEmptyMap(); ++ final ListType sections = Types.NBT.createEmptyList(); ++ for (final Section section : this.sections) { ++ if (section == null) { ++ continue; ++ } ++ ++ sections.addMap(section.writeBackToSection()); ++ indices.setInts(Integer.toString(section.y), Arrays.copyOf(section.update.elements(), section.update.size())); ++ } ++ ++ this.level.setList("Sections", sections); ++ ++ final MapType upgradeData = Types.NBT.createEmptyMap(); ++ upgradeData.setByte("Sides", (byte)this.sides); ++ upgradeData.setMap("Indices", indices); ++ ++ this.level.setMap("UpgradeData", upgradeData); ++ ++ return this.level; ++ } ++ } ++ ++ static class Section { ++ final Palette palette = new Palette(); ++ ++ static final class Palette extends Reference2IntOpenHashMap> { ++ ++ final ListType paletteStates = Types.NBT.createEmptyList(); ++ ++ private int find(final MapType k) { ++ if (((k) == (null))) ++ return containsNullKey ? n : -(n + 1); ++ MapType curr; ++ final Object[] key = this.key; ++ int pos; ++ // The starting point. ++ if (((curr = (MapType)key[pos = (it.unimi.dsi.fastutil.HashCommon.mix(System.identityHashCode(k))) & mask]) == (null))) ++ return -(pos + 1); ++ if (((k) == (curr))) ++ return pos; ++ // There's always an unused entry. ++ while (true) { ++ if (((curr = (MapType)key[pos = (pos + 1) & mask]) == (null))) ++ return -(pos + 1); ++ if (((k) == (curr))) ++ return pos; ++ } ++ } ++ ++ private void insert(final int pos, final MapType k, final int v) { ++ if (pos == n) ++ containsNullKey = true; ++ ((Object[])key)[pos] = k; ++ value[pos] = v; ++ if (size++ >= maxFill) ++ rehash(arraySize(size + 1, f)); ++ } ++ ++ private MapType[] byId = new MapType[4]; ++ private MapType last = null; ++ ++ public int getOrCreateId(final MapType k) { ++ if (k == this.last) { ++ return this.size - 1; ++ } ++ final int pos = find(k); ++ if (pos >= 0) { ++ return this.value[pos]; ++ } ++ ++ final int insert = this.size; ++ MapType inPalette = k; ++ ++ if ("%%FILTER_ME%%".equals(getName(k))) { ++ inPalette = AIR; ++ } ++ ++ if (insert >= this.byId.length) { ++ this.byId = Arrays.copyOf(this.byId, this.byId.length * 2); ++ this.byId[insert] = k; ++ } else { ++ this.byId[insert] = k; ++ } ++ this.paletteStates.addMap(inPalette); ++ ++ this.last = k; ++ ++ this.insert(-pos - 1, k, insert); ++ ++ return insert; ++ } ++ ++ } ++ ++ final MapType section; ++ final boolean hasData; ++ final Int2ObjectLinkedOpenHashMap toFix = new Int2ObjectLinkedOpenHashMap<>(); ++ final IntArrayList update = new IntArrayList(); ++ final int y; ++ final int[] buffer = new int[4096]; ++ ++ public Section(final MapType section) { ++ this.section = section; ++ this.y = section.getInt("Y"); ++ this.hasData = section.hasKey("Blocks", ObjectType.BYTE_ARRAY); ++ } ++ ++ public MapType getBlock(final int index) { ++ if (index >= 0 && index <= 4095) { ++ final MapType state = this.palette.byId[this.buffer[index]]; ++ return state == null ? AIR : state; ++ } else { ++ return AIR; ++ } ++ } ++ ++ public void setBlock(final int index, final MapType blockState) { ++ this.buffer[index] = this.palette.getOrCreateId(blockState); ++ } ++ ++ public int upgrade(int sides) { ++ if (!this.hasData) { ++ return sides; ++ } ++ ++ final byte[] blocks = this.section.getBytes("Blocks"); ++ final DataLayer data = DataLayer.getOrNull(this.section.getBytes("Data")); ++ final DataLayer add = DataLayer.getOrNull(this.section.getBytes("Add")); ++ ++ this.palette.getOrCreateId(AIR); ++ ++ for (int index = 0; index < 4096; ++index) { ++ final int x = index & 15; ++ final int z = index >> 4 & 15; ++ ++ int blockStateId = (blocks[index] & 255) << 4; ++ if (data != null) { ++ blockStateId |= data.get(index); ++ } ++ if (add != null) { ++ blockStateId |= add.get(index) << 12; ++ } ++ if (IDS_NEEDING_FIX[blockStateId >>> 4]) { ++ this.addFix(blockStateId >>> 4, index); ++ } ++ ++ if (VIRTUAL[blockStateId >>> 4]) { ++ final int additionalSides = getSideMask(x == 0, x == 15, z == 0, z == 15); ++ if (additionalSides == 0) { ++ this.update.add(index); ++ } else { ++ sides |= additionalSides; ++ } ++ } ++ ++ this.setBlock(index, HelperBlockFlatteningV1450.getNBTForId(blockStateId)); ++ } ++ ++ return sides; ++ } ++ ++ private void addFix(final int block, final int index) { ++ this.toFix.computeIfAbsent(block, (final int keyInMap) -> { ++ return new IntArrayList(); ++ }).add(index); ++ } ++ ++ // Note: modifies the current section and returns it. ++ public MapType writeBackToSection() { ++ if (!this.hasData) { ++ return this.section; ++ } ++ ++ this.section.setList("Palette", this.palette.paletteStates.copy()); // deep copy to ensure palette compound tags are NOT shared ++ ++ final int bitSize = Math.max(4, DataFixUtils.ceillog2(this.palette.size())); ++ final PackedBitStorage packedIds = new PackedBitStorage(bitSize, 4096); ++ ++ for(int index = 0; index < this.buffer.length; ++index) { ++ packedIds.set(index, this.buffer[index]); ++ } ++ ++ this.section.setLongs("BlockStates", packedIds.getRaw()); ++ ++ this.section.remove("Blocks"); ++ this.section.remove("Data"); ++ this.section.remove("Add"); ++ ++ return this.section; ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterRenameStatus.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterRenameStatus.java +new file mode 100644 +index 0000000000000000000000000000000000000000..084c67a46bc5ec7f5a4bef3216805a87b32c83d0 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterRenameStatus.java +@@ -0,0 +1,32 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.chunk; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.util.NamespaceUtil; ++import java.util.function.Function; ++ ++public final class ConverterRenameStatus extends DataConverter, MapType> { ++ ++ private final Function renamer; ++ ++ public ConverterRenameStatus(final int toVersion, final Function renamer) { ++ this(toVersion, 0, renamer); ++ } ++ ++ public ConverterRenameStatus(final int toVersion, final int versionStep, final Function renamer) { ++ super(toVersion, versionStep); ++ this.renamer = renamer; ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ // Note: DFU technically enforces namespace due to how they wrote their converter, so we will do the same. ++ NamespaceUtil.enforceForPath(data, "Status"); ++ RenameHelper.renameString(data, "Status", this.renamer); ++ ++ NamespaceUtil.enforceForPath(data.getMap("below_zero_retrogen"), "target_status"); ++ RenameHelper.renameString(data.getMap("below_zero_retrogen"), "target_status", this.renamer); ++ return null; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/custom/V3818_Commands.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/custom/V3818_Commands.java +new file mode 100644 +index 0000000000000000000000000000000000000000..17e75ef0fe1f7e20d4100b40c445c9fccbf23833 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/custom/V3818_Commands.java +@@ -0,0 +1,288 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.custom; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCDataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.util.CommandArgumentUpgrader; ++import com.google.common.base.Suppliers; ++import com.google.gson.JsonArray; ++import com.google.gson.JsonElement; ++import com.google.gson.JsonObject; ++import com.google.gson.JsonParseException; ++import com.google.gson.JsonParser; ++import com.google.gson.JsonPrimitive; ++import com.mojang.brigadier.exceptions.CommandSyntaxException; ++import com.mojang.serialization.Dynamic; ++import com.mojang.serialization.JsonOps; ++import net.minecraft.SharedConstants; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.NbtOps; ++import net.minecraft.nbt.Tag; ++import net.minecraft.nbt.TagParser; ++import net.minecraft.util.GsonHelper; ++import java.util.Iterator; ++import java.util.function.Supplier; ++ ++public final class V3818_Commands { ++ ++ private static final int VERSION = MCVersions.V24W07A + 1; ++ ++ private static final boolean DISABLE_COMMAND_CONVERTER = Boolean.getBoolean("Paper.DisableCommandConverter"); ++ ++ public static String toCommandFormat(final CompoundTag components) { ++ final StringBuilder ret = new StringBuilder(); ++ ret.append('['); ++ for (final Iterator iterator = components.getAllKeys().iterator(); iterator.hasNext();) { ++ final String key = iterator.next(); ++ ret.append(key); ++ ret.append('='); ++ ret.append(components.get(key).toString()); ++ if (iterator.hasNext()) { ++ ret.append(','); ++ } ++ } ++ ret.append(']'); ++ ++ return ret.toString(); ++ } ++ ++ public static JsonElement convertToJson(final Tag tag) { ++ // We don't have conversion utilities, but DFU does... ++ ++ return new Dynamic<>(NbtOps.INSTANCE, tag).convert(JsonOps.INSTANCE).getValue(); ++ } ++ ++ public static void walkComponent(final JsonElement primitive) { ++ if (!(primitive instanceof JsonObject root)) { ++ if (primitive instanceof JsonArray array) { ++ for (final JsonElement component : array) { ++ walkComponent(component); ++ } ++ } ++ return; ++ } ++ ++ final JsonElement clickEventElement = root.get("clickEvent"); ++ if (clickEventElement instanceof JsonObject clickEvent) { ++ final JsonElement actionElement = clickEvent.get("action"); ++ final JsonElement cmdElement = clickEvent.get("value"); ++ if (actionElement instanceof JsonPrimitive action && cmdElement instanceof JsonPrimitive cmd) { ++ final String actionString = action.getAsString(); ++ final String cmdString = cmd.getAsString(); ++ ++ if ((actionString.equals("suggest_command") && cmdString.startsWith("/")) || actionString.equals("run_command")) { ++ final Object res = MCTypeRegistry.DATACONVERTER_CUSTOM_TYPE_COMMAND.convert( ++ cmdString, MCVersions.V1_20_4, SharedConstants.getCurrentVersion().getDataVersion().getVersion() ++ ); ++ if (res instanceof String newCmd) { ++ clickEvent.addProperty("value", newCmd); ++ } ++ } ++ } ++ } ++ ++ final JsonElement hoverEventElement = root.get("hoverEvent"); ++ if (hoverEventElement instanceof JsonObject hoverEvent) { ++ final JsonElement showText = hoverEvent.get("action"); ++ if (showText instanceof JsonPrimitive showTextPrimitive && showTextPrimitive.getAsString().equals("show_item")) { ++ final JsonElement contentsElement = hoverEvent.get("contents"); ++ if (contentsElement instanceof JsonObject contents) { ++ final JsonElement idElement = contents.get("id"); ++ final JsonElement tagElement = contents.get("tag"); ++ ++ if (idElement instanceof JsonPrimitive idPrimitive) { ++ final CompoundTag itemNBT = new CompoundTag(); ++ itemNBT.putString("id", idPrimitive.getAsString()); ++ itemNBT.putInt("Count", 1); ++ ++ if (tagElement instanceof JsonPrimitive tagPrimitive) { ++ try { ++ final CompoundTag tag = TagParser.parseTag(tagPrimitive.getAsString()); ++ itemNBT.put("tag", tag); ++ } catch (final CommandSyntaxException ignore) {} ++ } ++ ++ final CompoundTag converted = MCDataConverter.convertTag( ++ MCTypeRegistry.ITEM_STACK, itemNBT, MCVersions.V1_20_4, ++ SharedConstants.getCurrentVersion().getDataVersion().getVersion() ++ ); ++ ++ contents.remove("tag"); ++ ++ contents.addProperty("id", converted.getString("id")); ++ ++ if (converted.contains("components", Tag.TAG_COMPOUND)) { ++ contents.add("components", convertToJson(converted.getCompound("components"))); ++ } ++ } ++ } ++ final JsonElement valueElement = hoverEvent.get("value"); ++ if (valueElement instanceof JsonPrimitive valuePrimitive) { ++ try { ++ final CompoundTag itemNBT = TagParser.parseTag(valuePrimitive.getAsString()); ++ if (itemNBT.contains("id", Tag.TAG_STRING)) { ++ final boolean explicitCount = itemNBT.contains("Count", Tag.TAG_ANY_NUMERIC); ++ if (!explicitCount) { ++ itemNBT.putInt("Count", 1); ++ } ++ final CompoundTag converted = MCDataConverter.convertTag( ++ MCTypeRegistry.ITEM_STACK, itemNBT, MCVersions.V1_20_4, ++ SharedConstants.getCurrentVersion().getDataVersion().getVersion() ++ ); ++ ++ hoverEvent.remove("value"); ++ ++ final JsonObject contents = new JsonObject(); ++ hoverEvent.add("contents", contents); ++ ++ contents.addProperty("id", converted.getString("id")); ++ if (explicitCount) { ++ contents.addProperty("count", converted.getInt("count")); ++ } ++ ++ if (converted.contains("components", Tag.TAG_COMPOUND)) { ++ contents.add("components", convertToJson(converted.getCompound("components"))); ++ } ++ } ++ } catch (final CommandSyntaxException ignore) {} ++ } ++ } ++ } ++ ++ final JsonElement extra = root.get("extra"); ++ if (extra instanceof JsonArray array) { ++ for (final JsonElement component : array) { ++ walkComponent(component); ++ } ++ } ++ } ++ ++ private static String walkComponent(final String json) { ++ if (json == null || json.isEmpty()) { ++ return json; ++ } ++ ++ try { ++ final JsonElement element = JsonParser.parseString(json); ++ walkComponent(element); ++ return GsonHelper.toStableString(element); ++ } catch (final JsonParseException ex) { ++ return json; ++ } ++ } ++ ++ // this is AFTER all the converters for subversion 5, so these run AFTER them ++ public static void register_5() { ++ if (DISABLE_COMMAND_CONVERTER) { ++ return; ++ } ++ // Command is already registered in walker for command blocks ++ MCTypeRegistry.DATACONVERTER_CUSTOM_TYPE_COMMAND.addConverter(new DataConverter<>(VERSION, 5) { ++ private static final Supplier COMMAND_UPGRADER = Suppliers.memoize(() -> ++ CommandArgumentUpgrader.upgrader_1_20_4_to_1_20_5(999)); ++ ++ @Override ++ public Object convert(final Object data, final long sourceVersion, final long toVersion) { ++ if (!(data instanceof String cmd)) { ++ return null; ++ } ++ // We use startsWith("/") because we aren't supporting WorldEdit style commands, ++ // and passing the context of whether the use supports leading slash would be high effort low return ++ return COMMAND_UPGRADER.get().upgradeCommandArguments(cmd, cmd.startsWith("/")); ++ } ++ }); ++ ++ // command is not registered in any walkers for books/signs, and we don't want to do that as we would parse ++ // the json every walk. instead, we create a one time converter to avoid the additional cost of parsing the json ++ // for future updates ++ ++ // books ++ // note: at this stage, item is converted to components, so we can use the data components type ++ MCTypeRegistry.DATA_COMPONENTS.addStructureConverter(new DataConverter<>(VERSION, 5) { ++ private static void walkPath(final MapType data, final String path) { ++ final String str = data.getString(path); ++ if (str == null) { ++ return; ++ } ++ ++ final String newStr = walkComponent(str); ++ if (newStr != null) { ++ data.setString(path, newStr); ++ } ++ } ++ ++ private static void walkBookContent(final MapType data, final String path) { ++ if (data == null) { ++ return; ++ } ++ ++ final MapType content = data.getMap(path); ++ if (content == null) { ++ return; ++ } ++ ++ final ListType pages = content.getList("pages", ObjectType.MAP); ++ if (pages == null) { ++ return; ++ } ++ ++ for (int i = 0, len = pages.size(); i < len; ++i) { ++ final MapType text = pages.getMap(i); ++ ++ walkPath(text, "raw"); ++ walkPath(text, "filtered"); ++ } ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ walkBookContent(data, "minecraft:written_book_content"); ++ return null; ++ } ++ }); ++ ++ // signs ++ ++ final DataConverter, MapType> signTileConverter = new DataConverter<>(VERSION, 5) { ++ private static void walkText(final MapType data, final String path) { ++ if (data == null) { ++ return; ++ } ++ ++ final MapType text = data.getMap(path); ++ if (text == null) { ++ return; ++ } ++ ++ final ListType messages = text.getList("messages", ObjectType.STRING); ++ if (messages != null) { ++ for (int i = 0, len = Math.min(4, messages.size()); i < len; ++i) { ++ messages.setString(i, walkComponent(messages.getString(i))); ++ } ++ } ++ ++ final ListType filteredMessages = text.getList("filtered_messages", ObjectType.STRING); ++ ++ if (filteredMessages != null) { ++ for (int i = 0, len = Math.min(4, filteredMessages.size()); i < len; ++i) { ++ filteredMessages.setString(i, walkComponent(filteredMessages.getString(i))); ++ } ++ } ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ walkText(data, "front_text"); ++ walkText(data, "back_text"); ++ return null; ++ } ++ }; ++ ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:sign", signTileConverter); ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:hanging_sign", signTileConverter); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterAbstractEntityRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterAbstractEntityRename.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6684915d6c0c44328a9296dc3ceb530e69482083 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterAbstractEntityRename.java +@@ -0,0 +1,38 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.entity; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import java.util.function.Function; ++ ++public final class ConverterAbstractEntityRename { ++ ++ private ConverterAbstractEntityRename() {} ++ ++ public static void register(final int version, final Function renamer) { ++ register(version, 0, renamer); ++ } ++ ++ public static void register(final int version, final int subVersion, final Function renamer) { ++ MCTypeRegistry.ENTITY.addStructureConverter(new DataConverter<>(version, subVersion) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final String id = data.getString("id"); ++ if (id == null) { ++ return null; ++ } ++ ++ final String converted = renamer.apply(id); ++ ++ if (converted != null) { ++ data.setString("id", converted); ++ } ++ ++ return null; ++ } ++ }); ++ ConverterAbstractStringValueTypeRename.register(version, subVersion, MCTypeRegistry.ENTITY_NAME, renamer); ++ } ++ ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityToVariant.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityToVariant.java +new file mode 100644 +index 0000000000000000000000000000000000000000..985af815e3c23ad7c8b774eac46a7202d3020234 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityToVariant.java +@@ -0,0 +1,44 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.entity; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.types.MapType; ++import java.util.function.IntFunction; ++ ++public final class ConverterEntityToVariant extends DataConverter, MapType> { ++ ++ public final String path; ++ public final IntFunction conversion; ++ ++ public ConverterEntityToVariant(final int toVersion, final String path, final IntFunction conversion) { ++ super(toVersion); ++ this.path = path; ++ this.conversion = conversion; ++ } ++ ++ public ConverterEntityToVariant(final int toVersion, final int versionStep, final String path, final IntFunction conversion) { ++ super(toVersion, versionStep); ++ this.path = path; ++ this.conversion = conversion; ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final Number value = data.getNumber(this.path); ++ if (value == null) { ++ // nothing to do, DFU does the same ++ return null; ++ } ++ ++ final String converted = this.conversion.apply(value.intValue()); ++ ++ if (converted == null) { ++ throw new NullPointerException("Conversion " + this.conversion + " cannot return null value!"); ++ } ++ ++ // DFU doesn't appear to remove the old field, so why should we? ++ ++ data.setString("variant", converted); ++ ++ return null; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityVariantRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityVariantRename.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ed5dcf6f8160742c07e23e98c85409209350a7d4 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityVariantRename.java +@@ -0,0 +1,37 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.entity; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.types.MapType; ++import java.util.function.Function; ++ ++public final class ConverterEntityVariantRename extends DataConverter, MapType> { ++ ++ private final Function renamer; ++ ++ public ConverterEntityVariantRename(final int toVersion, final Function renamer) { ++ super(toVersion); ++ this.renamer = renamer; ++ } ++ ++ public ConverterEntityVariantRename(final int toVersion, final int versionStep, final Function renamer) { ++ super(toVersion, versionStep); ++ this.renamer = renamer; ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final String variant = data.getString("variant"); ++ ++ if (variant == null) { ++ return null; ++ } ++ ++ final String rename = this.renamer.apply(variant); ++ ++ if (rename != null) { ++ data.setString("variant", rename); ++ } ++ ++ return null; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterFlattenEntity.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterFlattenEntity.java +new file mode 100644 +index 0000000000000000000000000000000000000000..afad2d92f78d4727ff4440ad2778f018d5a2a609 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterFlattenEntity.java +@@ -0,0 +1,371 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.entity; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.HelperBlockFlatteningV1450; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++ ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class ConverterFlattenEntity extends DataConverter, MapType> { ++ ++ private static final Map BLOCK_NAME_TO_ID = new HashMap<>(); ++ static { ++ BLOCK_NAME_TO_ID.put("minecraft:air", 0); ++ BLOCK_NAME_TO_ID.put("minecraft:stone", 1); ++ BLOCK_NAME_TO_ID.put("minecraft:grass", 2); ++ BLOCK_NAME_TO_ID.put("minecraft:dirt", 3); ++ BLOCK_NAME_TO_ID.put("minecraft:cobblestone", 4); ++ BLOCK_NAME_TO_ID.put("minecraft:planks", 5); ++ BLOCK_NAME_TO_ID.put("minecraft:sapling", 6); ++ BLOCK_NAME_TO_ID.put("minecraft:bedrock", 7); ++ BLOCK_NAME_TO_ID.put("minecraft:flowing_water", 8); ++ BLOCK_NAME_TO_ID.put("minecraft:water", 9); ++ BLOCK_NAME_TO_ID.put("minecraft:flowing_lava", 10); ++ BLOCK_NAME_TO_ID.put("minecraft:lava", 11); ++ BLOCK_NAME_TO_ID.put("minecraft:sand", 12); ++ BLOCK_NAME_TO_ID.put("minecraft:gravel", 13); ++ BLOCK_NAME_TO_ID.put("minecraft:gold_ore", 14); ++ BLOCK_NAME_TO_ID.put("minecraft:iron_ore", 15); ++ BLOCK_NAME_TO_ID.put("minecraft:coal_ore", 16); ++ BLOCK_NAME_TO_ID.put("minecraft:log", 17); ++ BLOCK_NAME_TO_ID.put("minecraft:leaves", 18); ++ BLOCK_NAME_TO_ID.put("minecraft:sponge", 19); ++ BLOCK_NAME_TO_ID.put("minecraft:glass", 20); ++ BLOCK_NAME_TO_ID.put("minecraft:lapis_ore", 21); ++ BLOCK_NAME_TO_ID.put("minecraft:lapis_block", 22); ++ BLOCK_NAME_TO_ID.put("minecraft:dispenser", 23); ++ BLOCK_NAME_TO_ID.put("minecraft:sandstone", 24); ++ BLOCK_NAME_TO_ID.put("minecraft:noteblock", 25); ++ BLOCK_NAME_TO_ID.put("minecraft:bed", 26); ++ BLOCK_NAME_TO_ID.put("minecraft:golden_rail", 27); ++ BLOCK_NAME_TO_ID.put("minecraft:detector_rail", 28); ++ BLOCK_NAME_TO_ID.put("minecraft:sticky_piston", 29); ++ BLOCK_NAME_TO_ID.put("minecraft:web", 30); ++ BLOCK_NAME_TO_ID.put("minecraft:tallgrass", 31); ++ BLOCK_NAME_TO_ID.put("minecraft:deadbush", 32); ++ BLOCK_NAME_TO_ID.put("minecraft:piston", 33); ++ BLOCK_NAME_TO_ID.put("minecraft:piston_head", 34); ++ BLOCK_NAME_TO_ID.put("minecraft:wool", 35); ++ BLOCK_NAME_TO_ID.put("minecraft:piston_extension", 36); ++ BLOCK_NAME_TO_ID.put("minecraft:yellow_flower", 37); ++ BLOCK_NAME_TO_ID.put("minecraft:red_flower", 38); ++ BLOCK_NAME_TO_ID.put("minecraft:brown_mushroom", 39); ++ BLOCK_NAME_TO_ID.put("minecraft:red_mushroom", 40); ++ BLOCK_NAME_TO_ID.put("minecraft:gold_block", 41); ++ BLOCK_NAME_TO_ID.put("minecraft:iron_block", 42); ++ BLOCK_NAME_TO_ID.put("minecraft:double_stone_slab", 43); ++ BLOCK_NAME_TO_ID.put("minecraft:stone_slab", 44); ++ BLOCK_NAME_TO_ID.put("minecraft:brick_block", 45); ++ BLOCK_NAME_TO_ID.put("minecraft:tnt", 46); ++ BLOCK_NAME_TO_ID.put("minecraft:bookshelf", 47); ++ BLOCK_NAME_TO_ID.put("minecraft:mossy_cobblestone", 48); ++ BLOCK_NAME_TO_ID.put("minecraft:obsidian", 49); ++ BLOCK_NAME_TO_ID.put("minecraft:torch", 50); ++ BLOCK_NAME_TO_ID.put("minecraft:fire", 51); ++ BLOCK_NAME_TO_ID.put("minecraft:mob_spawner", 52); ++ BLOCK_NAME_TO_ID.put("minecraft:oak_stairs", 53); ++ BLOCK_NAME_TO_ID.put("minecraft:chest", 54); ++ BLOCK_NAME_TO_ID.put("minecraft:redstone_wire", 55); ++ BLOCK_NAME_TO_ID.put("minecraft:diamond_ore", 56); ++ BLOCK_NAME_TO_ID.put("minecraft:diamond_block", 57); ++ BLOCK_NAME_TO_ID.put("minecraft:crafting_table", 58); ++ BLOCK_NAME_TO_ID.put("minecraft:wheat", 59); ++ BLOCK_NAME_TO_ID.put("minecraft:farmland", 60); ++ BLOCK_NAME_TO_ID.put("minecraft:furnace", 61); ++ BLOCK_NAME_TO_ID.put("minecraft:lit_furnace", 62); ++ BLOCK_NAME_TO_ID.put("minecraft:standing_sign", 63); ++ BLOCK_NAME_TO_ID.put("minecraft:wooden_door", 64); ++ BLOCK_NAME_TO_ID.put("minecraft:ladder", 65); ++ BLOCK_NAME_TO_ID.put("minecraft:rail", 66); ++ BLOCK_NAME_TO_ID.put("minecraft:stone_stairs", 67); ++ BLOCK_NAME_TO_ID.put("minecraft:wall_sign", 68); ++ BLOCK_NAME_TO_ID.put("minecraft:lever", 69); ++ BLOCK_NAME_TO_ID.put("minecraft:stone_pressure_plate", 70); ++ BLOCK_NAME_TO_ID.put("minecraft:iron_door", 71); ++ BLOCK_NAME_TO_ID.put("minecraft:wooden_pressure_plate", 72); ++ BLOCK_NAME_TO_ID.put("minecraft:redstone_ore", 73); ++ BLOCK_NAME_TO_ID.put("minecraft:lit_redstone_ore", 74); ++ BLOCK_NAME_TO_ID.put("minecraft:unlit_redstone_torch", 75); ++ BLOCK_NAME_TO_ID.put("minecraft:redstone_torch", 76); ++ BLOCK_NAME_TO_ID.put("minecraft:stone_button", 77); ++ BLOCK_NAME_TO_ID.put("minecraft:snow_layer", 78); ++ BLOCK_NAME_TO_ID.put("minecraft:ice", 79); ++ BLOCK_NAME_TO_ID.put("minecraft:snow", 80); ++ BLOCK_NAME_TO_ID.put("minecraft:cactus", 81); ++ BLOCK_NAME_TO_ID.put("minecraft:clay", 82); ++ BLOCK_NAME_TO_ID.put("minecraft:reeds", 83); ++ BLOCK_NAME_TO_ID.put("minecraft:jukebox", 84); ++ BLOCK_NAME_TO_ID.put("minecraft:fence", 85); ++ BLOCK_NAME_TO_ID.put("minecraft:pumpkin", 86); ++ BLOCK_NAME_TO_ID.put("minecraft:netherrack", 87); ++ BLOCK_NAME_TO_ID.put("minecraft:soul_sand", 88); ++ BLOCK_NAME_TO_ID.put("minecraft:glowstone", 89); ++ BLOCK_NAME_TO_ID.put("minecraft:portal", 90); ++ BLOCK_NAME_TO_ID.put("minecraft:lit_pumpkin", 91); ++ BLOCK_NAME_TO_ID.put("minecraft:cake", 92); ++ BLOCK_NAME_TO_ID.put("minecraft:unpowered_repeater", 93); ++ BLOCK_NAME_TO_ID.put("minecraft:powered_repeater", 94); ++ BLOCK_NAME_TO_ID.put("minecraft:stained_glass", 95); ++ BLOCK_NAME_TO_ID.put("minecraft:trapdoor", 96); ++ BLOCK_NAME_TO_ID.put("minecraft:monster_egg", 97); ++ BLOCK_NAME_TO_ID.put("minecraft:stonebrick", 98); ++ BLOCK_NAME_TO_ID.put("minecraft:brown_mushroom_block", 99); ++ BLOCK_NAME_TO_ID.put("minecraft:red_mushroom_block", 100); ++ BLOCK_NAME_TO_ID.put("minecraft:iron_bars", 101); ++ BLOCK_NAME_TO_ID.put("minecraft:glass_pane", 102); ++ BLOCK_NAME_TO_ID.put("minecraft:melon_block", 103); ++ BLOCK_NAME_TO_ID.put("minecraft:pumpkin_stem", 104); ++ BLOCK_NAME_TO_ID.put("minecraft:melon_stem", 105); ++ BLOCK_NAME_TO_ID.put("minecraft:vine", 106); ++ BLOCK_NAME_TO_ID.put("minecraft:fence_gate", 107); ++ BLOCK_NAME_TO_ID.put("minecraft:brick_stairs", 108); ++ BLOCK_NAME_TO_ID.put("minecraft:stone_brick_stairs", 109); ++ BLOCK_NAME_TO_ID.put("minecraft:mycelium", 110); ++ BLOCK_NAME_TO_ID.put("minecraft:waterlily", 111); ++ BLOCK_NAME_TO_ID.put("minecraft:nether_brick", 112); ++ BLOCK_NAME_TO_ID.put("minecraft:nether_brick_fence", 113); ++ BLOCK_NAME_TO_ID.put("minecraft:nether_brick_stairs", 114); ++ BLOCK_NAME_TO_ID.put("minecraft:nether_wart", 115); ++ BLOCK_NAME_TO_ID.put("minecraft:enchanting_table", 116); ++ BLOCK_NAME_TO_ID.put("minecraft:brewing_stand", 117); ++ BLOCK_NAME_TO_ID.put("minecraft:cauldron", 118); ++ BLOCK_NAME_TO_ID.put("minecraft:end_portal", 119); ++ BLOCK_NAME_TO_ID.put("minecraft:end_portal_frame", 120); ++ BLOCK_NAME_TO_ID.put("minecraft:end_stone", 121); ++ BLOCK_NAME_TO_ID.put("minecraft:dragon_egg", 122); ++ BLOCK_NAME_TO_ID.put("minecraft:redstone_lamp", 123); ++ BLOCK_NAME_TO_ID.put("minecraft:lit_redstone_lamp", 124); ++ BLOCK_NAME_TO_ID.put("minecraft:double_wooden_slab", 125); ++ BLOCK_NAME_TO_ID.put("minecraft:wooden_slab", 126); ++ BLOCK_NAME_TO_ID.put("minecraft:cocoa", 127); ++ BLOCK_NAME_TO_ID.put("minecraft:sandstone_stairs", 128); ++ BLOCK_NAME_TO_ID.put("minecraft:emerald_ore", 129); ++ BLOCK_NAME_TO_ID.put("minecraft:ender_chest", 130); ++ BLOCK_NAME_TO_ID.put("minecraft:tripwire_hook", 131); ++ BLOCK_NAME_TO_ID.put("minecraft:tripwire", 132); ++ BLOCK_NAME_TO_ID.put("minecraft:emerald_block", 133); ++ BLOCK_NAME_TO_ID.put("minecraft:spruce_stairs", 134); ++ BLOCK_NAME_TO_ID.put("minecraft:birch_stairs", 135); ++ BLOCK_NAME_TO_ID.put("minecraft:jungle_stairs", 136); ++ BLOCK_NAME_TO_ID.put("minecraft:command_block", 137); ++ BLOCK_NAME_TO_ID.put("minecraft:beacon", 138); ++ BLOCK_NAME_TO_ID.put("minecraft:cobblestone_wall", 139); ++ BLOCK_NAME_TO_ID.put("minecraft:flower_pot", 140); ++ BLOCK_NAME_TO_ID.put("minecraft:carrots", 141); ++ BLOCK_NAME_TO_ID.put("minecraft:potatoes", 142); ++ BLOCK_NAME_TO_ID.put("minecraft:wooden_button", 143); ++ BLOCK_NAME_TO_ID.put("minecraft:skull", 144); ++ BLOCK_NAME_TO_ID.put("minecraft:anvil", 145); ++ BLOCK_NAME_TO_ID.put("minecraft:trapped_chest", 146); ++ BLOCK_NAME_TO_ID.put("minecraft:light_weighted_pressure_plate", 147); ++ BLOCK_NAME_TO_ID.put("minecraft:heavy_weighted_pressure_plate", 148); ++ BLOCK_NAME_TO_ID.put("minecraft:unpowered_comparator", 149); ++ BLOCK_NAME_TO_ID.put("minecraft:powered_comparator", 150); ++ BLOCK_NAME_TO_ID.put("minecraft:daylight_detector", 151); ++ BLOCK_NAME_TO_ID.put("minecraft:redstone_block", 152); ++ BLOCK_NAME_TO_ID.put("minecraft:quartz_ore", 153); ++ BLOCK_NAME_TO_ID.put("minecraft:hopper", 154); ++ BLOCK_NAME_TO_ID.put("minecraft:quartz_block", 155); ++ BLOCK_NAME_TO_ID.put("minecraft:quartz_stairs", 156); ++ BLOCK_NAME_TO_ID.put("minecraft:activator_rail", 157); ++ BLOCK_NAME_TO_ID.put("minecraft:dropper", 158); ++ BLOCK_NAME_TO_ID.put("minecraft:stained_hardened_clay", 159); ++ BLOCK_NAME_TO_ID.put("minecraft:stained_glass_pane", 160); ++ BLOCK_NAME_TO_ID.put("minecraft:leaves2", 161); ++ BLOCK_NAME_TO_ID.put("minecraft:log2", 162); ++ BLOCK_NAME_TO_ID.put("minecraft:acacia_stairs", 163); ++ BLOCK_NAME_TO_ID.put("minecraft:dark_oak_stairs", 164); ++ BLOCK_NAME_TO_ID.put("minecraft:slime", 165); ++ BLOCK_NAME_TO_ID.put("minecraft:barrier", 166); ++ BLOCK_NAME_TO_ID.put("minecraft:iron_trapdoor", 167); ++ BLOCK_NAME_TO_ID.put("minecraft:prismarine", 168); ++ BLOCK_NAME_TO_ID.put("minecraft:sea_lantern", 169); ++ BLOCK_NAME_TO_ID.put("minecraft:hay_block", 170); ++ BLOCK_NAME_TO_ID.put("minecraft:carpet", 171); ++ BLOCK_NAME_TO_ID.put("minecraft:hardened_clay", 172); ++ BLOCK_NAME_TO_ID.put("minecraft:coal_block", 173); ++ BLOCK_NAME_TO_ID.put("minecraft:packed_ice", 174); ++ BLOCK_NAME_TO_ID.put("minecraft:double_plant", 175); ++ BLOCK_NAME_TO_ID.put("minecraft:standing_banner", 176); ++ BLOCK_NAME_TO_ID.put("minecraft:wall_banner", 177); ++ BLOCK_NAME_TO_ID.put("minecraft:daylight_detector_inverted", 178); ++ BLOCK_NAME_TO_ID.put("minecraft:red_sandstone", 179); ++ BLOCK_NAME_TO_ID.put("minecraft:red_sandstone_stairs", 180); ++ BLOCK_NAME_TO_ID.put("minecraft:double_stone_slab2", 181); ++ BLOCK_NAME_TO_ID.put("minecraft:stone_slab2", 182); ++ BLOCK_NAME_TO_ID.put("minecraft:spruce_fence_gate", 183); ++ BLOCK_NAME_TO_ID.put("minecraft:birch_fence_gate", 184); ++ BLOCK_NAME_TO_ID.put("minecraft:jungle_fence_gate", 185); ++ BLOCK_NAME_TO_ID.put("minecraft:dark_oak_fence_gate", 186); ++ BLOCK_NAME_TO_ID.put("minecraft:acacia_fence_gate", 187); ++ BLOCK_NAME_TO_ID.put("minecraft:spruce_fence", 188); ++ BLOCK_NAME_TO_ID.put("minecraft:birch_fence", 189); ++ BLOCK_NAME_TO_ID.put("minecraft:jungle_fence", 190); ++ BLOCK_NAME_TO_ID.put("minecraft:dark_oak_fence", 191); ++ BLOCK_NAME_TO_ID.put("minecraft:acacia_fence", 192); ++ BLOCK_NAME_TO_ID.put("minecraft:spruce_door", 193); ++ BLOCK_NAME_TO_ID.put("minecraft:birch_door", 194); ++ BLOCK_NAME_TO_ID.put("minecraft:jungle_door", 195); ++ BLOCK_NAME_TO_ID.put("minecraft:acacia_door", 196); ++ BLOCK_NAME_TO_ID.put("minecraft:dark_oak_door", 197); ++ BLOCK_NAME_TO_ID.put("minecraft:end_rod", 198); ++ BLOCK_NAME_TO_ID.put("minecraft:chorus_plant", 199); ++ BLOCK_NAME_TO_ID.put("minecraft:chorus_flower", 200); ++ BLOCK_NAME_TO_ID.put("minecraft:purpur_block", 201); ++ BLOCK_NAME_TO_ID.put("minecraft:purpur_pillar", 202); ++ BLOCK_NAME_TO_ID.put("minecraft:purpur_stairs", 203); ++ BLOCK_NAME_TO_ID.put("minecraft:purpur_double_slab", 204); ++ BLOCK_NAME_TO_ID.put("minecraft:purpur_slab", 205); ++ BLOCK_NAME_TO_ID.put("minecraft:end_bricks", 206); ++ BLOCK_NAME_TO_ID.put("minecraft:beetroots", 207); ++ BLOCK_NAME_TO_ID.put("minecraft:grass_path", 208); ++ BLOCK_NAME_TO_ID.put("minecraft:end_gateway", 209); ++ BLOCK_NAME_TO_ID.put("minecraft:repeating_command_block", 210); ++ BLOCK_NAME_TO_ID.put("minecraft:chain_command_block", 211); ++ BLOCK_NAME_TO_ID.put("minecraft:frosted_ice", 212); ++ BLOCK_NAME_TO_ID.put("minecraft:magma", 213); ++ BLOCK_NAME_TO_ID.put("minecraft:nether_wart_block", 214); ++ BLOCK_NAME_TO_ID.put("minecraft:red_nether_brick", 215); ++ BLOCK_NAME_TO_ID.put("minecraft:bone_block", 216); ++ BLOCK_NAME_TO_ID.put("minecraft:structure_void", 217); ++ BLOCK_NAME_TO_ID.put("minecraft:observer", 218); ++ BLOCK_NAME_TO_ID.put("minecraft:white_shulker_box", 219); ++ BLOCK_NAME_TO_ID.put("minecraft:orange_shulker_box", 220); ++ BLOCK_NAME_TO_ID.put("minecraft:magenta_shulker_box", 221); ++ BLOCK_NAME_TO_ID.put("minecraft:light_blue_shulker_box", 222); ++ BLOCK_NAME_TO_ID.put("minecraft:yellow_shulker_box", 223); ++ BLOCK_NAME_TO_ID.put("minecraft:lime_shulker_box", 224); ++ BLOCK_NAME_TO_ID.put("minecraft:pink_shulker_box", 225); ++ BLOCK_NAME_TO_ID.put("minecraft:gray_shulker_box", 226); ++ BLOCK_NAME_TO_ID.put("minecraft:silver_shulker_box", 227); ++ BLOCK_NAME_TO_ID.put("minecraft:cyan_shulker_box", 228); ++ BLOCK_NAME_TO_ID.put("minecraft:purple_shulker_box", 229); ++ BLOCK_NAME_TO_ID.put("minecraft:blue_shulker_box", 230); ++ BLOCK_NAME_TO_ID.put("minecraft:brown_shulker_box", 231); ++ BLOCK_NAME_TO_ID.put("minecraft:green_shulker_box", 232); ++ BLOCK_NAME_TO_ID.put("minecraft:red_shulker_box", 233); ++ BLOCK_NAME_TO_ID.put("minecraft:black_shulker_box", 234); ++ BLOCK_NAME_TO_ID.put("minecraft:white_glazed_terracotta", 235); ++ BLOCK_NAME_TO_ID.put("minecraft:orange_glazed_terracotta", 236); ++ BLOCK_NAME_TO_ID.put("minecraft:magenta_glazed_terracotta", 237); ++ BLOCK_NAME_TO_ID.put("minecraft:light_blue_glazed_terracotta", 238); ++ BLOCK_NAME_TO_ID.put("minecraft:yellow_glazed_terracotta", 239); ++ BLOCK_NAME_TO_ID.put("minecraft:lime_glazed_terracotta", 240); ++ BLOCK_NAME_TO_ID.put("minecraft:pink_glazed_terracotta", 241); ++ BLOCK_NAME_TO_ID.put("minecraft:gray_glazed_terracotta", 242); ++ BLOCK_NAME_TO_ID.put("minecraft:silver_glazed_terracotta", 243); ++ BLOCK_NAME_TO_ID.put("minecraft:cyan_glazed_terracotta", 244); ++ BLOCK_NAME_TO_ID.put("minecraft:purple_glazed_terracotta", 245); ++ BLOCK_NAME_TO_ID.put("minecraft:blue_glazed_terracotta", 246); ++ BLOCK_NAME_TO_ID.put("minecraft:brown_glazed_terracotta", 247); ++ BLOCK_NAME_TO_ID.put("minecraft:green_glazed_terracotta", 248); ++ BLOCK_NAME_TO_ID.put("minecraft:red_glazed_terracotta", 249); ++ BLOCK_NAME_TO_ID.put("minecraft:black_glazed_terracotta", 250); ++ BLOCK_NAME_TO_ID.put("minecraft:concrete", 251); ++ BLOCK_NAME_TO_ID.put("minecraft:concrete_powder", 252); ++ BLOCK_NAME_TO_ID.put("minecraft:structure_block", 255); ++ } ++ ++ protected static final int VERSION = MCVersions.V17W47A; ++ ++ protected final String[] paths; ++ ++ public ConverterFlattenEntity(final String... paths) { ++ super(VERSION, 3); ++ this.paths = paths; ++ } ++ ++ private static void register(final String id, final String... paths) { ++ MCTypeRegistry.ENTITY.addConverterForId(id, new ConverterFlattenEntity(paths)); ++ } ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:falling_block", new DataConverter<>(VERSION, 3) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final int blockId; ++ if (data.hasKey("Block")) { ++ final Number id = data.getNumber("Block"); ++ if (id != null) { ++ blockId = id.intValue(); ++ } else { ++ blockId = getBlockId(data.getString("Block")); ++ } ++ } else { ++ final Number tileId = data.getNumber("TileID"); ++ if (tileId != null) { ++ blockId = tileId.intValue(); ++ } else { ++ blockId = data.getByte("Tile") & 255; ++ } ++ } ++ ++ final int blockData = data.getInt("Data") & 15; ++ ++ data.remove("Block"); // from type update ++ data.remove("Data"); ++ data.remove("TileID"); ++ data.remove("Tile"); ++ ++ // key is from type update ++ data.setMap("BlockState", HelperBlockFlatteningV1450.getNBTForId((blockId << 4) | blockData).copy()); // copy to avoid problems with later state datafixers ++ ++ return null; ++ } ++ }); ++ register("minecraft:enderman", "carried", "carriedData", "carriedBlockState"); ++ register("minecraft:arrow", "inTile", "inData", "inBlockState"); ++ register("minecraft:spectral_arrow", "inTile", "inData", "inBlockState"); ++ register("minecraft:egg", "inTile"); ++ register("minecraft:ender_pearl", "inTile"); ++ register("minecraft:fireball", "inTile"); ++ register("minecraft:potion", "inTile"); ++ register("minecraft:small_fireball", "inTile"); ++ register("minecraft:snowball", "inTile"); ++ register("minecraft:wither_skull", "inTile"); ++ register("minecraft:xp_bottle", "inTile"); ++ register("minecraft:commandblock_minecart", "DisplayTile", "DisplayData", "DisplayState"); ++ register("minecraft:minecart", "DisplayTile", "DisplayData", "DisplayState"); ++ register("minecraft:chest_minecart", "DisplayTile", "DisplayData", "DisplayState"); ++ register("minecraft:furnace_minecart", "DisplayTile", "DisplayData", "DisplayState"); ++ register("minecraft:tnt_minecart", "DisplayTile", "DisplayData", "DisplayState"); ++ register("minecraft:hopper_minecart", "DisplayTile", "DisplayData", "DisplayState"); ++ register("minecraft:spawner_minecart", "DisplayTile", "DisplayData", "DisplayState"); ++ } ++ ++ public static int getBlockId(final String block) { ++ final Integer ret = BLOCK_NAME_TO_ID.get(block); ++ return ret == null ? 0 : ret.intValue(); ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (this.paths.length == 1) { ++ data.remove(this.paths[0]); ++ return null; ++ } ++ final String idPath = this.paths[0]; ++ final String dataPath = this.paths[1]; ++ final String outputStatePath = this.paths[2]; ++ ++ final int blockId; ++ if (data.hasKey(idPath, ObjectType.NUMBER)) { ++ blockId = data.getInt(idPath); ++ } else { ++ blockId = getBlockId(data.getString(idPath)); ++ } ++ ++ final int blockData = data.getInt(dataPath) & 15; ++ ++ data.remove(idPath); // from type update ++ data.remove(dataPath); ++ ++ data.setMap(outputStatePath, HelperBlockFlatteningV1450.getNBTForId((blockId << 4) | blockData).copy()); // copy to avoid problems with later state datafixers ++ ++ return null; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/AddFlagIfAbsent.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/AddFlagIfAbsent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4ab607f946782cc483535564e86fa9753dd7897a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/AddFlagIfAbsent.java +@@ -0,0 +1,30 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.helpers; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class AddFlagIfAbsent extends DataConverter, MapType> { ++ ++ public final String path; ++ public final boolean dfl; ++ ++ public AddFlagIfAbsent(final int toVersion, final String path, final boolean dfl) { ++ super(toVersion); ++ this.path = path; ++ this.dfl = dfl; ++ } ++ ++ public AddFlagIfAbsent(final int toVersion, final int versionStep, final String path, final boolean dfl) { ++ super(toVersion, versionStep); ++ this.path = path; ++ this.dfl = dfl; ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (!data.hasKey(this.path)) { ++ data.setBoolean(this.path, this.dfl); ++ } ++ return null; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/ConverterAbstractStringValueTypeRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/ConverterAbstractStringValueTypeRename.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bc79670f47aaa413ea3e96ef6a32e14099ad8a58 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/ConverterAbstractStringValueTypeRename.java +@@ -0,0 +1,24 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.helpers; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCValueType; ++import java.util.function.Function; ++ ++public final class ConverterAbstractStringValueTypeRename { ++ ++ private ConverterAbstractStringValueTypeRename() {} ++ ++ public static void register(final int version, final MCValueType type, final Function renamer) { ++ register(version, 0, type, renamer); ++ } ++ public static void register(final int version, final int subVersion, final MCValueType type, final Function renamer) { ++ type.addConverter(new DataConverter<>(version, subVersion) { ++ @Override ++ public Object convert(final Object data, final long sourceVersion, final long toVersion) { ++ final String ret = (data instanceof String) ? renamer.apply((String)data) : null; ++ return ret == data ? null : ret; ++ } ++ }); ++ } ++ ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperBlockFlatteningV1450.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperBlockFlatteningV1450.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4f4f4cb6037c2a46ffcf427f5812164bbb98b8b7 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperBlockFlatteningV1450.java +@@ -0,0 +1,1829 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.helpers; ++ ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.nbt.NBTMapType; ++import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; ++import net.minecraft.nbt.TagParser; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class HelperBlockFlatteningV1450 { ++ ++ protected static final MapType[] FLATTENED_BY_ID = new MapType[4096]; ++ protected static final MapType[] BLOCK_DEFAULTS = new MapType[4096]; ++ ++ private static final Object2IntOpenHashMap> ID_BY_OLD_NBT = new Object2IntOpenHashMap>(64, 0.7f) { ++ @Override ++ public int put(final MapType o, final int v) { ++ if (this.containsKey(o)) { ++ throw new RuntimeException("Already contains mapping for " + o); ++ } ++ ++ return super.put(o, v); ++ } ++ }; ++ static { ++ ID_BY_OLD_NBT.defaultReturnValue(-1); ++ } ++ ++ private static final Object2IntOpenHashMap ID_BY_OLD_NAME = new Object2IntOpenHashMap(64, 0.7f) { ++ @Override ++ public int put(final String o, final int v) { ++ if (this.containsKey(o)) { ++ throw new RuntimeException("Already contains mapping for " + o); ++ } ++ ++ return super.put(o, v); ++ } ++ }; ++ static { ++ ID_BY_OLD_NAME.defaultReturnValue(-1); ++ } ++ ++ // map used to ensure that each parsed block state contains no duplicates ++ protected static final Map, MapType> IDENTITY_ENSURE = new HashMap<>(); ++ ++ public static MapType parseTag(final String blockstate) { ++ try { ++ final MapType ret = new NBTMapType(TagParser.parseTag(blockstate.replace('\'', '"'))); ++ ++ synchronized (IDENTITY_ENSURE) { ++ final MapType identity = IDENTITY_ENSURE.putIfAbsent(ret, ret); ++ ++ return identity == null ? ret : identity; ++ } ++ ++ } catch (final Exception ex) { ++ throw new RuntimeException("Exception parsing " + blockstate, ex); ++ } ++ } ++ ++ private static void register(final int id, final String flattened, final String... preFlattenings) { ++ final MapType flattenedNBT = parseTag(flattened); ++ if (FLATTENED_BY_ID[id] != null) { ++ throw new RuntimeException("Mapping already exists for id " + id); ++ } ++ FLATTENED_BY_ID[id] = flattenedNBT; ++ ++ // it's important that we register ids from smallest to largest, so that ++ // the default is going to be correct ++ final int block = id >> 4; ++ if (BLOCK_DEFAULTS[block] == null) { ++ BLOCK_DEFAULTS[block] = flattenedNBT; ++ } ++ ++ for (final String preFlattening : preFlattenings) { ++ final MapType preFlatteningNBT = parseTag(preFlattening); ++ final String name = preFlatteningNBT.getString("Name"); ++ if (name == null) { ++ throw new RuntimeException("Name does not exist for pre flattenings for id " + id); ++ } ++ ++ // putIfAbsent so we default to the lowest id, which is going to be the block default ++ ID_BY_OLD_NAME.putIfAbsent(name, id); ++ ID_BY_OLD_NBT.put(preFlatteningNBT, id); ++ } ++ } ++ ++ private static void finalizeMaps() { ++ for(int i = 0; i < FLATTENED_BY_ID.length; ++i) { ++ if (FLATTENED_BY_ID[i] == null) { ++ FLATTENED_BY_ID[i] = BLOCK_DEFAULTS[i >> 4]; ++ } ++ } ++ } ++ ++ public static MapType flattenNBT(final MapType old) { ++ final int id = ID_BY_OLD_NBT.getInt(old); ++ final MapType ret = getNBTForIdRaw(id); ++ ++ return ret == null ? old : ret; ++ } ++ ++ public static String getNewBlockName(final String old) { ++ final int id = ID_BY_OLD_NAME.getInt(old); ++ final MapType ret = getNBTForIdRaw(id); ++ return ret == null ? old : ret.getString("Name"); ++ } ++ ++ public static String getNameForId(final int block) { ++ final MapType nbt = getNBTForIdRaw(block); ++ return nbt == null ? "minecraft:air" : nbt.getString("Name"); ++ } ++ ++ protected static MapType getNBTForIdRaw(final int block) { ++ return block >= 0 && block < FLATTENED_BY_ID.length ? FLATTENED_BY_ID[block] : null; ++ } ++ ++ public static MapType getNBTForId(final int block) { ++ MapType ret = getNBTForIdRaw(block); ++ return ret == null ? FLATTENED_BY_ID[0] : ret; ++ } ++ ++ private HelperBlockFlatteningV1450() {} ++ ++ static { ++ ID_BY_OLD_NBT.defaultReturnValue(-1); ++ register(0, "{Name:'minecraft:air'}", "{Name:'minecraft:air'}"); ++ register(16, "{Name:'minecraft:stone'}", "{Name:'minecraft:stone',Properties:{variant:'stone'}}"); ++ register(17, "{Name:'minecraft:granite'}", "{Name:'minecraft:stone',Properties:{variant:'granite'}}"); ++ register(18, "{Name:'minecraft:polished_granite'}", "{Name:'minecraft:stone',Properties:{variant:'smooth_granite'}}"); ++ register(19, "{Name:'minecraft:diorite'}", "{Name:'minecraft:stone',Properties:{variant:'diorite'}}"); ++ register(20, "{Name:'minecraft:polished_diorite'}", "{Name:'minecraft:stone',Properties:{variant:'smooth_diorite'}}"); ++ register(21, "{Name:'minecraft:andesite'}", "{Name:'minecraft:stone',Properties:{variant:'andesite'}}"); ++ register(22, "{Name:'minecraft:polished_andesite'}", "{Name:'minecraft:stone',Properties:{variant:'smooth_andesite'}}"); ++ register(32, "{Name:'minecraft:grass_block',Properties:{snowy:'false'}}", "{Name:'minecraft:grass',Properties:{snowy:'false'}}", "{Name:'minecraft:grass',Properties:{snowy:'true'}}"); ++ register(48, "{Name:'minecraft:dirt'}", "{Name:'minecraft:dirt',Properties:{snowy:'false',variant:'dirt'}}", "{Name:'minecraft:dirt',Properties:{snowy:'true',variant:'dirt'}}"); ++ register(49, "{Name:'minecraft:coarse_dirt'}", "{Name:'minecraft:dirt',Properties:{snowy:'false',variant:'coarse_dirt'}}", "{Name:'minecraft:dirt',Properties:{snowy:'true',variant:'coarse_dirt'}}"); ++ register(50, "{Name:'minecraft:podzol',Properties:{snowy:'false'}}", "{Name:'minecraft:dirt',Properties:{snowy:'false',variant:'podzol'}}", "{Name:'minecraft:dirt',Properties:{snowy:'true',variant:'podzol'}}"); ++ register(64, "{Name:'minecraft:cobblestone'}", "{Name:'minecraft:cobblestone'}"); ++ register(80, "{Name:'minecraft:oak_planks'}", "{Name:'minecraft:planks',Properties:{variant:'oak'}}"); ++ register(81, "{Name:'minecraft:spruce_planks'}", "{Name:'minecraft:planks',Properties:{variant:'spruce'}}"); ++ register(82, "{Name:'minecraft:birch_planks'}", "{Name:'minecraft:planks',Properties:{variant:'birch'}}"); ++ register(83, "{Name:'minecraft:jungle_planks'}", "{Name:'minecraft:planks',Properties:{variant:'jungle'}}"); ++ register(84, "{Name:'minecraft:acacia_planks'}", "{Name:'minecraft:planks',Properties:{variant:'acacia'}}"); ++ register(85, "{Name:'minecraft:dark_oak_planks'}", "{Name:'minecraft:planks',Properties:{variant:'dark_oak'}}"); ++ register(96, "{Name:'minecraft:oak_sapling',Properties:{stage:'0'}}", "{Name:'minecraft:sapling',Properties:{stage:'0',type:'oak'}}"); ++ register(97, "{Name:'minecraft:spruce_sapling',Properties:{stage:'0'}}", "{Name:'minecraft:sapling',Properties:{stage:'0',type:'spruce'}}"); ++ register(98, "{Name:'minecraft:birch_sapling',Properties:{stage:'0'}}", "{Name:'minecraft:sapling',Properties:{stage:'0',type:'birch'}}"); ++ register(99, "{Name:'minecraft:jungle_sapling',Properties:{stage:'0'}}", "{Name:'minecraft:sapling',Properties:{stage:'0',type:'jungle'}}"); ++ register(100, "{Name:'minecraft:acacia_sapling',Properties:{stage:'0'}}", "{Name:'minecraft:sapling',Properties:{stage:'0',type:'acacia'}}"); ++ register(101, "{Name:'minecraft:dark_oak_sapling',Properties:{stage:'0'}}", "{Name:'minecraft:sapling',Properties:{stage:'0',type:'dark_oak'}}"); ++ register(104, "{Name:'minecraft:oak_sapling',Properties:{stage:'1'}}", "{Name:'minecraft:sapling',Properties:{stage:'1',type:'oak'}}"); ++ register(105, "{Name:'minecraft:spruce_sapling',Properties:{stage:'1'}}", "{Name:'minecraft:sapling',Properties:{stage:'1',type:'spruce'}}"); ++ register(106, "{Name:'minecraft:birch_sapling',Properties:{stage:'1'}}", "{Name:'minecraft:sapling',Properties:{stage:'1',type:'birch'}}"); ++ register(107, "{Name:'minecraft:jungle_sapling',Properties:{stage:'1'}}", "{Name:'minecraft:sapling',Properties:{stage:'1',type:'jungle'}}"); ++ register(108, "{Name:'minecraft:acacia_sapling',Properties:{stage:'1'}}", "{Name:'minecraft:sapling',Properties:{stage:'1',type:'acacia'}}"); ++ register(109, "{Name:'minecraft:dark_oak_sapling',Properties:{stage:'1'}}", "{Name:'minecraft:sapling',Properties:{stage:'1',type:'dark_oak'}}"); ++ register(112, "{Name:'minecraft:bedrock'}", "{Name:'minecraft:bedrock'}"); ++ register(128, "{Name:'minecraft:water',Properties:{level:'0'}}", "{Name:'minecraft:flowing_water',Properties:{level:'0'}}"); ++ register(129, "{Name:'minecraft:water',Properties:{level:'1'}}", "{Name:'minecraft:flowing_water',Properties:{level:'1'}}"); ++ register(130, "{Name:'minecraft:water',Properties:{level:'2'}}", "{Name:'minecraft:flowing_water',Properties:{level:'2'}}"); ++ register(131, "{Name:'minecraft:water',Properties:{level:'3'}}", "{Name:'minecraft:flowing_water',Properties:{level:'3'}}"); ++ register(132, "{Name:'minecraft:water',Properties:{level:'4'}}", "{Name:'minecraft:flowing_water',Properties:{level:'4'}}"); ++ register(133, "{Name:'minecraft:water',Properties:{level:'5'}}", "{Name:'minecraft:flowing_water',Properties:{level:'5'}}"); ++ register(134, "{Name:'minecraft:water',Properties:{level:'6'}}", "{Name:'minecraft:flowing_water',Properties:{level:'6'}}"); ++ register(135, "{Name:'minecraft:water',Properties:{level:'7'}}", "{Name:'minecraft:flowing_water',Properties:{level:'7'}}"); ++ register(136, "{Name:'minecraft:water',Properties:{level:'8'}}", "{Name:'minecraft:flowing_water',Properties:{level:'8'}}"); ++ register(137, "{Name:'minecraft:water',Properties:{level:'9'}}", "{Name:'minecraft:flowing_water',Properties:{level:'9'}}"); ++ register(138, "{Name:'minecraft:water',Properties:{level:'10'}}", "{Name:'minecraft:flowing_water',Properties:{level:'10'}}"); ++ register(139, "{Name:'minecraft:water',Properties:{level:'11'}}", "{Name:'minecraft:flowing_water',Properties:{level:'11'}}"); ++ register(140, "{Name:'minecraft:water',Properties:{level:'12'}}", "{Name:'minecraft:flowing_water',Properties:{level:'12'}}"); ++ register(141, "{Name:'minecraft:water',Properties:{level:'13'}}", "{Name:'minecraft:flowing_water',Properties:{level:'13'}}"); ++ register(142, "{Name:'minecraft:water',Properties:{level:'14'}}", "{Name:'minecraft:flowing_water',Properties:{level:'14'}}"); ++ register(143, "{Name:'minecraft:water',Properties:{level:'15'}}", "{Name:'minecraft:flowing_water',Properties:{level:'15'}}"); ++ register(144, "{Name:'minecraft:water',Properties:{level:'0'}}", "{Name:'minecraft:water',Properties:{level:'0'}}"); ++ register(145, "{Name:'minecraft:water',Properties:{level:'1'}}", "{Name:'minecraft:water',Properties:{level:'1'}}"); ++ register(146, "{Name:'minecraft:water',Properties:{level:'2'}}", "{Name:'minecraft:water',Properties:{level:'2'}}"); ++ register(147, "{Name:'minecraft:water',Properties:{level:'3'}}", "{Name:'minecraft:water',Properties:{level:'3'}}"); ++ register(148, "{Name:'minecraft:water',Properties:{level:'4'}}", "{Name:'minecraft:water',Properties:{level:'4'}}"); ++ register(149, "{Name:'minecraft:water',Properties:{level:'5'}}", "{Name:'minecraft:water',Properties:{level:'5'}}"); ++ register(150, "{Name:'minecraft:water',Properties:{level:'6'}}", "{Name:'minecraft:water',Properties:{level:'6'}}"); ++ register(151, "{Name:'minecraft:water',Properties:{level:'7'}}", "{Name:'minecraft:water',Properties:{level:'7'}}"); ++ register(152, "{Name:'minecraft:water',Properties:{level:'8'}}", "{Name:'minecraft:water',Properties:{level:'8'}}"); ++ register(153, "{Name:'minecraft:water',Properties:{level:'9'}}", "{Name:'minecraft:water',Properties:{level:'9'}}"); ++ register(154, "{Name:'minecraft:water',Properties:{level:'10'}}", "{Name:'minecraft:water',Properties:{level:'10'}}"); ++ register(155, "{Name:'minecraft:water',Properties:{level:'11'}}", "{Name:'minecraft:water',Properties:{level:'11'}}"); ++ register(156, "{Name:'minecraft:water',Properties:{level:'12'}}", "{Name:'minecraft:water',Properties:{level:'12'}}"); ++ register(157, "{Name:'minecraft:water',Properties:{level:'13'}}", "{Name:'minecraft:water',Properties:{level:'13'}}"); ++ register(158, "{Name:'minecraft:water',Properties:{level:'14'}}", "{Name:'minecraft:water',Properties:{level:'14'}}"); ++ register(159, "{Name:'minecraft:water',Properties:{level:'15'}}", "{Name:'minecraft:water',Properties:{level:'15'}}"); ++ register(160, "{Name:'minecraft:lava',Properties:{level:'0'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'0'}}"); ++ register(161, "{Name:'minecraft:lava',Properties:{level:'1'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'1'}}"); ++ register(162, "{Name:'minecraft:lava',Properties:{level:'2'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'2'}}"); ++ register(163, "{Name:'minecraft:lava',Properties:{level:'3'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'3'}}"); ++ register(164, "{Name:'minecraft:lava',Properties:{level:'4'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'4'}}"); ++ register(165, "{Name:'minecraft:lava',Properties:{level:'5'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'5'}}"); ++ register(166, "{Name:'minecraft:lava',Properties:{level:'6'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'6'}}"); ++ register(167, "{Name:'minecraft:lava',Properties:{level:'7'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'7'}}"); ++ register(168, "{Name:'minecraft:lava',Properties:{level:'8'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'8'}}"); ++ register(169, "{Name:'minecraft:lava',Properties:{level:'9'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'9'}}"); ++ register(170, "{Name:'minecraft:lava',Properties:{level:'10'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'10'}}"); ++ register(171, "{Name:'minecraft:lava',Properties:{level:'11'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'11'}}"); ++ register(172, "{Name:'minecraft:lava',Properties:{level:'12'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'12'}}"); ++ register(173, "{Name:'minecraft:lava',Properties:{level:'13'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'13'}}"); ++ register(174, "{Name:'minecraft:lava',Properties:{level:'14'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'14'}}"); ++ register(175, "{Name:'minecraft:lava',Properties:{level:'15'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'15'}}"); ++ register(176, "{Name:'minecraft:lava',Properties:{level:'0'}}", "{Name:'minecraft:lava',Properties:{level:'0'}}"); ++ register(177, "{Name:'minecraft:lava',Properties:{level:'1'}}", "{Name:'minecraft:lava',Properties:{level:'1'}}"); ++ register(178, "{Name:'minecraft:lava',Properties:{level:'2'}}", "{Name:'minecraft:lava',Properties:{level:'2'}}"); ++ register(179, "{Name:'minecraft:lava',Properties:{level:'3'}}", "{Name:'minecraft:lava',Properties:{level:'3'}}"); ++ register(180, "{Name:'minecraft:lava',Properties:{level:'4'}}", "{Name:'minecraft:lava',Properties:{level:'4'}}"); ++ register(181, "{Name:'minecraft:lava',Properties:{level:'5'}}", "{Name:'minecraft:lava',Properties:{level:'5'}}"); ++ register(182, "{Name:'minecraft:lava',Properties:{level:'6'}}", "{Name:'minecraft:lava',Properties:{level:'6'}}"); ++ register(183, "{Name:'minecraft:lava',Properties:{level:'7'}}", "{Name:'minecraft:lava',Properties:{level:'7'}}"); ++ register(184, "{Name:'minecraft:lava',Properties:{level:'8'}}", "{Name:'minecraft:lava',Properties:{level:'8'}}"); ++ register(185, "{Name:'minecraft:lava',Properties:{level:'9'}}", "{Name:'minecraft:lava',Properties:{level:'9'}}"); ++ register(186, "{Name:'minecraft:lava',Properties:{level:'10'}}", "{Name:'minecraft:lava',Properties:{level:'10'}}"); ++ register(187, "{Name:'minecraft:lava',Properties:{level:'11'}}", "{Name:'minecraft:lava',Properties:{level:'11'}}"); ++ register(188, "{Name:'minecraft:lava',Properties:{level:'12'}}", "{Name:'minecraft:lava',Properties:{level:'12'}}"); ++ register(189, "{Name:'minecraft:lava',Properties:{level:'13'}}", "{Name:'minecraft:lava',Properties:{level:'13'}}"); ++ register(190, "{Name:'minecraft:lava',Properties:{level:'14'}}", "{Name:'minecraft:lava',Properties:{level:'14'}}"); ++ register(191, "{Name:'minecraft:lava',Properties:{level:'15'}}", "{Name:'minecraft:lava',Properties:{level:'15'}}"); ++ register(192, "{Name:'minecraft:sand'}", "{Name:'minecraft:sand',Properties:{variant:'sand'}}"); ++ register(193, "{Name:'minecraft:red_sand'}", "{Name:'minecraft:sand',Properties:{variant:'red_sand'}}"); ++ register(208, "{Name:'minecraft:gravel'}", "{Name:'minecraft:gravel'}"); ++ register(224, "{Name:'minecraft:gold_ore'}", "{Name:'minecraft:gold_ore'}"); ++ register(240, "{Name:'minecraft:iron_ore'}", "{Name:'minecraft:iron_ore'}"); ++ register(256, "{Name:'minecraft:coal_ore'}", "{Name:'minecraft:coal_ore'}"); ++ register(272, "{Name:'minecraft:oak_log',Properties:{axis:'y'}}", "{Name:'minecraft:log',Properties:{axis:'y',variant:'oak'}}"); ++ register(273, "{Name:'minecraft:spruce_log',Properties:{axis:'y'}}", "{Name:'minecraft:log',Properties:{axis:'y',variant:'spruce'}}"); ++ register(274, "{Name:'minecraft:birch_log',Properties:{axis:'y'}}", "{Name:'minecraft:log',Properties:{axis:'y',variant:'birch'}}"); ++ register(275, "{Name:'minecraft:jungle_log',Properties:{axis:'y'}}", "{Name:'minecraft:log',Properties:{axis:'y',variant:'jungle'}}"); ++ register(276, "{Name:'minecraft:oak_log',Properties:{axis:'x'}}", "{Name:'minecraft:log',Properties:{axis:'x',variant:'oak'}}"); ++ register(277, "{Name:'minecraft:spruce_log',Properties:{axis:'x'}}", "{Name:'minecraft:log',Properties:{axis:'x',variant:'spruce'}}"); ++ register(278, "{Name:'minecraft:birch_log',Properties:{axis:'x'}}", "{Name:'minecraft:log',Properties:{axis:'x',variant:'birch'}}"); ++ register(279, "{Name:'minecraft:jungle_log',Properties:{axis:'x'}}", "{Name:'minecraft:log',Properties:{axis:'x',variant:'jungle'}}"); ++ register(280, "{Name:'minecraft:oak_log',Properties:{axis:'z'}}", "{Name:'minecraft:log',Properties:{axis:'z',variant:'oak'}}"); ++ register(281, "{Name:'minecraft:spruce_log',Properties:{axis:'z'}}", "{Name:'minecraft:log',Properties:{axis:'z',variant:'spruce'}}"); ++ register(282, "{Name:'minecraft:birch_log',Properties:{axis:'z'}}", "{Name:'minecraft:log',Properties:{axis:'z',variant:'birch'}}"); ++ register(283, "{Name:'minecraft:jungle_log',Properties:{axis:'z'}}", "{Name:'minecraft:log',Properties:{axis:'z',variant:'jungle'}}"); ++ register(284, "{Name:'minecraft:oak_bark'}", "{Name:'minecraft:log',Properties:{axis:'none',variant:'oak'}}"); ++ register(285, "{Name:'minecraft:spruce_bark'}", "{Name:'minecraft:log',Properties:{axis:'none',variant:'spruce'}}"); ++ register(286, "{Name:'minecraft:birch_bark'}", "{Name:'minecraft:log',Properties:{axis:'none',variant:'birch'}}"); ++ register(287, "{Name:'minecraft:jungle_bark'}", "{Name:'minecraft:log',Properties:{axis:'none',variant:'jungle'}}"); ++ register(288, "{Name:'minecraft:oak_leaves',Properties:{check_decay:'false',decayable:'true'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'false',decayable:'true',variant:'oak'}}"); ++ register(289, "{Name:'minecraft:spruce_leaves',Properties:{check_decay:'false',decayable:'true'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'false',decayable:'true',variant:'spruce'}}"); ++ register(290, "{Name:'minecraft:birch_leaves',Properties:{check_decay:'false',decayable:'true'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'false',decayable:'true',variant:'birch'}}"); ++ register(291, "{Name:'minecraft:jungle_leaves',Properties:{check_decay:'false',decayable:'true'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'false',decayable:'true',variant:'jungle'}}"); ++ register(292, "{Name:'minecraft:oak_leaves',Properties:{check_decay:'false',decayable:'false'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'false',decayable:'false',variant:'oak'}}"); ++ register(293, "{Name:'minecraft:spruce_leaves',Properties:{check_decay:'false',decayable:'false'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'false',decayable:'false',variant:'spruce'}}"); ++ register(294, "{Name:'minecraft:birch_leaves',Properties:{check_decay:'false',decayable:'false'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'false',decayable:'false',variant:'birch'}}"); ++ register(295, "{Name:'minecraft:jungle_leaves',Properties:{check_decay:'false',decayable:'false'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'false',decayable:'false',variant:'jungle'}}"); ++ register(296, "{Name:'minecraft:oak_leaves',Properties:{check_decay:'true',decayable:'true'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'true',decayable:'true',variant:'oak'}}"); ++ register(297, "{Name:'minecraft:spruce_leaves',Properties:{check_decay:'true',decayable:'true'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'true',decayable:'true',variant:'spruce'}}"); ++ register(298, "{Name:'minecraft:birch_leaves',Properties:{check_decay:'true',decayable:'true'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'true',decayable:'true',variant:'birch'}}"); ++ register(299, "{Name:'minecraft:jungle_leaves',Properties:{check_decay:'true',decayable:'true'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'true',decayable:'true',variant:'jungle'}}"); ++ register(300, "{Name:'minecraft:oak_leaves',Properties:{check_decay:'true',decayable:'false'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'true',decayable:'false',variant:'oak'}}"); ++ register(301, "{Name:'minecraft:spruce_leaves',Properties:{check_decay:'true',decayable:'false'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'true',decayable:'false',variant:'spruce'}}"); ++ register(302, "{Name:'minecraft:birch_leaves',Properties:{check_decay:'true',decayable:'false'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'true',decayable:'false',variant:'birch'}}"); ++ register(303, "{Name:'minecraft:jungle_leaves',Properties:{check_decay:'true',decayable:'false'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'true',decayable:'false',variant:'jungle'}}"); ++ register(304, "{Name:'minecraft:sponge'}", "{Name:'minecraft:sponge',Properties:{wet:'false'}}"); ++ register(305, "{Name:'minecraft:wet_sponge'}", "{Name:'minecraft:sponge',Properties:{wet:'true'}}"); ++ register(320, "{Name:'minecraft:glass'}", "{Name:'minecraft:glass'}"); ++ register(336, "{Name:'minecraft:lapis_ore'}", "{Name:'minecraft:lapis_ore'}"); ++ register(352, "{Name:'minecraft:lapis_block'}", "{Name:'minecraft:lapis_block'}"); ++ register(368, "{Name:'minecraft:dispenser',Properties:{facing:'down',triggered:'false'}}", "{Name:'minecraft:dispenser',Properties:{facing:'down',triggered:'false'}}"); ++ register(369, "{Name:'minecraft:dispenser',Properties:{facing:'up',triggered:'false'}}", "{Name:'minecraft:dispenser',Properties:{facing:'up',triggered:'false'}}"); ++ register(370, "{Name:'minecraft:dispenser',Properties:{facing:'north',triggered:'false'}}", "{Name:'minecraft:dispenser',Properties:{facing:'north',triggered:'false'}}"); ++ register(371, "{Name:'minecraft:dispenser',Properties:{facing:'south',triggered:'false'}}", "{Name:'minecraft:dispenser',Properties:{facing:'south',triggered:'false'}}"); ++ register(372, "{Name:'minecraft:dispenser',Properties:{facing:'west',triggered:'false'}}", "{Name:'minecraft:dispenser',Properties:{facing:'west',triggered:'false'}}"); ++ register(373, "{Name:'minecraft:dispenser',Properties:{facing:'east',triggered:'false'}}", "{Name:'minecraft:dispenser',Properties:{facing:'east',triggered:'false'}}"); ++ register(376, "{Name:'minecraft:dispenser',Properties:{facing:'down',triggered:'true'}}", "{Name:'minecraft:dispenser',Properties:{facing:'down',triggered:'true'}}"); ++ register(377, "{Name:'minecraft:dispenser',Properties:{facing:'up',triggered:'true'}}", "{Name:'minecraft:dispenser',Properties:{facing:'up',triggered:'true'}}"); ++ register(378, "{Name:'minecraft:dispenser',Properties:{facing:'north',triggered:'true'}}", "{Name:'minecraft:dispenser',Properties:{facing:'north',triggered:'true'}}"); ++ register(379, "{Name:'minecraft:dispenser',Properties:{facing:'south',triggered:'true'}}", "{Name:'minecraft:dispenser',Properties:{facing:'south',triggered:'true'}}"); ++ register(380, "{Name:'minecraft:dispenser',Properties:{facing:'west',triggered:'true'}}", "{Name:'minecraft:dispenser',Properties:{facing:'west',triggered:'true'}}"); ++ register(381, "{Name:'minecraft:dispenser',Properties:{facing:'east',triggered:'true'}}", "{Name:'minecraft:dispenser',Properties:{facing:'east',triggered:'true'}}"); ++ register(384, "{Name:'minecraft:sandstone'}", "{Name:'minecraft:sandstone',Properties:{type:'sandstone'}}"); ++ register(385, "{Name:'minecraft:chiseled_sandstone'}", "{Name:'minecraft:sandstone',Properties:{type:'chiseled_sandstone'}}"); ++ register(386, "{Name:'minecraft:cut_sandstone'}", "{Name:'minecraft:sandstone',Properties:{type:'smooth_sandstone'}}"); ++ register(400, "{Name:'minecraft:note_block'}", "{Name:'minecraft:noteblock'}"); ++ register(416, "{Name:'minecraft:red_bed',Properties:{facing:'south',occupied:'false',part:'foot'}}", "{Name:'minecraft:bed',Properties:{facing:'south',occupied:'false',part:'foot'}}", "{Name:'minecraft:bed',Properties:{facing:'south',occupied:'true',part:'foot'}}"); ++ register(417, "{Name:'minecraft:red_bed',Properties:{facing:'west',occupied:'false',part:'foot'}}", "{Name:'minecraft:bed',Properties:{facing:'west',occupied:'false',part:'foot'}}", "{Name:'minecraft:bed',Properties:{facing:'west',occupied:'true',part:'foot'}}"); ++ register(418, "{Name:'minecraft:red_bed',Properties:{facing:'north',occupied:'false',part:'foot'}}", "{Name:'minecraft:bed',Properties:{facing:'north',occupied:'false',part:'foot'}}", "{Name:'minecraft:bed',Properties:{facing:'north',occupied:'true',part:'foot'}}"); ++ register(419, "{Name:'minecraft:red_bed',Properties:{facing:'east',occupied:'false',part:'foot'}}", "{Name:'minecraft:bed',Properties:{facing:'east',occupied:'false',part:'foot'}}", "{Name:'minecraft:bed',Properties:{facing:'east',occupied:'true',part:'foot'}}"); ++ register(424, "{Name:'minecraft:red_bed',Properties:{facing:'south',occupied:'false',part:'head'}}", "{Name:'minecraft:bed',Properties:{facing:'south',occupied:'false',part:'head'}}"); ++ register(425, "{Name:'minecraft:red_bed',Properties:{facing:'west',occupied:'false',part:'head'}}", "{Name:'minecraft:bed',Properties:{facing:'west',occupied:'false',part:'head'}}"); ++ register(426, "{Name:'minecraft:red_bed',Properties:{facing:'north',occupied:'false',part:'head'}}", "{Name:'minecraft:bed',Properties:{facing:'north',occupied:'false',part:'head'}}"); ++ register(427, "{Name:'minecraft:red_bed',Properties:{facing:'east',occupied:'false',part:'head'}}", "{Name:'minecraft:bed',Properties:{facing:'east',occupied:'false',part:'head'}}"); ++ register(428, "{Name:'minecraft:red_bed',Properties:{facing:'south',occupied:'true',part:'head'}}", "{Name:'minecraft:bed',Properties:{facing:'south',occupied:'true',part:'head'}}"); ++ register(429, "{Name:'minecraft:red_bed',Properties:{facing:'west',occupied:'true',part:'head'}}", "{Name:'minecraft:bed',Properties:{facing:'west',occupied:'true',part:'head'}}"); ++ register(430, "{Name:'minecraft:red_bed',Properties:{facing:'north',occupied:'true',part:'head'}}", "{Name:'minecraft:bed',Properties:{facing:'north',occupied:'true',part:'head'}}"); ++ register(431, "{Name:'minecraft:red_bed',Properties:{facing:'east',occupied:'true',part:'head'}}", "{Name:'minecraft:bed',Properties:{facing:'east',occupied:'true',part:'head'}}"); ++ register(432, "{Name:'minecraft:powered_rail',Properties:{powered:'false',shape:'north_south'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'false',shape:'north_south'}}"); ++ register(433, "{Name:'minecraft:powered_rail',Properties:{powered:'false',shape:'east_west'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'false',shape:'east_west'}}"); ++ register(434, "{Name:'minecraft:powered_rail',Properties:{powered:'false',shape:'ascending_east'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'false',shape:'ascending_east'}}"); ++ register(435, "{Name:'minecraft:powered_rail',Properties:{powered:'false',shape:'ascending_west'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'false',shape:'ascending_west'}}"); ++ register(436, "{Name:'minecraft:powered_rail',Properties:{powered:'false',shape:'ascending_north'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'false',shape:'ascending_north'}}"); ++ register(437, "{Name:'minecraft:powered_rail',Properties:{powered:'false',shape:'ascending_south'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'false',shape:'ascending_south'}}"); ++ register(440, "{Name:'minecraft:powered_rail',Properties:{powered:'true',shape:'north_south'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'true',shape:'north_south'}}"); ++ register(441, "{Name:'minecraft:powered_rail',Properties:{powered:'true',shape:'east_west'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'true',shape:'east_west'}}"); ++ register(442, "{Name:'minecraft:powered_rail',Properties:{powered:'true',shape:'ascending_east'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'true',shape:'ascending_east'}}"); ++ register(443, "{Name:'minecraft:powered_rail',Properties:{powered:'true',shape:'ascending_west'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'true',shape:'ascending_west'}}"); ++ register(444, "{Name:'minecraft:powered_rail',Properties:{powered:'true',shape:'ascending_north'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'true',shape:'ascending_north'}}"); ++ register(445, "{Name:'minecraft:powered_rail',Properties:{powered:'true',shape:'ascending_south'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'true',shape:'ascending_south'}}"); ++ register(448, "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'north_south'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'north_south'}}"); ++ register(449, "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'east_west'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'east_west'}}"); ++ register(450, "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'ascending_east'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'ascending_east'}}"); ++ register(451, "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'ascending_west'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'ascending_west'}}"); ++ register(452, "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'ascending_north'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'ascending_north'}}"); ++ register(453, "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'ascending_south'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'ascending_south'}}"); ++ register(456, "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'north_south'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'north_south'}}"); ++ register(457, "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'east_west'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'east_west'}}"); ++ register(458, "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'ascending_east'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'ascending_east'}}"); ++ register(459, "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'ascending_west'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'ascending_west'}}"); ++ register(460, "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'ascending_north'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'ascending_north'}}"); ++ register(461, "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'ascending_south'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'ascending_south'}}"); ++ register(464, "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'down'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'down'}}"); ++ register(465, "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'up'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'up'}}"); ++ register(466, "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'north'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'north'}}"); ++ register(467, "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'south'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'south'}}"); ++ register(468, "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'west'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'west'}}"); ++ register(469, "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'east'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'east'}}"); ++ register(472, "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'down'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'down'}}"); ++ register(473, "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'up'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'up'}}"); ++ register(474, "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'north'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'north'}}"); ++ register(475, "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'south'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'south'}}"); ++ register(476, "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'west'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'west'}}"); ++ register(477, "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'east'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'east'}}"); ++ register(480, "{Name:'minecraft:cobweb'}", "{Name:'minecraft:web'}"); ++ register(496, "{Name:'minecraft:dead_bush'}", "{Name:'minecraft:tallgrass',Properties:{type:'dead_bush'}}"); ++ register(497, "{Name:'minecraft:grass'}", "{Name:'minecraft:tallgrass',Properties:{type:'tall_grass'}}"); ++ register(498, "{Name:'minecraft:fern'}", "{Name:'minecraft:tallgrass',Properties:{type:'fern'}}"); ++ register(512, "{Name:'minecraft:dead_bush'}", "{Name:'minecraft:deadbush'}"); ++ register(528, "{Name:'minecraft:piston',Properties:{extended:'false',facing:'down'}}", "{Name:'minecraft:piston',Properties:{extended:'false',facing:'down'}}"); ++ register(529, "{Name:'minecraft:piston',Properties:{extended:'false',facing:'up'}}", "{Name:'minecraft:piston',Properties:{extended:'false',facing:'up'}}"); ++ register(530, "{Name:'minecraft:piston',Properties:{extended:'false',facing:'north'}}", "{Name:'minecraft:piston',Properties:{extended:'false',facing:'north'}}"); ++ register(531, "{Name:'minecraft:piston',Properties:{extended:'false',facing:'south'}}", "{Name:'minecraft:piston',Properties:{extended:'false',facing:'south'}}"); ++ register(532, "{Name:'minecraft:piston',Properties:{extended:'false',facing:'west'}}", "{Name:'minecraft:piston',Properties:{extended:'false',facing:'west'}}"); ++ register(533, "{Name:'minecraft:piston',Properties:{extended:'false',facing:'east'}}", "{Name:'minecraft:piston',Properties:{extended:'false',facing:'east'}}"); ++ register(536, "{Name:'minecraft:piston',Properties:{extended:'true',facing:'down'}}", "{Name:'minecraft:piston',Properties:{extended:'true',facing:'down'}}"); ++ register(537, "{Name:'minecraft:piston',Properties:{extended:'true',facing:'up'}}", "{Name:'minecraft:piston',Properties:{extended:'true',facing:'up'}}"); ++ register(538, "{Name:'minecraft:piston',Properties:{extended:'true',facing:'north'}}", "{Name:'minecraft:piston',Properties:{extended:'true',facing:'north'}}"); ++ register(539, "{Name:'minecraft:piston',Properties:{extended:'true',facing:'south'}}", "{Name:'minecraft:piston',Properties:{extended:'true',facing:'south'}}"); ++ register(540, "{Name:'minecraft:piston',Properties:{extended:'true',facing:'west'}}", "{Name:'minecraft:piston',Properties:{extended:'true',facing:'west'}}"); ++ register(541, "{Name:'minecraft:piston',Properties:{extended:'true',facing:'east'}}", "{Name:'minecraft:piston',Properties:{extended:'true',facing:'east'}}"); ++ register(544, "{Name:'minecraft:piston_head',Properties:{facing:'down',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'down',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'down',short:'true',type:'normal'}}"); ++ register(545, "{Name:'minecraft:piston_head',Properties:{facing:'up',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'up',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'up',short:'true',type:'normal'}}"); ++ register(546, "{Name:'minecraft:piston_head',Properties:{facing:'north',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'north',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'north',short:'true',type:'normal'}}"); ++ register(547, "{Name:'minecraft:piston_head',Properties:{facing:'south',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'south',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'south',short:'true',type:'normal'}}"); ++ register(548, "{Name:'minecraft:piston_head',Properties:{facing:'west',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'west',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'west',short:'true',type:'normal'}}"); ++ register(549, "{Name:'minecraft:piston_head',Properties:{facing:'east',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'east',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'east',short:'true',type:'normal'}}"); ++ register(552, "{Name:'minecraft:piston_head',Properties:{facing:'down',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'down',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'down',short:'true',type:'sticky'}}"); ++ register(553, "{Name:'minecraft:piston_head',Properties:{facing:'up',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'up',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'up',short:'true',type:'sticky'}}"); ++ register(554, "{Name:'minecraft:piston_head',Properties:{facing:'north',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'north',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'north',short:'true',type:'sticky'}}"); ++ register(555, "{Name:'minecraft:piston_head',Properties:{facing:'south',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'south',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'south',short:'true',type:'sticky'}}"); ++ register(556, "{Name:'minecraft:piston_head',Properties:{facing:'west',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'west',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'west',short:'true',type:'sticky'}}"); ++ register(557, "{Name:'minecraft:piston_head',Properties:{facing:'east',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'east',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'east',short:'true',type:'sticky'}}"); ++ register(560, "{Name:'minecraft:white_wool'}", "{Name:'minecraft:wool',Properties:{color:'white'}}"); ++ register(561, "{Name:'minecraft:orange_wool'}", "{Name:'minecraft:wool',Properties:{color:'orange'}}"); ++ register(562, "{Name:'minecraft:magenta_wool'}", "{Name:'minecraft:wool',Properties:{color:'magenta'}}"); ++ register(563, "{Name:'minecraft:light_blue_wool'}", "{Name:'minecraft:wool',Properties:{color:'light_blue'}}"); ++ register(564, "{Name:'minecraft:yellow_wool'}", "{Name:'minecraft:wool',Properties:{color:'yellow'}}"); ++ register(565, "{Name:'minecraft:lime_wool'}", "{Name:'minecraft:wool',Properties:{color:'lime'}}"); ++ register(566, "{Name:'minecraft:pink_wool'}", "{Name:'minecraft:wool',Properties:{color:'pink'}}"); ++ register(567, "{Name:'minecraft:gray_wool'}", "{Name:'minecraft:wool',Properties:{color:'gray'}}"); ++ register(568, "{Name:'minecraft:light_gray_wool'}", "{Name:'minecraft:wool',Properties:{color:'silver'}}"); ++ register(569, "{Name:'minecraft:cyan_wool'}", "{Name:'minecraft:wool',Properties:{color:'cyan'}}"); ++ register(570, "{Name:'minecraft:purple_wool'}", "{Name:'minecraft:wool',Properties:{color:'purple'}}"); ++ register(571, "{Name:'minecraft:blue_wool'}", "{Name:'minecraft:wool',Properties:{color:'blue'}}"); ++ register(572, "{Name:'minecraft:brown_wool'}", "{Name:'minecraft:wool',Properties:{color:'brown'}}"); ++ register(573, "{Name:'minecraft:green_wool'}", "{Name:'minecraft:wool',Properties:{color:'green'}}"); ++ register(574, "{Name:'minecraft:red_wool'}", "{Name:'minecraft:wool',Properties:{color:'red'}}"); ++ register(575, "{Name:'minecraft:black_wool'}", "{Name:'minecraft:wool',Properties:{color:'black'}}"); ++ register(576, "{Name:'minecraft:moving_piston',Properties:{facing:'down',type:'normal'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'down',type:'normal'}}"); ++ register(577, "{Name:'minecraft:moving_piston',Properties:{facing:'up',type:'normal'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'up',type:'normal'}}"); ++ register(578, "{Name:'minecraft:moving_piston',Properties:{facing:'north',type:'normal'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'north',type:'normal'}}"); ++ register(579, "{Name:'minecraft:moving_piston',Properties:{facing:'south',type:'normal'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'south',type:'normal'}}"); ++ register(580, "{Name:'minecraft:moving_piston',Properties:{facing:'west',type:'normal'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'west',type:'normal'}}"); ++ register(581, "{Name:'minecraft:moving_piston',Properties:{facing:'east',type:'normal'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'east',type:'normal'}}"); ++ register(584, "{Name:'minecraft:moving_piston',Properties:{facing:'down',type:'sticky'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'down',type:'sticky'}}"); ++ register(585, "{Name:'minecraft:moving_piston',Properties:{facing:'up',type:'sticky'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'up',type:'sticky'}}"); ++ register(586, "{Name:'minecraft:moving_piston',Properties:{facing:'north',type:'sticky'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'north',type:'sticky'}}"); ++ register(587, "{Name:'minecraft:moving_piston',Properties:{facing:'south',type:'sticky'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'south',type:'sticky'}}"); ++ register(588, "{Name:'minecraft:moving_piston',Properties:{facing:'west',type:'sticky'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'west',type:'sticky'}}"); ++ register(589, "{Name:'minecraft:moving_piston',Properties:{facing:'east',type:'sticky'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'east',type:'sticky'}}"); ++ register(592, "{Name:'minecraft:dandelion'}", "{Name:'minecraft:yellow_flower',Properties:{type:'dandelion'}}"); ++ register(608, "{Name:'minecraft:poppy'}", "{Name:'minecraft:red_flower',Properties:{type:'poppy'}}"); ++ register(609, "{Name:'minecraft:blue_orchid'}", "{Name:'minecraft:red_flower',Properties:{type:'blue_orchid'}}"); ++ register(610, "{Name:'minecraft:allium'}", "{Name:'minecraft:red_flower',Properties:{type:'allium'}}"); ++ register(611, "{Name:'minecraft:azure_bluet'}", "{Name:'minecraft:red_flower',Properties:{type:'houstonia'}}"); ++ register(612, "{Name:'minecraft:red_tulip'}", "{Name:'minecraft:red_flower',Properties:{type:'red_tulip'}}"); ++ register(613, "{Name:'minecraft:orange_tulip'}", "{Name:'minecraft:red_flower',Properties:{type:'orange_tulip'}}"); ++ register(614, "{Name:'minecraft:white_tulip'}", "{Name:'minecraft:red_flower',Properties:{type:'white_tulip'}}"); ++ register(615, "{Name:'minecraft:pink_tulip'}", "{Name:'minecraft:red_flower',Properties:{type:'pink_tulip'}}"); ++ register(616, "{Name:'minecraft:oxeye_daisy'}", "{Name:'minecraft:red_flower',Properties:{type:'oxeye_daisy'}}"); ++ register(624, "{Name:'minecraft:brown_mushroom'}", "{Name:'minecraft:brown_mushroom'}"); ++ register(640, "{Name:'minecraft:red_mushroom'}", "{Name:'minecraft:red_mushroom'}"); ++ register(656, "{Name:'minecraft:gold_block'}", "{Name:'minecraft:gold_block'}"); ++ register(672, "{Name:'minecraft:iron_block'}", "{Name:'minecraft:iron_block'}"); ++ register(688, "{Name:'minecraft:stone_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'false',variant:'stone'}}"); ++ register(689, "{Name:'minecraft:sandstone_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'false',variant:'sandstone'}}"); ++ register(690, "{Name:'minecraft:petrified_oak_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'false',variant:'wood_old'}}"); ++ register(691, "{Name:'minecraft:cobblestone_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'false',variant:'cobblestone'}}"); ++ register(692, "{Name:'minecraft:brick_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'false',variant:'brick'}}"); ++ register(693, "{Name:'minecraft:stone_brick_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'false',variant:'stone_brick'}}"); ++ register(694, "{Name:'minecraft:nether_brick_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'false',variant:'nether_brick'}}"); ++ register(695, "{Name:'minecraft:quartz_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'false',variant:'quartz'}}"); ++ register(696, "{Name:'minecraft:smooth_stone'}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'true',variant:'stone'}}"); ++ register(697, "{Name:'minecraft:smooth_sandstone'}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'true',variant:'sandstone'}}"); ++ register(698, "{Name:'minecraft:petrified_oak_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'true',variant:'wood_old'}}"); ++ register(699, "{Name:'minecraft:cobblestone_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'true',variant:'cobblestone'}}"); ++ register(700, "{Name:'minecraft:brick_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'true',variant:'brick'}}"); ++ register(701, "{Name:'minecraft:stone_brick_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'true',variant:'stone_brick'}}"); ++ register(702, "{Name:'minecraft:nether_brick_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'true',variant:'nether_brick'}}"); ++ register(703, "{Name:'minecraft:smooth_quartz'}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'true',variant:'quartz'}}"); ++ register(704, "{Name:'minecraft:stone_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:stone_slab',Properties:{half:'bottom',variant:'stone'}}"); ++ register(705, "{Name:'minecraft:sandstone_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:stone_slab',Properties:{half:'bottom',variant:'sandstone'}}"); ++ register(706, "{Name:'minecraft:petrified_oak_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:stone_slab',Properties:{half:'bottom',variant:'wood_old'}}"); ++ register(707, "{Name:'minecraft:cobblestone_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:stone_slab',Properties:{half:'bottom',variant:'cobblestone'}}"); ++ register(708, "{Name:'minecraft:brick_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:stone_slab',Properties:{half:'bottom',variant:'brick'}}"); ++ register(709, "{Name:'minecraft:stone_brick_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:stone_slab',Properties:{half:'bottom',variant:'stone_brick'}}"); ++ register(710, "{Name:'minecraft:nether_brick_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:stone_slab',Properties:{half:'bottom',variant:'nether_brick'}}"); ++ register(711, "{Name:'minecraft:quartz_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:stone_slab',Properties:{half:'bottom',variant:'quartz'}}"); ++ register(712, "{Name:'minecraft:stone_slab',Properties:{type:'top'}}", "{Name:'minecraft:stone_slab',Properties:{half:'top',variant:'stone'}}"); ++ register(713, "{Name:'minecraft:sandstone_slab',Properties:{type:'top'}}", "{Name:'minecraft:stone_slab',Properties:{half:'top',variant:'sandstone'}}"); ++ register(714, "{Name:'minecraft:petrified_oak_slab',Properties:{type:'top'}}", "{Name:'minecraft:stone_slab',Properties:{half:'top',variant:'wood_old'}}"); ++ register(715, "{Name:'minecraft:cobblestone_slab',Properties:{type:'top'}}", "{Name:'minecraft:stone_slab',Properties:{half:'top',variant:'cobblestone'}}"); ++ register(716, "{Name:'minecraft:brick_slab',Properties:{type:'top'}}", "{Name:'minecraft:stone_slab',Properties:{half:'top',variant:'brick'}}"); ++ register(717, "{Name:'minecraft:stone_brick_slab',Properties:{type:'top'}}", "{Name:'minecraft:stone_slab',Properties:{half:'top',variant:'stone_brick'}}"); ++ register(718, "{Name:'minecraft:nether_brick_slab',Properties:{type:'top'}}", "{Name:'minecraft:stone_slab',Properties:{half:'top',variant:'nether_brick'}}"); ++ register(719, "{Name:'minecraft:quartz_slab',Properties:{type:'top'}}", "{Name:'minecraft:stone_slab',Properties:{half:'top',variant:'quartz'}}"); ++ register(720, "{Name:'minecraft:bricks'}", "{Name:'minecraft:brick_block'}"); ++ register(736, "{Name:'minecraft:tnt',Properties:{unstable:'false'}}", "{Name:'minecraft:tnt',Properties:{explode:'false'}}"); ++ register(737, "{Name:'minecraft:tnt',Properties:{unstable:'true'}}", "{Name:'minecraft:tnt',Properties:{explode:'true'}}"); ++ register(752, "{Name:'minecraft:bookshelf'}", "{Name:'minecraft:bookshelf'}"); ++ register(768, "{Name:'minecraft:mossy_cobblestone'}", "{Name:'minecraft:mossy_cobblestone'}"); ++ register(784, "{Name:'minecraft:obsidian'}", "{Name:'minecraft:obsidian'}"); ++ register(801, "{Name:'minecraft:wall_torch',Properties:{facing:'east'}}", "{Name:'minecraft:torch',Properties:{facing:'east'}}"); ++ register(802, "{Name:'minecraft:wall_torch',Properties:{facing:'west'}}", "{Name:'minecraft:torch',Properties:{facing:'west'}}"); ++ register(803, "{Name:'minecraft:wall_torch',Properties:{facing:'south'}}", "{Name:'minecraft:torch',Properties:{facing:'south'}}"); ++ register(804, "{Name:'minecraft:wall_torch',Properties:{facing:'north'}}", "{Name:'minecraft:torch',Properties:{facing:'north'}}"); ++ register(805, "{Name:'minecraft:torch'}", "{Name:'minecraft:torch',Properties:{facing:'up'}}"); ++ register(816, "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); ++ register(817, "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); ++ register(818, "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); ++ register(819, "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); ++ register(820, "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); ++ register(821, "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); ++ register(822, "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); ++ register(823, "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); ++ register(824, "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); ++ register(825, "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); ++ register(826, "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); ++ register(827, "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); ++ register(828, "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); ++ register(829, "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); ++ register(830, "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); ++ register(831, "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); ++ register(832, "{Name:'minecraft:mob_spawner'}", "{Name:'minecraft:mob_spawner'}"); ++ register(848, "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); ++ register(849, "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); ++ register(850, "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); ++ register(851, "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); ++ register(852, "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); ++ register(853, "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); ++ register(854, "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); ++ register(855, "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); ++ register(866, "{Name:'minecraft:chest',Properties:{facing:'north',type:'single'}}", "{Name:'minecraft:chest',Properties:{facing:'north'}}"); ++ register(867, "{Name:'minecraft:chest',Properties:{facing:'south',type:'single'}}", "{Name:'minecraft:chest',Properties:{facing:'south'}}"); ++ register(868, "{Name:'minecraft:chest',Properties:{facing:'west',type:'single'}}", "{Name:'minecraft:chest',Properties:{facing:'west'}}"); ++ register(869, "{Name:'minecraft:chest',Properties:{facing:'east',type:'single'}}", "{Name:'minecraft:chest',Properties:{facing:'east'}}"); ++ register(880, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'0',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'0',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'0',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'0',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'0',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'0',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'0',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'0',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'0',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'0',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'0',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'0',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'0',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'0',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'0',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'0',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'0',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'0',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'0',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'0',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'0',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'0',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'0',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'0',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'0',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'0',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'0',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'0',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'0',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'0',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'0',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'0',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'0',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'0',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'0',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'0',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'0',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'0',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'0',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'0',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'0',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'0',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'0',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'0',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'0',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'0',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'0',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'0',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'0',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'0',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'0',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'0',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'0',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'0',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'0',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'0',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'0',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'0',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'0',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'0',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'0',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'0',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'0',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'0',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'0',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'0',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'0',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'0',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'0',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'0',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'0',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'0',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'0',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'0',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'0',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'0',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'0',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'0',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'0',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'0',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'0',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'0',south:'up',west:'up'}}"); ++ register(881, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'1',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'1',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'1',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'1',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'1',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'1',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'1',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'1',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'1',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'1',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'1',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'1',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'1',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'1',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'1',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'1',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'1',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'1',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'1',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'1',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'1',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'1',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'1',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'1',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'1',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'1',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'1',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'1',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'1',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'1',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'1',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'1',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'1',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'1',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'1',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'1',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'1',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'1',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'1',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'1',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'1',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'1',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'1',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'1',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'1',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'1',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'1',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'1',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'1',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'1',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'1',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'1',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'1',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'1',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'1',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'1',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'1',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'1',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'1',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'1',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'1',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'1',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'1',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'1',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'1',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'1',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'1',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'1',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'1',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'1',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'1',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'1',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'1',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'1',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'1',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'1',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'1',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'1',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'1',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'1',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'1',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'1',south:'up',west:'up'}}"); ++ register(882, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'2',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'2',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'2',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'2',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'2',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'2',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'2',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'2',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'2',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'2',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'2',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'2',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'2',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'2',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'2',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'2',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'2',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'2',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'2',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'2',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'2',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'2',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'2',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'2',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'2',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'2',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'2',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'2',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'2',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'2',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'2',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'2',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'2',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'2',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'2',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'2',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'2',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'2',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'2',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'2',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'2',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'2',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'2',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'2',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'2',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'2',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'2',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'2',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'2',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'2',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'2',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'2',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'2',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'2',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'2',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'2',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'2',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'2',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'2',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'2',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'2',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'2',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'2',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'2',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'2',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'2',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'2',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'2',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'2',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'2',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'2',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'2',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'2',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'2',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'2',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'2',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'2',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'2',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'2',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'2',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'2',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'2',south:'up',west:'up'}}"); ++ register(883, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'3',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'3',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'3',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'3',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'3',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'3',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'3',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'3',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'3',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'3',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'3',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'3',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'3',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'3',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'3',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'3',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'3',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'3',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'3',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'3',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'3',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'3',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'3',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'3',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'3',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'3',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'3',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'3',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'3',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'3',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'3',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'3',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'3',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'3',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'3',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'3',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'3',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'3',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'3',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'3',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'3',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'3',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'3',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'3',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'3',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'3',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'3',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'3',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'3',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'3',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'3',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'3',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'3',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'3',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'3',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'3',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'3',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'3',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'3',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'3',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'3',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'3',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'3',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'3',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'3',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'3',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'3',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'3',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'3',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'3',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'3',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'3',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'3',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'3',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'3',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'3',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'3',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'3',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'3',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'3',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'3',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'3',south:'up',west:'up'}}"); ++ register(884, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'4',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'4',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'4',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'4',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'4',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'4',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'4',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'4',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'4',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'4',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'4',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'4',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'4',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'4',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'4',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'4',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'4',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'4',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'4',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'4',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'4',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'4',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'4',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'4',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'4',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'4',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'4',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'4',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'4',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'4',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'4',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'4',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'4',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'4',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'4',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'4',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'4',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'4',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'4',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'4',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'4',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'4',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'4',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'4',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'4',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'4',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'4',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'4',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'4',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'4',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'4',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'4',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'4',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'4',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'4',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'4',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'4',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'4',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'4',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'4',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'4',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'4',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'4',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'4',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'4',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'4',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'4',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'4',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'4',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'4',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'4',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'4',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'4',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'4',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'4',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'4',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'4',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'4',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'4',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'4',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'4',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'4',south:'up',west:'up'}}"); ++ register(885, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'5',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'5',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'5',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'5',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'5',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'5',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'5',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'5',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'5',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'5',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'5',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'5',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'5',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'5',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'5',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'5',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'5',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'5',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'5',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'5',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'5',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'5',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'5',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'5',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'5',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'5',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'5',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'5',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'5',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'5',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'5',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'5',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'5',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'5',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'5',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'5',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'5',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'5',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'5',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'5',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'5',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'5',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'5',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'5',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'5',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'5',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'5',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'5',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'5',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'5',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'5',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'5',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'5',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'5',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'5',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'5',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'5',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'5',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'5',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'5',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'5',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'5',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'5',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'5',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'5',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'5',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'5',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'5',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'5',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'5',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'5',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'5',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'5',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'5',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'5',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'5',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'5',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'5',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'5',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'5',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'5',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'5',south:'up',west:'up'}}"); ++ register(886, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'6',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'6',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'6',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'6',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'6',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'6',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'6',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'6',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'6',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'6',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'6',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'6',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'6',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'6',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'6',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'6',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'6',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'6',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'6',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'6',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'6',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'6',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'6',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'6',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'6',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'6',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'6',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'6',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'6',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'6',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'6',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'6',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'6',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'6',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'6',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'6',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'6',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'6',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'6',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'6',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'6',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'6',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'6',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'6',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'6',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'6',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'6',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'6',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'6',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'6',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'6',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'6',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'6',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'6',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'6',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'6',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'6',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'6',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'6',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'6',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'6',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'6',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'6',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'6',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'6',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'6',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'6',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'6',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'6',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'6',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'6',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'6',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'6',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'6',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'6',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'6',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'6',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'6',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'6',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'6',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'6',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'6',south:'up',west:'up'}}"); ++ register(887, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'7',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'7',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'7',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'7',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'7',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'7',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'7',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'7',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'7',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'7',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'7',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'7',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'7',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'7',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'7',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'7',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'7',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'7',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'7',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'7',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'7',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'7',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'7',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'7',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'7',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'7',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'7',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'7',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'7',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'7',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'7',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'7',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'7',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'7',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'7',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'7',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'7',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'7',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'7',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'7',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'7',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'7',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'7',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'7',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'7',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'7',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'7',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'7',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'7',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'7',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'7',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'7',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'7',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'7',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'7',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'7',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'7',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'7',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'7',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'7',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'7',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'7',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'7',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'7',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'7',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'7',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'7',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'7',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'7',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'7',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'7',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'7',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'7',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'7',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'7',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'7',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'7',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'7',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'7',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'7',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'7',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'7',south:'up',west:'up'}}"); ++ register(888, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'8',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'8',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'8',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'8',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'8',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'8',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'8',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'8',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'8',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'8',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'8',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'8',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'8',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'8',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'8',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'8',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'8',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'8',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'8',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'8',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'8',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'8',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'8',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'8',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'8',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'8',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'8',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'8',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'8',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'8',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'8',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'8',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'8',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'8',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'8',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'8',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'8',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'8',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'8',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'8',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'8',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'8',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'8',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'8',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'8',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'8',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'8',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'8',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'8',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'8',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'8',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'8',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'8',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'8',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'8',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'8',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'8',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'8',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'8',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'8',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'8',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'8',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'8',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'8',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'8',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'8',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'8',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'8',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'8',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'8',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'8',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'8',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'8',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'8',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'8',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'8',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'8',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'8',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'8',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'8',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'8',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'8',south:'up',west:'up'}}"); ++ register(889, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'9',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'9',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'9',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'9',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'9',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'9',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'9',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'9',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'9',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'9',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'9',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'9',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'9',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'9',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'9',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'9',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'9',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'9',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'9',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'9',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'9',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'9',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'9',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'9',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'9',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'9',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'9',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'9',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'9',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'9',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'9',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'9',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'9',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'9',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'9',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'9',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'9',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'9',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'9',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'9',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'9',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'9',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'9',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'9',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'9',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'9',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'9',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'9',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'9',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'9',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'9',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'9',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'9',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'9',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'9',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'9',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'9',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'9',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'9',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'9',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'9',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'9',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'9',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'9',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'9',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'9',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'9',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'9',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'9',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'9',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'9',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'9',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'9',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'9',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'9',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'9',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'9',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'9',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'9',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'9',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'9',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'9',south:'up',west:'up'}}"); ++ register(890, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'10',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'10',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'10',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'10',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'10',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'10',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'10',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'10',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'10',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'10',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'10',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'10',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'10',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'10',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'10',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'10',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'10',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'10',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'10',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'10',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'10',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'10',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'10',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'10',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'10',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'10',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'10',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'10',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'10',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'10',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'10',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'10',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'10',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'10',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'10',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'10',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'10',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'10',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'10',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'10',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'10',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'10',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'10',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'10',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'10',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'10',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'10',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'10',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'10',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'10',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'10',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'10',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'10',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'10',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'10',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'10',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'10',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'10',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'10',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'10',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'10',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'10',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'10',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'10',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'10',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'10',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'10',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'10',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'10',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'10',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'10',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'10',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'10',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'10',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'10',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'10',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'10',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'10',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'10',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'10',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'10',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'10',south:'up',west:'up'}}"); ++ register(891, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'11',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'11',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'11',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'11',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'11',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'11',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'11',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'11',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'11',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'11',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'11',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'11',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'11',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'11',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'11',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'11',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'11',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'11',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'11',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'11',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'11',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'11',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'11',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'11',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'11',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'11',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'11',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'11',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'11',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'11',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'11',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'11',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'11',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'11',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'11',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'11',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'11',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'11',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'11',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'11',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'11',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'11',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'11',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'11',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'11',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'11',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'11',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'11',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'11',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'11',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'11',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'11',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'11',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'11',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'11',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'11',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'11',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'11',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'11',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'11',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'11',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'11',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'11',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'11',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'11',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'11',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'11',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'11',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'11',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'11',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'11',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'11',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'11',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'11',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'11',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'11',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'11',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'11',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'11',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'11',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'11',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'11',south:'up',west:'up'}}"); ++ register(892, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'12',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'12',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'12',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'12',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'12',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'12',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'12',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'12',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'12',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'12',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'12',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'12',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'12',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'12',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'12',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'12',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'12',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'12',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'12',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'12',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'12',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'12',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'12',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'12',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'12',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'12',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'12',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'12',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'12',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'12',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'12',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'12',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'12',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'12',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'12',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'12',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'12',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'12',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'12',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'12',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'12',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'12',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'12',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'12',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'12',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'12',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'12',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'12',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'12',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'12',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'12',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'12',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'12',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'12',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'12',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'12',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'12',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'12',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'12',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'12',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'12',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'12',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'12',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'12',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'12',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'12',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'12',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'12',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'12',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'12',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'12',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'12',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'12',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'12',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'12',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'12',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'12',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'12',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'12',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'12',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'12',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'12',south:'up',west:'up'}}"); ++ register(893, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'13',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'13',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'13',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'13',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'13',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'13',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'13',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'13',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'13',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'13',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'13',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'13',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'13',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'13',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'13',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'13',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'13',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'13',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'13',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'13',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'13',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'13',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'13',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'13',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'13',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'13',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'13',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'13',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'13',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'13',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'13',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'13',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'13',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'13',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'13',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'13',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'13',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'13',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'13',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'13',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'13',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'13',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'13',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'13',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'13',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'13',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'13',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'13',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'13',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'13',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'13',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'13',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'13',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'13',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'13',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'13',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'13',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'13',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'13',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'13',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'13',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'13',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'13',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'13',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'13',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'13',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'13',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'13',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'13',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'13',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'13',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'13',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'13',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'13',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'13',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'13',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'13',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'13',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'13',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'13',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'13',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'13',south:'up',west:'up'}}"); ++ register(894, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'14',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'14',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'14',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'14',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'14',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'14',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'14',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'14',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'14',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'14',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'14',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'14',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'14',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'14',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'14',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'14',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'14',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'14',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'14',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'14',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'14',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'14',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'14',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'14',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'14',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'14',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'14',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'14',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'14',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'14',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'14',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'14',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'14',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'14',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'14',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'14',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'14',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'14',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'14',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'14',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'14',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'14',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'14',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'14',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'14',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'14',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'14',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'14',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'14',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'14',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'14',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'14',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'14',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'14',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'14',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'14',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'14',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'14',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'14',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'14',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'14',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'14',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'14',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'14',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'14',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'14',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'14',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'14',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'14',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'14',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'14',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'14',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'14',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'14',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'14',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'14',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'14',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'14',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'14',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'14',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'14',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'14',south:'up',west:'up'}}"); ++ register(895, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'15',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'15',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'15',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'15',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'15',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'15',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'15',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'15',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'15',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'15',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'15',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'15',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'15',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'15',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'15',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'15',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'15',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'15',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'15',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'15',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'15',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'15',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'15',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'15',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'15',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'15',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'15',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'15',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'15',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'15',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'15',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'15',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'15',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'15',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'15',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'15',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'15',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'15',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'15',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'15',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'15',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'15',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'15',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'15',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'15',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'15',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'15',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'15',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'15',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'15',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'15',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'15',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'15',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'15',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'15',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'15',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'15',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'15',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'15',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'15',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'15',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'15',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'15',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'15',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'15',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'15',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'15',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'15',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'15',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'15',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'15',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'15',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'15',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'15',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'15',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'15',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'15',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'15',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'15',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'15',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'15',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'15',south:'up',west:'up'}}"); ++ register(896, "{Name:'minecraft:diamond_ore'}", "{Name:'minecraft:diamond_ore'}"); ++ register(912, "{Name:'minecraft:diamond_block'}", "{Name:'minecraft:diamond_block'}"); ++ register(928, "{Name:'minecraft:crafting_table'}", "{Name:'minecraft:crafting_table'}"); ++ register(944, "{Name:'minecraft:wheat',Properties:{age:'0'}}", "{Name:'minecraft:wheat',Properties:{age:'0'}}"); ++ register(945, "{Name:'minecraft:wheat',Properties:{age:'1'}}", "{Name:'minecraft:wheat',Properties:{age:'1'}}"); ++ register(946, "{Name:'minecraft:wheat',Properties:{age:'2'}}", "{Name:'minecraft:wheat',Properties:{age:'2'}}"); ++ register(947, "{Name:'minecraft:wheat',Properties:{age:'3'}}", "{Name:'minecraft:wheat',Properties:{age:'3'}}"); ++ register(948, "{Name:'minecraft:wheat',Properties:{age:'4'}}", "{Name:'minecraft:wheat',Properties:{age:'4'}}"); ++ register(949, "{Name:'minecraft:wheat',Properties:{age:'5'}}", "{Name:'minecraft:wheat',Properties:{age:'5'}}"); ++ register(950, "{Name:'minecraft:wheat',Properties:{age:'6'}}", "{Name:'minecraft:wheat',Properties:{age:'6'}}"); ++ register(951, "{Name:'minecraft:wheat',Properties:{age:'7'}}", "{Name:'minecraft:wheat',Properties:{age:'7'}}"); ++ register(960, "{Name:'minecraft:farmland',Properties:{moisture:'0'}}", "{Name:'minecraft:farmland',Properties:{moisture:'0'}}"); ++ register(961, "{Name:'minecraft:farmland',Properties:{moisture:'1'}}", "{Name:'minecraft:farmland',Properties:{moisture:'1'}}"); ++ register(962, "{Name:'minecraft:farmland',Properties:{moisture:'2'}}", "{Name:'minecraft:farmland',Properties:{moisture:'2'}}"); ++ register(963, "{Name:'minecraft:farmland',Properties:{moisture:'3'}}", "{Name:'minecraft:farmland',Properties:{moisture:'3'}}"); ++ register(964, "{Name:'minecraft:farmland',Properties:{moisture:'4'}}", "{Name:'minecraft:farmland',Properties:{moisture:'4'}}"); ++ register(965, "{Name:'minecraft:farmland',Properties:{moisture:'5'}}", "{Name:'minecraft:farmland',Properties:{moisture:'5'}}"); ++ register(966, "{Name:'minecraft:farmland',Properties:{moisture:'6'}}", "{Name:'minecraft:farmland',Properties:{moisture:'6'}}"); ++ register(967, "{Name:'minecraft:farmland',Properties:{moisture:'7'}}", "{Name:'minecraft:farmland',Properties:{moisture:'7'}}"); ++ register(978, "{Name:'minecraft:furnace',Properties:{facing:'north',lit:'false'}}", "{Name:'minecraft:furnace',Properties:{facing:'north'}}"); ++ register(979, "{Name:'minecraft:furnace',Properties:{facing:'south',lit:'false'}}", "{Name:'minecraft:furnace',Properties:{facing:'south'}}"); ++ register(980, "{Name:'minecraft:furnace',Properties:{facing:'west',lit:'false'}}", "{Name:'minecraft:furnace',Properties:{facing:'west'}}"); ++ register(981, "{Name:'minecraft:furnace',Properties:{facing:'east',lit:'false'}}", "{Name:'minecraft:furnace',Properties:{facing:'east'}}"); ++ register(994, "{Name:'minecraft:furnace',Properties:{facing:'north',lit:'true'}}", "{Name:'minecraft:lit_furnace',Properties:{facing:'north'}}"); ++ register(995, "{Name:'minecraft:furnace',Properties:{facing:'south',lit:'true'}}", "{Name:'minecraft:lit_furnace',Properties:{facing:'south'}}"); ++ register(996, "{Name:'minecraft:furnace',Properties:{facing:'west',lit:'true'}}", "{Name:'minecraft:lit_furnace',Properties:{facing:'west'}}"); ++ register(997, "{Name:'minecraft:furnace',Properties:{facing:'east',lit:'true'}}", "{Name:'minecraft:lit_furnace',Properties:{facing:'east'}}"); ++ register(1008, "{Name:'minecraft:sign',Properties:{rotation:'0'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'0'}}"); ++ register(1009, "{Name:'minecraft:sign',Properties:{rotation:'1'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'1'}}"); ++ register(1010, "{Name:'minecraft:sign',Properties:{rotation:'2'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'2'}}"); ++ register(1011, "{Name:'minecraft:sign',Properties:{rotation:'3'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'3'}}"); ++ register(1012, "{Name:'minecraft:sign',Properties:{rotation:'4'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'4'}}"); ++ register(1013, "{Name:'minecraft:sign',Properties:{rotation:'5'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'5'}}"); ++ register(1014, "{Name:'minecraft:sign',Properties:{rotation:'6'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'6'}}"); ++ register(1015, "{Name:'minecraft:sign',Properties:{rotation:'7'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'7'}}"); ++ register(1016, "{Name:'minecraft:sign',Properties:{rotation:'8'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'8'}}"); ++ register(1017, "{Name:'minecraft:sign',Properties:{rotation:'9'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'9'}}"); ++ register(1018, "{Name:'minecraft:sign',Properties:{rotation:'10'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'10'}}"); ++ register(1019, "{Name:'minecraft:sign',Properties:{rotation:'11'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'11'}}"); ++ register(1020, "{Name:'minecraft:sign',Properties:{rotation:'12'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'12'}}"); ++ register(1021, "{Name:'minecraft:sign',Properties:{rotation:'13'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'13'}}"); ++ register(1022, "{Name:'minecraft:sign',Properties:{rotation:'14'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'14'}}"); ++ register(1023, "{Name:'minecraft:sign',Properties:{rotation:'15'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'15'}}"); ++ register(1024, "{Name:'minecraft:oak_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(1025, "{Name:'minecraft:oak_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(1026, "{Name:'minecraft:oak_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(1027, "{Name:'minecraft:oak_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(1028, "{Name:'minecraft:oak_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(1029, "{Name:'minecraft:oak_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(1030, "{Name:'minecraft:oak_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(1031, "{Name:'minecraft:oak_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(1032, "{Name:'minecraft:oak_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'false'}}"); ++ register(1033, "{Name:'minecraft:oak_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'false'}}"); ++ register(1034, "{Name:'minecraft:oak_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'true'}}"); ++ register(1035, "{Name:'minecraft:oak_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'true'}}"); ++ register(1036, "{Name:'minecraft:oak_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'false'}}"); ++ register(1037, "{Name:'minecraft:oak_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'false'}}"); ++ register(1038, "{Name:'minecraft:oak_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'false'}}"); ++ register(1039, "{Name:'minecraft:oak_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'false'}}"); ++ register(1042, "{Name:'minecraft:ladder',Properties:{facing:'north'}}", "{Name:'minecraft:ladder',Properties:{facing:'north'}}"); ++ register(1043, "{Name:'minecraft:ladder',Properties:{facing:'south'}}", "{Name:'minecraft:ladder',Properties:{facing:'south'}}"); ++ register(1044, "{Name:'minecraft:ladder',Properties:{facing:'west'}}", "{Name:'minecraft:ladder',Properties:{facing:'west'}}"); ++ register(1045, "{Name:'minecraft:ladder',Properties:{facing:'east'}}", "{Name:'minecraft:ladder',Properties:{facing:'east'}}"); ++ register(1056, "{Name:'minecraft:rail',Properties:{shape:'north_south'}}", "{Name:'minecraft:rail',Properties:{shape:'north_south'}}"); ++ register(1057, "{Name:'minecraft:rail',Properties:{shape:'east_west'}}", "{Name:'minecraft:rail',Properties:{shape:'east_west'}}"); ++ register(1058, "{Name:'minecraft:rail',Properties:{shape:'ascending_east'}}", "{Name:'minecraft:rail',Properties:{shape:'ascending_east'}}"); ++ register(1059, "{Name:'minecraft:rail',Properties:{shape:'ascending_west'}}", "{Name:'minecraft:rail',Properties:{shape:'ascending_west'}}"); ++ register(1060, "{Name:'minecraft:rail',Properties:{shape:'ascending_north'}}", "{Name:'minecraft:rail',Properties:{shape:'ascending_north'}}"); ++ register(1061, "{Name:'minecraft:rail',Properties:{shape:'ascending_south'}}", "{Name:'minecraft:rail',Properties:{shape:'ascending_south'}}"); ++ register(1062, "{Name:'minecraft:rail',Properties:{shape:'south_east'}}", "{Name:'minecraft:rail',Properties:{shape:'south_east'}}"); ++ register(1063, "{Name:'minecraft:rail',Properties:{shape:'south_west'}}", "{Name:'minecraft:rail',Properties:{shape:'south_west'}}"); ++ register(1064, "{Name:'minecraft:rail',Properties:{shape:'north_west'}}", "{Name:'minecraft:rail',Properties:{shape:'north_west'}}"); ++ register(1065, "{Name:'minecraft:rail',Properties:{shape:'north_east'}}", "{Name:'minecraft:rail',Properties:{shape:'north_east'}}"); ++ register(1072, "{Name:'minecraft:cobblestone_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); ++ register(1073, "{Name:'minecraft:cobblestone_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); ++ register(1074, "{Name:'minecraft:cobblestone_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); ++ register(1075, "{Name:'minecraft:cobblestone_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); ++ register(1076, "{Name:'minecraft:cobblestone_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); ++ register(1077, "{Name:'minecraft:cobblestone_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); ++ register(1078, "{Name:'minecraft:cobblestone_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); ++ register(1079, "{Name:'minecraft:cobblestone_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); ++ register(1090, "{Name:'minecraft:wall_sign',Properties:{facing:'north'}}", "{Name:'minecraft:wall_sign',Properties:{facing:'north'}}"); ++ register(1091, "{Name:'minecraft:wall_sign',Properties:{facing:'south'}}", "{Name:'minecraft:wall_sign',Properties:{facing:'south'}}"); ++ register(1092, "{Name:'minecraft:wall_sign',Properties:{facing:'west'}}", "{Name:'minecraft:wall_sign',Properties:{facing:'west'}}"); ++ register(1093, "{Name:'minecraft:wall_sign',Properties:{facing:'east'}}", "{Name:'minecraft:wall_sign',Properties:{facing:'east'}}"); ++ register(1104, "{Name:'minecraft:lever',Properties:{face:'ceiling',facing:'west',powered:'false'}}", "{Name:'minecraft:lever',Properties:{facing:'down_x',powered:'false'}}"); ++ register(1105, "{Name:'minecraft:lever',Properties:{face:'wall',facing:'east',powered:'false'}}", "{Name:'minecraft:lever',Properties:{facing:'east',powered:'false'}}"); ++ register(1106, "{Name:'minecraft:lever',Properties:{face:'wall',facing:'west',powered:'false'}}", "{Name:'minecraft:lever',Properties:{facing:'west',powered:'false'}}"); ++ register(1107, "{Name:'minecraft:lever',Properties:{face:'wall',facing:'south',powered:'false'}}", "{Name:'minecraft:lever',Properties:{facing:'south',powered:'false'}}"); ++ register(1108, "{Name:'minecraft:lever',Properties:{face:'wall',facing:'north',powered:'false'}}", "{Name:'minecraft:lever',Properties:{facing:'north',powered:'false'}}"); ++ register(1109, "{Name:'minecraft:lever',Properties:{face:'floor',facing:'north',powered:'false'}}", "{Name:'minecraft:lever',Properties:{facing:'up_z',powered:'false'}}"); ++ register(1110, "{Name:'minecraft:lever',Properties:{face:'floor',facing:'west',powered:'false'}}", "{Name:'minecraft:lever',Properties:{facing:'up_x',powered:'false'}}"); ++ register(1111, "{Name:'minecraft:lever',Properties:{face:'ceiling',facing:'north',powered:'false'}}", "{Name:'minecraft:lever',Properties:{facing:'down_z',powered:'false'}}"); ++ register(1112, "{Name:'minecraft:lever',Properties:{face:'ceiling',facing:'west',powered:'true'}}", "{Name:'minecraft:lever',Properties:{facing:'down_x',powered:'true'}}"); ++ register(1113, "{Name:'minecraft:lever',Properties:{face:'wall',facing:'east',powered:'true'}}", "{Name:'minecraft:lever',Properties:{facing:'east',powered:'true'}}"); ++ register(1114, "{Name:'minecraft:lever',Properties:{face:'wall',facing:'west',powered:'true'}}", "{Name:'minecraft:lever',Properties:{facing:'west',powered:'true'}}"); ++ register(1115, "{Name:'minecraft:lever',Properties:{face:'wall',facing:'south',powered:'true'}}", "{Name:'minecraft:lever',Properties:{facing:'south',powered:'true'}}"); ++ register(1116, "{Name:'minecraft:lever',Properties:{face:'wall',facing:'north',powered:'true'}}", "{Name:'minecraft:lever',Properties:{facing:'north',powered:'true'}}"); ++ register(1117, "{Name:'minecraft:lever',Properties:{face:'floor',facing:'north',powered:'true'}}", "{Name:'minecraft:lever',Properties:{facing:'up_z',powered:'true'}}"); ++ register(1118, "{Name:'minecraft:lever',Properties:{face:'floor',facing:'west',powered:'true'}}", "{Name:'minecraft:lever',Properties:{facing:'up_x',powered:'true'}}"); ++ register(1119, "{Name:'minecraft:lever',Properties:{face:'ceiling',facing:'north',powered:'true'}}", "{Name:'minecraft:lever',Properties:{facing:'down_z',powered:'true'}}"); ++ register(1120, "{Name:'minecraft:stone_pressure_plate',Properties:{powered:'false'}}", "{Name:'minecraft:stone_pressure_plate',Properties:{powered:'false'}}"); ++ register(1121, "{Name:'minecraft:stone_pressure_plate',Properties:{powered:'true'}}", "{Name:'minecraft:stone_pressure_plate',Properties:{powered:'true'}}"); ++ register(1136, "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(1137, "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(1138, "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(1139, "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(1140, "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(1141, "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(1142, "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(1143, "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(1144, "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'false'}}"); ++ register(1145, "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'false'}}"); ++ register(1146, "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'true'}}"); ++ register(1147, "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'true'}}"); ++ register(1148, "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'false'}}"); ++ register(1149, "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'false'}}"); ++ register(1150, "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'false'}}"); ++ register(1151, "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'false'}}"); ++ register(1152, "{Name:'minecraft:oak_pressure_plate',Properties:{powered:'false'}}", "{Name:'minecraft:wooden_pressure_plate',Properties:{powered:'false'}}"); ++ register(1153, "{Name:'minecraft:oak_pressure_plate',Properties:{powered:'true'}}", "{Name:'minecraft:wooden_pressure_plate',Properties:{powered:'true'}}"); ++ register(1168, "{Name:'minecraft:redstone_ore',Properties:{lit:'false'}}", "{Name:'minecraft:redstone_ore'}"); ++ register(1184, "{Name:'minecraft:redstone_ore',Properties:{lit:'true'}}", "{Name:'minecraft:lit_redstone_ore'}"); ++ register(1201, "{Name:'minecraft:redstone_wall_torch',Properties:{facing:'east',lit:'false'}}", "{Name:'minecraft:unlit_redstone_torch',Properties:{facing:'east'}}"); ++ register(1202, "{Name:'minecraft:redstone_wall_torch',Properties:{facing:'west',lit:'false'}}", "{Name:'minecraft:unlit_redstone_torch',Properties:{facing:'west'}}"); ++ register(1203, "{Name:'minecraft:redstone_wall_torch',Properties:{facing:'south',lit:'false'}}", "{Name:'minecraft:unlit_redstone_torch',Properties:{facing:'south'}}"); ++ register(1204, "{Name:'minecraft:redstone_wall_torch',Properties:{facing:'north',lit:'false'}}", "{Name:'minecraft:unlit_redstone_torch',Properties:{facing:'north'}}"); ++ register(1205, "{Name:'minecraft:redstone_torch',Properties:{lit:'false'}}", "{Name:'minecraft:unlit_redstone_torch',Properties:{facing:'up'}}"); ++ register(1217, "{Name:'minecraft:redstone_wall_torch',Properties:{facing:'east',lit:'true'}}", "{Name:'minecraft:redstone_torch',Properties:{facing:'east'}}"); ++ register(1218, "{Name:'minecraft:redstone_wall_torch',Properties:{facing:'west',lit:'true'}}", "{Name:'minecraft:redstone_torch',Properties:{facing:'west'}}"); ++ register(1219, "{Name:'minecraft:redstone_wall_torch',Properties:{facing:'south',lit:'true'}}", "{Name:'minecraft:redstone_torch',Properties:{facing:'south'}}"); ++ register(1220, "{Name:'minecraft:redstone_wall_torch',Properties:{facing:'north',lit:'true'}}", "{Name:'minecraft:redstone_torch',Properties:{facing:'north'}}"); ++ register(1221, "{Name:'minecraft:redstone_torch',Properties:{lit:'true'}}", "{Name:'minecraft:redstone_torch',Properties:{facing:'up'}}"); ++ register(1232, "{Name:'minecraft:stone_button',Properties:{face:'ceiling',facing:'north',powered:'false'}}", "{Name:'minecraft:stone_button',Properties:{facing:'down',powered:'false'}}"); ++ register(1233, "{Name:'minecraft:stone_button',Properties:{face:'wall',facing:'east',powered:'false'}}", "{Name:'minecraft:stone_button',Properties:{facing:'east',powered:'false'}}"); ++ register(1234, "{Name:'minecraft:stone_button',Properties:{face:'wall',facing:'west',powered:'false'}}", "{Name:'minecraft:stone_button',Properties:{facing:'west',powered:'false'}}"); ++ register(1235, "{Name:'minecraft:stone_button',Properties:{face:'wall',facing:'south',powered:'false'}}", "{Name:'minecraft:stone_button',Properties:{facing:'south',powered:'false'}}"); ++ register(1236, "{Name:'minecraft:stone_button',Properties:{face:'wall',facing:'north',powered:'false'}}", "{Name:'minecraft:stone_button',Properties:{facing:'north',powered:'false'}}"); ++ register(1237, "{Name:'minecraft:stone_button',Properties:{face:'floor',facing:'north',powered:'false'}}", "{Name:'minecraft:stone_button',Properties:{facing:'up',powered:'false'}}"); ++ register(1240, "{Name:'minecraft:stone_button',Properties:{face:'ceiling',facing:'north',powered:'true'}}", "{Name:'minecraft:stone_button',Properties:{facing:'down',powered:'true'}}"); ++ register(1241, "{Name:'minecraft:stone_button',Properties:{face:'wall',facing:'east',powered:'true'}}", "{Name:'minecraft:stone_button',Properties:{facing:'east',powered:'true'}}"); ++ register(1242, "{Name:'minecraft:stone_button',Properties:{face:'wall',facing:'west',powered:'true'}}", "{Name:'minecraft:stone_button',Properties:{facing:'west',powered:'true'}}"); ++ register(1243, "{Name:'minecraft:stone_button',Properties:{face:'wall',facing:'south',powered:'true'}}", "{Name:'minecraft:stone_button',Properties:{facing:'south',powered:'true'}}"); ++ register(1244, "{Name:'minecraft:stone_button',Properties:{face:'wall',facing:'north',powered:'true'}}", "{Name:'minecraft:stone_button',Properties:{facing:'north',powered:'true'}}"); ++ register(1245, "{Name:'minecraft:stone_button',Properties:{face:'floor',facing:'north',powered:'true'}}", "{Name:'minecraft:stone_button',Properties:{facing:'up',powered:'true'}}"); ++ register(1248, "{Name:'minecraft:snow',Properties:{layers:'1'}}", "{Name:'minecraft:snow_layer',Properties:{layers:'1'}}"); ++ register(1249, "{Name:'minecraft:snow',Properties:{layers:'2'}}", "{Name:'minecraft:snow_layer',Properties:{layers:'2'}}"); ++ register(1250, "{Name:'minecraft:snow',Properties:{layers:'3'}}", "{Name:'minecraft:snow_layer',Properties:{layers:'3'}}"); ++ register(1251, "{Name:'minecraft:snow',Properties:{layers:'4'}}", "{Name:'minecraft:snow_layer',Properties:{layers:'4'}}"); ++ register(1252, "{Name:'minecraft:snow',Properties:{layers:'5'}}", "{Name:'minecraft:snow_layer',Properties:{layers:'5'}}"); ++ register(1253, "{Name:'minecraft:snow',Properties:{layers:'6'}}", "{Name:'minecraft:snow_layer',Properties:{layers:'6'}}"); ++ register(1254, "{Name:'minecraft:snow',Properties:{layers:'7'}}", "{Name:'minecraft:snow_layer',Properties:{layers:'7'}}"); ++ register(1255, "{Name:'minecraft:snow',Properties:{layers:'8'}}", "{Name:'minecraft:snow_layer',Properties:{layers:'8'}}"); ++ register(1264, "{Name:'minecraft:ice'}", "{Name:'minecraft:ice'}"); ++ register(1280, "{Name:'minecraft:snow_block'}", "{Name:'minecraft:snow'}"); ++ register(1296, "{Name:'minecraft:cactus',Properties:{age:'0'}}", "{Name:'minecraft:cactus',Properties:{age:'0'}}"); ++ register(1297, "{Name:'minecraft:cactus',Properties:{age:'1'}}", "{Name:'minecraft:cactus',Properties:{age:'1'}}"); ++ register(1298, "{Name:'minecraft:cactus',Properties:{age:'2'}}", "{Name:'minecraft:cactus',Properties:{age:'2'}}"); ++ register(1299, "{Name:'minecraft:cactus',Properties:{age:'3'}}", "{Name:'minecraft:cactus',Properties:{age:'3'}}"); ++ register(1300, "{Name:'minecraft:cactus',Properties:{age:'4'}}", "{Name:'minecraft:cactus',Properties:{age:'4'}}"); ++ register(1301, "{Name:'minecraft:cactus',Properties:{age:'5'}}", "{Name:'minecraft:cactus',Properties:{age:'5'}}"); ++ register(1302, "{Name:'minecraft:cactus',Properties:{age:'6'}}", "{Name:'minecraft:cactus',Properties:{age:'6'}}"); ++ register(1303, "{Name:'minecraft:cactus',Properties:{age:'7'}}", "{Name:'minecraft:cactus',Properties:{age:'7'}}"); ++ register(1304, "{Name:'minecraft:cactus',Properties:{age:'8'}}", "{Name:'minecraft:cactus',Properties:{age:'8'}}"); ++ register(1305, "{Name:'minecraft:cactus',Properties:{age:'9'}}", "{Name:'minecraft:cactus',Properties:{age:'9'}}"); ++ register(1306, "{Name:'minecraft:cactus',Properties:{age:'10'}}", "{Name:'minecraft:cactus',Properties:{age:'10'}}"); ++ register(1307, "{Name:'minecraft:cactus',Properties:{age:'11'}}", "{Name:'minecraft:cactus',Properties:{age:'11'}}"); ++ register(1308, "{Name:'minecraft:cactus',Properties:{age:'12'}}", "{Name:'minecraft:cactus',Properties:{age:'12'}}"); ++ register(1309, "{Name:'minecraft:cactus',Properties:{age:'13'}}", "{Name:'minecraft:cactus',Properties:{age:'13'}}"); ++ register(1310, "{Name:'minecraft:cactus',Properties:{age:'14'}}", "{Name:'minecraft:cactus',Properties:{age:'14'}}"); ++ register(1311, "{Name:'minecraft:cactus',Properties:{age:'15'}}", "{Name:'minecraft:cactus',Properties:{age:'15'}}"); ++ register(1312, "{Name:'minecraft:clay'}", "{Name:'minecraft:clay'}"); ++ register(1328, "{Name:'minecraft:sugar_cane',Properties:{age:'0'}}", "{Name:'minecraft:reeds',Properties:{age:'0'}}"); ++ register(1329, "{Name:'minecraft:sugar_cane',Properties:{age:'1'}}", "{Name:'minecraft:reeds',Properties:{age:'1'}}"); ++ register(1330, "{Name:'minecraft:sugar_cane',Properties:{age:'2'}}", "{Name:'minecraft:reeds',Properties:{age:'2'}}"); ++ register(1331, "{Name:'minecraft:sugar_cane',Properties:{age:'3'}}", "{Name:'minecraft:reeds',Properties:{age:'3'}}"); ++ register(1332, "{Name:'minecraft:sugar_cane',Properties:{age:'4'}}", "{Name:'minecraft:reeds',Properties:{age:'4'}}"); ++ register(1333, "{Name:'minecraft:sugar_cane',Properties:{age:'5'}}", "{Name:'minecraft:reeds',Properties:{age:'5'}}"); ++ register(1334, "{Name:'minecraft:sugar_cane',Properties:{age:'6'}}", "{Name:'minecraft:reeds',Properties:{age:'6'}}"); ++ register(1335, "{Name:'minecraft:sugar_cane',Properties:{age:'7'}}", "{Name:'minecraft:reeds',Properties:{age:'7'}}"); ++ register(1336, "{Name:'minecraft:sugar_cane',Properties:{age:'8'}}", "{Name:'minecraft:reeds',Properties:{age:'8'}}"); ++ register(1337, "{Name:'minecraft:sugar_cane',Properties:{age:'9'}}", "{Name:'minecraft:reeds',Properties:{age:'9'}}"); ++ register(1338, "{Name:'minecraft:sugar_cane',Properties:{age:'10'}}", "{Name:'minecraft:reeds',Properties:{age:'10'}}"); ++ register(1339, "{Name:'minecraft:sugar_cane',Properties:{age:'11'}}", "{Name:'minecraft:reeds',Properties:{age:'11'}}"); ++ register(1340, "{Name:'minecraft:sugar_cane',Properties:{age:'12'}}", "{Name:'minecraft:reeds',Properties:{age:'12'}}"); ++ register(1341, "{Name:'minecraft:sugar_cane',Properties:{age:'13'}}", "{Name:'minecraft:reeds',Properties:{age:'13'}}"); ++ register(1342, "{Name:'minecraft:sugar_cane',Properties:{age:'14'}}", "{Name:'minecraft:reeds',Properties:{age:'14'}}"); ++ register(1343, "{Name:'minecraft:sugar_cane',Properties:{age:'15'}}", "{Name:'minecraft:reeds',Properties:{age:'15'}}"); ++ register(1344, "{Name:'minecraft:jukebox',Properties:{has_record:'false'}}", "{Name:'minecraft:jukebox',Properties:{has_record:'false'}}"); ++ register(1345, "{Name:'minecraft:jukebox',Properties:{has_record:'true'}}", "{Name:'minecraft:jukebox',Properties:{has_record:'true'}}"); ++ register(1360, "{Name:'minecraft:oak_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:fence',Properties:{east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:fence',Properties:{east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:fence',Properties:{east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:fence',Properties:{east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:fence',Properties:{east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:fence',Properties:{east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:fence',Properties:{east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:fence',Properties:{east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:fence',Properties:{east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:fence',Properties:{east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:fence',Properties:{east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:fence',Properties:{east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:fence',Properties:{east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:fence',Properties:{east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:fence',Properties:{east:'true',north:'true',south:'true',west:'true'}}"); ++ register(1376, "{Name:'minecraft:carved_pumpkin',Properties:{facing:'south'}}", "{Name:'minecraft:pumpkin',Properties:{facing:'south'}}"); ++ register(1377, "{Name:'minecraft:carved_pumpkin',Properties:{facing:'west'}}", "{Name:'minecraft:pumpkin',Properties:{facing:'west'}}"); ++ register(1378, "{Name:'minecraft:carved_pumpkin',Properties:{facing:'north'}}", "{Name:'minecraft:pumpkin',Properties:{facing:'north'}}"); ++ register(1379, "{Name:'minecraft:carved_pumpkin',Properties:{facing:'east'}}", "{Name:'minecraft:pumpkin',Properties:{facing:'east'}}"); ++ register(1392, "{Name:'minecraft:netherrack'}", "{Name:'minecraft:netherrack'}"); ++ register(1408, "{Name:'minecraft:soul_sand'}", "{Name:'minecraft:soul_sand'}"); ++ register(1424, "{Name:'minecraft:glowstone'}", "{Name:'minecraft:glowstone'}"); ++ register(1441, "{Name:'minecraft:portal',Properties:{axis:'x'}}", "{Name:'minecraft:portal',Properties:{axis:'x'}}"); ++ register(1442, "{Name:'minecraft:portal',Properties:{axis:'z'}}", "{Name:'minecraft:portal',Properties:{axis:'z'}}"); ++ register(1456, "{Name:'minecraft:jack_o_lantern',Properties:{facing:'south'}}", "{Name:'minecraft:lit_pumpkin',Properties:{facing:'south'}}"); ++ register(1457, "{Name:'minecraft:jack_o_lantern',Properties:{facing:'west'}}", "{Name:'minecraft:lit_pumpkin',Properties:{facing:'west'}}"); ++ register(1458, "{Name:'minecraft:jack_o_lantern',Properties:{facing:'north'}}", "{Name:'minecraft:lit_pumpkin',Properties:{facing:'north'}}"); ++ register(1459, "{Name:'minecraft:jack_o_lantern',Properties:{facing:'east'}}", "{Name:'minecraft:lit_pumpkin',Properties:{facing:'east'}}"); ++ register(1472, "{Name:'minecraft:cake',Properties:{bites:'0'}}", "{Name:'minecraft:cake',Properties:{bites:'0'}}"); ++ register(1473, "{Name:'minecraft:cake',Properties:{bites:'1'}}", "{Name:'minecraft:cake',Properties:{bites:'1'}}"); ++ register(1474, "{Name:'minecraft:cake',Properties:{bites:'2'}}", "{Name:'minecraft:cake',Properties:{bites:'2'}}"); ++ register(1475, "{Name:'minecraft:cake',Properties:{bites:'3'}}", "{Name:'minecraft:cake',Properties:{bites:'3'}}"); ++ register(1476, "{Name:'minecraft:cake',Properties:{bites:'4'}}", "{Name:'minecraft:cake',Properties:{bites:'4'}}"); ++ register(1477, "{Name:'minecraft:cake',Properties:{bites:'5'}}", "{Name:'minecraft:cake',Properties:{bites:'5'}}"); ++ register(1478, "{Name:'minecraft:cake',Properties:{bites:'6'}}", "{Name:'minecraft:cake',Properties:{bites:'6'}}"); ++ register(1488, "{Name:'minecraft:repeater',Properties:{delay:'1',facing:'south',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'1',facing:'south',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'1',facing:'south',locked:'true'}}"); ++ register(1489, "{Name:'minecraft:repeater',Properties:{delay:'1',facing:'west',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'1',facing:'west',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'1',facing:'west',locked:'true'}}"); ++ register(1490, "{Name:'minecraft:repeater',Properties:{delay:'1',facing:'north',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'1',facing:'north',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'1',facing:'north',locked:'true'}}"); ++ register(1491, "{Name:'minecraft:repeater',Properties:{delay:'1',facing:'east',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'1',facing:'east',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'1',facing:'east',locked:'true'}}"); ++ register(1492, "{Name:'minecraft:repeater',Properties:{delay:'2',facing:'south',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'2',facing:'south',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'2',facing:'south',locked:'true'}}"); ++ register(1493, "{Name:'minecraft:repeater',Properties:{delay:'2',facing:'west',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'2',facing:'west',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'2',facing:'west',locked:'true'}}"); ++ register(1494, "{Name:'minecraft:repeater',Properties:{delay:'2',facing:'north',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'2',facing:'north',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'2',facing:'north',locked:'true'}}"); ++ register(1495, "{Name:'minecraft:repeater',Properties:{delay:'2',facing:'east',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'2',facing:'east',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'2',facing:'east',locked:'true'}}"); ++ register(1496, "{Name:'minecraft:repeater',Properties:{delay:'3',facing:'south',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'3',facing:'south',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'3',facing:'south',locked:'true'}}"); ++ register(1497, "{Name:'minecraft:repeater',Properties:{delay:'3',facing:'west',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'3',facing:'west',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'3',facing:'west',locked:'true'}}"); ++ register(1498, "{Name:'minecraft:repeater',Properties:{delay:'3',facing:'north',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'3',facing:'north',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'3',facing:'north',locked:'true'}}"); ++ register(1499, "{Name:'minecraft:repeater',Properties:{delay:'3',facing:'east',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'3',facing:'east',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'3',facing:'east',locked:'true'}}"); ++ register(1500, "{Name:'minecraft:repeater',Properties:{delay:'4',facing:'south',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'4',facing:'south',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'4',facing:'south',locked:'true'}}"); ++ register(1501, "{Name:'minecraft:repeater',Properties:{delay:'4',facing:'west',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'4',facing:'west',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'4',facing:'west',locked:'true'}}"); ++ register(1502, "{Name:'minecraft:repeater',Properties:{delay:'4',facing:'north',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'4',facing:'north',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'4',facing:'north',locked:'true'}}"); ++ register(1503, "{Name:'minecraft:repeater',Properties:{delay:'4',facing:'east',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'4',facing:'east',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'4',facing:'east',locked:'true'}}"); ++ register(1504, "{Name:'minecraft:repeater',Properties:{delay:'1',facing:'south',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'1',facing:'south',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'1',facing:'south',locked:'true'}}"); ++ register(1505, "{Name:'minecraft:repeater',Properties:{delay:'1',facing:'west',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'1',facing:'west',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'1',facing:'west',locked:'true'}}"); ++ register(1506, "{Name:'minecraft:repeater',Properties:{delay:'1',facing:'north',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'1',facing:'north',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'1',facing:'north',locked:'true'}}"); ++ register(1507, "{Name:'minecraft:repeater',Properties:{delay:'1',facing:'east',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'1',facing:'east',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'1',facing:'east',locked:'true'}}"); ++ register(1508, "{Name:'minecraft:repeater',Properties:{delay:'2',facing:'south',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'2',facing:'south',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'2',facing:'south',locked:'true'}}"); ++ register(1509, "{Name:'minecraft:repeater',Properties:{delay:'2',facing:'west',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'2',facing:'west',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'2',facing:'west',locked:'true'}}"); ++ register(1510, "{Name:'minecraft:repeater',Properties:{delay:'2',facing:'north',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'2',facing:'north',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'2',facing:'north',locked:'true'}}"); ++ register(1511, "{Name:'minecraft:repeater',Properties:{delay:'2',facing:'east',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'2',facing:'east',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'2',facing:'east',locked:'true'}}"); ++ register(1512, "{Name:'minecraft:repeater',Properties:{delay:'3',facing:'south',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'3',facing:'south',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'3',facing:'south',locked:'true'}}"); ++ register(1513, "{Name:'minecraft:repeater',Properties:{delay:'3',facing:'west',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'3',facing:'west',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'3',facing:'west',locked:'true'}}"); ++ register(1514, "{Name:'minecraft:repeater',Properties:{delay:'3',facing:'north',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'3',facing:'north',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'3',facing:'north',locked:'true'}}"); ++ register(1515, "{Name:'minecraft:repeater',Properties:{delay:'3',facing:'east',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'3',facing:'east',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'3',facing:'east',locked:'true'}}"); ++ register(1516, "{Name:'minecraft:repeater',Properties:{delay:'4',facing:'south',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'4',facing:'south',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'4',facing:'south',locked:'true'}}"); ++ register(1517, "{Name:'minecraft:repeater',Properties:{delay:'4',facing:'west',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'4',facing:'west',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'4',facing:'west',locked:'true'}}"); ++ register(1518, "{Name:'minecraft:repeater',Properties:{delay:'4',facing:'north',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'4',facing:'north',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'4',facing:'north',locked:'true'}}"); ++ register(1519, "{Name:'minecraft:repeater',Properties:{delay:'4',facing:'east',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'4',facing:'east',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'4',facing:'east',locked:'true'}}"); ++ register(1520, "{Name:'minecraft:white_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'white'}}"); ++ register(1521, "{Name:'minecraft:orange_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'orange'}}"); ++ register(1522, "{Name:'minecraft:magenta_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'magenta'}}"); ++ register(1523, "{Name:'minecraft:light_blue_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'light_blue'}}"); ++ register(1524, "{Name:'minecraft:yellow_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'yellow'}}"); ++ register(1525, "{Name:'minecraft:lime_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'lime'}}"); ++ register(1526, "{Name:'minecraft:pink_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'pink'}}"); ++ register(1527, "{Name:'minecraft:gray_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'gray'}}"); ++ register(1528, "{Name:'minecraft:light_gray_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'silver'}}"); ++ register(1529, "{Name:'minecraft:cyan_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'cyan'}}"); ++ register(1530, "{Name:'minecraft:purple_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'purple'}}"); ++ register(1531, "{Name:'minecraft:blue_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'blue'}}"); ++ register(1532, "{Name:'minecraft:brown_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'brown'}}"); ++ register(1533, "{Name:'minecraft:green_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'green'}}"); ++ register(1534, "{Name:'minecraft:red_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'red'}}"); ++ register(1535, "{Name:'minecraft:black_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'black'}}"); ++ register(1536, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'north',half:'bottom',open:'false'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'north',half:'bottom',open:'false'}}"); ++ register(1537, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'south',half:'bottom',open:'false'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'south',half:'bottom',open:'false'}}"); ++ register(1538, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'west',half:'bottom',open:'false'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'west',half:'bottom',open:'false'}}"); ++ register(1539, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'east',half:'bottom',open:'false'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'east',half:'bottom',open:'false'}}"); ++ register(1540, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'north',half:'bottom',open:'true'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'north',half:'bottom',open:'true'}}"); ++ register(1541, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'south',half:'bottom',open:'true'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'south',half:'bottom',open:'true'}}"); ++ register(1542, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'west',half:'bottom',open:'true'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'west',half:'bottom',open:'true'}}"); ++ register(1543, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'east',half:'bottom',open:'true'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'east',half:'bottom',open:'true'}}"); ++ register(1544, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'north',half:'top',open:'false'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'north',half:'top',open:'false'}}"); ++ register(1545, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'south',half:'top',open:'false'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'south',half:'top',open:'false'}}"); ++ register(1546, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'west',half:'top',open:'false'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'west',half:'top',open:'false'}}"); ++ register(1547, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'east',half:'top',open:'false'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'east',half:'top',open:'false'}}"); ++ register(1548, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'north',half:'top',open:'true'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'north',half:'top',open:'true'}}"); ++ register(1549, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'south',half:'top',open:'true'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'south',half:'top',open:'true'}}"); ++ register(1550, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'west',half:'top',open:'true'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'west',half:'top',open:'true'}}"); ++ register(1551, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'east',half:'top',open:'true'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'east',half:'top',open:'true'}}"); ++ register(1552, "{Name:'minecraft:infested_stone'}", "{Name:'minecraft:monster_egg',Properties:{variant:'stone'}}"); ++ register(1553, "{Name:'minecraft:infested_cobblestone'}", "{Name:'minecraft:monster_egg',Properties:{variant:'cobblestone'}}"); ++ register(1554, "{Name:'minecraft:infested_stone_bricks'}", "{Name:'minecraft:monster_egg',Properties:{variant:'stone_brick'}}"); ++ register(1555, "{Name:'minecraft:infested_mossy_stone_bricks'}", "{Name:'minecraft:monster_egg',Properties:{variant:'mossy_brick'}}"); ++ register(1556, "{Name:'minecraft:infested_cracked_stone_bricks'}", "{Name:'minecraft:monster_egg',Properties:{variant:'cracked_brick'}}"); ++ register(1557, "{Name:'minecraft:infested_chiseled_stone_bricks'}", "{Name:'minecraft:monster_egg',Properties:{variant:'chiseled_brick'}}"); ++ register(1568, "{Name:'minecraft:stone_bricks'}", "{Name:'minecraft:stonebrick',Properties:{variant:'stonebrick'}}"); ++ register(1569, "{Name:'minecraft:mossy_stone_bricks'}", "{Name:'minecraft:stonebrick',Properties:{variant:'mossy_stonebrick'}}"); ++ register(1570, "{Name:'minecraft:cracked_stone_bricks'}", "{Name:'minecraft:stonebrick',Properties:{variant:'cracked_stonebrick'}}"); ++ register(1571, "{Name:'minecraft:chiseled_stone_bricks'}", "{Name:'minecraft:stonebrick',Properties:{variant:'chiseled_stonebrick'}}"); ++ register(1584, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'false',up:'false',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'all_inside'}}"); ++ register(1585, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'true',east:'false',south:'false',west:'true',up:'true',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'north_west'}}"); ++ register(1586, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'true',east:'false',south:'false',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'north'}}"); ++ register(1587, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'true',east:'true',south:'false',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'north_east'}}"); ++ register(1588, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'true',up:'true',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'west'}}"); ++ register(1589, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'center'}}"); ++ register(1590, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'false',east:'true',south:'false',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'east'}}"); ++ register(1591, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'false',east:'false',south:'true',west:'true',up:'true',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'south_west'}}"); ++ register(1592, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'false',east:'false',south:'true',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'south'}}"); ++ register(1593, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'false',east:'true',south:'true',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'south_east'}}"); ++ register(1594, "{Name:'minecraft:mushroom_stem',Properties:{north:'true',east:'true',south:'true',west:'true',up:'false',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'stem'}}"); ++ register(1595, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'false',up:'false',down:'false'}}"); ++ register(1596, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'false',up:'false',down:'false'}}"); ++ register(1597, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'false',up:'false',down:'false'}}"); ++ register(1598, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'true',east:'true',south:'true',west:'true',up:'true',down:'true'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'all_outside'}}"); ++ register(1599, "{Name:'minecraft:mushroom_stem',Properties:{north:'true',east:'true',south:'true',west:'true',up:'true',down:'true'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'all_stem'}}"); ++ register(1600, "{Name:'minecraft:red_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'false',up:'false',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'all_inside'}}"); ++ register(1601, "{Name:'minecraft:red_mushroom_block',Properties:{north:'true',east:'false',south:'false',west:'true',up:'true',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'north_west'}}"); ++ register(1602, "{Name:'minecraft:red_mushroom_block',Properties:{north:'true',east:'false',south:'false',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'north'}}"); ++ register(1603, "{Name:'minecraft:red_mushroom_block',Properties:{north:'true',east:'true',south:'false',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'north_east'}}"); ++ register(1604, "{Name:'minecraft:red_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'true',up:'true',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'west'}}"); ++ register(1605, "{Name:'minecraft:red_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'center'}}"); ++ register(1606, "{Name:'minecraft:red_mushroom_block',Properties:{north:'false',east:'true',south:'false',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'east'}}"); ++ register(1607, "{Name:'minecraft:red_mushroom_block',Properties:{north:'false',east:'false',south:'true',west:'true',up:'true',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'south_west'}}"); ++ register(1608, "{Name:'minecraft:red_mushroom_block',Properties:{north:'false',east:'false',south:'true',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'south'}}"); ++ register(1609, "{Name:'minecraft:red_mushroom_block',Properties:{north:'false',east:'true',south:'true',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'south_east'}}"); ++ register(1610, "{Name:'minecraft:mushroom_stem',Properties:{north:'true',east:'true',south:'true',west:'true',up:'false',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'stem'}}"); ++ register(1611, "{Name:'minecraft:red_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'false',up:'false',down:'false'}}"); ++ register(1612, "{Name:'minecraft:red_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'false',up:'false',down:'false'}}"); ++ register(1613, "{Name:'minecraft:red_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'false',up:'false',down:'false'}}"); ++ register(1614, "{Name:'minecraft:red_mushroom_block',Properties:{north:'true',east:'true',south:'true',west:'true',up:'true',down:'true'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'all_outside'}}"); ++ register(1615, "{Name:'minecraft:mushroom_stem',Properties:{north:'true',east:'true',south:'true',west:'true',up:'true',down:'true'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'all_stem'}}"); ++ register(1616, "{Name:'minecraft:iron_bars',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:iron_bars',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:iron_bars',Properties:{east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:iron_bars',Properties:{east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:iron_bars',Properties:{east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:iron_bars',Properties:{east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:iron_bars',Properties:{east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:iron_bars',Properties:{east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:iron_bars',Properties:{east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:iron_bars',Properties:{east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:iron_bars',Properties:{east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:iron_bars',Properties:{east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:iron_bars',Properties:{east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:iron_bars',Properties:{east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:iron_bars',Properties:{east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:iron_bars',Properties:{east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:iron_bars',Properties:{east:'true',north:'true',south:'true',west:'true'}}"); ++ register(1632, "{Name:'minecraft:glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:glass_pane',Properties:{east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:glass_pane',Properties:{east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:glass_pane',Properties:{east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:glass_pane',Properties:{east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:glass_pane',Properties:{east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:glass_pane',Properties:{east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:glass_pane',Properties:{east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:glass_pane',Properties:{east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:glass_pane',Properties:{east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:glass_pane',Properties:{east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:glass_pane',Properties:{east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:glass_pane',Properties:{east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:glass_pane',Properties:{east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:glass_pane',Properties:{east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:glass_pane',Properties:{east:'true',north:'true',south:'true',west:'true'}}"); ++ register(1648, "{Name:'minecraft:melon_block'}", "{Name:'minecraft:melon_block'}"); ++ register(1664, "{Name:'minecraft:pumpkin_stem',Properties:{age:'0'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'0',facing:'east'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'0',facing:'north'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'0',facing:'south'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'0',facing:'up'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'0',facing:'west'}}"); ++ register(1665, "{Name:'minecraft:pumpkin_stem',Properties:{age:'1'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'1',facing:'east'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'1',facing:'north'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'1',facing:'south'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'1',facing:'up'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'1',facing:'west'}}"); ++ register(1666, "{Name:'minecraft:pumpkin_stem',Properties:{age:'2'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'2',facing:'east'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'2',facing:'north'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'2',facing:'south'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'2',facing:'up'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'2',facing:'west'}}"); ++ register(1667, "{Name:'minecraft:pumpkin_stem',Properties:{age:'3'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'3',facing:'east'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'3',facing:'north'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'3',facing:'south'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'3',facing:'up'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'3',facing:'west'}}"); ++ register(1668, "{Name:'minecraft:pumpkin_stem',Properties:{age:'4'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'4',facing:'east'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'4',facing:'north'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'4',facing:'south'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'4',facing:'up'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'4',facing:'west'}}"); ++ register(1669, "{Name:'minecraft:pumpkin_stem',Properties:{age:'5'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'5',facing:'east'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'5',facing:'north'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'5',facing:'south'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'5',facing:'up'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'5',facing:'west'}}"); ++ register(1670, "{Name:'minecraft:pumpkin_stem',Properties:{age:'6'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'6',facing:'east'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'6',facing:'north'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'6',facing:'south'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'6',facing:'up'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'6',facing:'west'}}"); ++ register(1671, "{Name:'minecraft:pumpkin_stem',Properties:{age:'7'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'7',facing:'east'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'7',facing:'north'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'7',facing:'south'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'7',facing:'up'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'7',facing:'west'}}"); ++ register(1680, "{Name:'minecraft:melon_stem',Properties:{age:'0'}}", "{Name:'minecraft:melon_stem',Properties:{age:'0',facing:'east'}}", "{Name:'minecraft:melon_stem',Properties:{age:'0',facing:'north'}}", "{Name:'minecraft:melon_stem',Properties:{age:'0',facing:'south'}}", "{Name:'minecraft:melon_stem',Properties:{age:'0',facing:'up'}}", "{Name:'minecraft:melon_stem',Properties:{age:'0',facing:'west'}}"); ++ register(1681, "{Name:'minecraft:melon_stem',Properties:{age:'1'}}", "{Name:'minecraft:melon_stem',Properties:{age:'1',facing:'east'}}", "{Name:'minecraft:melon_stem',Properties:{age:'1',facing:'north'}}", "{Name:'minecraft:melon_stem',Properties:{age:'1',facing:'south'}}", "{Name:'minecraft:melon_stem',Properties:{age:'1',facing:'up'}}", "{Name:'minecraft:melon_stem',Properties:{age:'1',facing:'west'}}"); ++ register(1682, "{Name:'minecraft:melon_stem',Properties:{age:'2'}}", "{Name:'minecraft:melon_stem',Properties:{age:'2',facing:'east'}}", "{Name:'minecraft:melon_stem',Properties:{age:'2',facing:'north'}}", "{Name:'minecraft:melon_stem',Properties:{age:'2',facing:'south'}}", "{Name:'minecraft:melon_stem',Properties:{age:'2',facing:'up'}}", "{Name:'minecraft:melon_stem',Properties:{age:'2',facing:'west'}}"); ++ register(1683, "{Name:'minecraft:melon_stem',Properties:{age:'3'}}", "{Name:'minecraft:melon_stem',Properties:{age:'3',facing:'east'}}", "{Name:'minecraft:melon_stem',Properties:{age:'3',facing:'north'}}", "{Name:'minecraft:melon_stem',Properties:{age:'3',facing:'south'}}", "{Name:'minecraft:melon_stem',Properties:{age:'3',facing:'up'}}", "{Name:'minecraft:melon_stem',Properties:{age:'3',facing:'west'}}"); ++ register(1684, "{Name:'minecraft:melon_stem',Properties:{age:'4'}}", "{Name:'minecraft:melon_stem',Properties:{age:'4',facing:'east'}}", "{Name:'minecraft:melon_stem',Properties:{age:'4',facing:'north'}}", "{Name:'minecraft:melon_stem',Properties:{age:'4',facing:'south'}}", "{Name:'minecraft:melon_stem',Properties:{age:'4',facing:'up'}}", "{Name:'minecraft:melon_stem',Properties:{age:'4',facing:'west'}}"); ++ register(1685, "{Name:'minecraft:melon_stem',Properties:{age:'5'}}", "{Name:'minecraft:melon_stem',Properties:{age:'5',facing:'east'}}", "{Name:'minecraft:melon_stem',Properties:{age:'5',facing:'north'}}", "{Name:'minecraft:melon_stem',Properties:{age:'5',facing:'south'}}", "{Name:'minecraft:melon_stem',Properties:{age:'5',facing:'up'}}", "{Name:'minecraft:melon_stem',Properties:{age:'5',facing:'west'}}"); ++ register(1686, "{Name:'minecraft:melon_stem',Properties:{age:'6'}}", "{Name:'minecraft:melon_stem',Properties:{age:'6',facing:'east'}}", "{Name:'minecraft:melon_stem',Properties:{age:'6',facing:'north'}}", "{Name:'minecraft:melon_stem',Properties:{age:'6',facing:'south'}}", "{Name:'minecraft:melon_stem',Properties:{age:'6',facing:'up'}}", "{Name:'minecraft:melon_stem',Properties:{age:'6',facing:'west'}}"); ++ register(1687, "{Name:'minecraft:melon_stem',Properties:{age:'7'}}", "{Name:'minecraft:melon_stem',Properties:{age:'7',facing:'east'}}", "{Name:'minecraft:melon_stem',Properties:{age:'7',facing:'north'}}", "{Name:'minecraft:melon_stem',Properties:{age:'7',facing:'south'}}", "{Name:'minecraft:melon_stem',Properties:{age:'7',facing:'up'}}", "{Name:'minecraft:melon_stem',Properties:{age:'7',facing:'west'}}"); ++ register(1696, "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'false',up:'true',west:'false'}}"); ++ register(1697, "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'true',up:'true',west:'false'}}"); ++ register(1698, "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'false',up:'true',west:'true'}}"); ++ register(1699, "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'true',up:'true',west:'true'}}"); ++ register(1700, "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'false',up:'true',west:'false'}}"); ++ register(1701, "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'true',up:'true',west:'false'}}"); ++ register(1702, "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'false',up:'true',west:'true'}}"); ++ register(1703, "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'true',up:'true',west:'true'}}"); ++ register(1704, "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'false',up:'true',west:'false'}}"); ++ register(1705, "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'true',up:'true',west:'false'}}"); ++ register(1706, "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'false',up:'true',west:'true'}}"); ++ register(1707, "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'true',up:'true',west:'true'}}"); ++ register(1708, "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'false',up:'true',west:'false'}}"); ++ register(1709, "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'true',up:'true',west:'false'}}"); ++ register(1710, "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'false',up:'true',west:'true'}}"); ++ register(1711, "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'true',up:'true',west:'true'}}"); ++ register(1712, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'false'}}"); ++ register(1713, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'false'}}"); ++ register(1714, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'false'}}"); ++ register(1715, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'false'}}"); ++ register(1716, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'false'}}"); ++ register(1717, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'false'}}"); ++ register(1718, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'false'}}"); ++ register(1719, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'false'}}"); ++ register(1720, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'true'}}"); ++ register(1721, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'true'}}"); ++ register(1722, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'true'}}"); ++ register(1723, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'true'}}"); ++ register(1724, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'true'}}"); ++ register(1725, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'true'}}"); ++ register(1726, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'true'}}"); ++ register(1727, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'true'}}"); ++ register(1728, "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); ++ register(1729, "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); ++ register(1730, "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); ++ register(1731, "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); ++ register(1732, "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); ++ register(1733, "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); ++ register(1734, "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); ++ register(1735, "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); ++ register(1744, "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); ++ register(1745, "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); ++ register(1746, "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); ++ register(1747, "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); ++ register(1748, "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); ++ register(1749, "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); ++ register(1750, "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); ++ register(1751, "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); ++ register(1760, "{Name:'minecraft:mycelium',Properties:{snowy:'false'}}", "{Name:'minecraft:mycelium',Properties:{snowy:'false'}}", "{Name:'minecraft:mycelium',Properties:{snowy:'true'}}"); ++ register(1776, "{Name:'minecraft:lily_pad'}", "{Name:'minecraft:waterlily'}"); ++ register(1792, "{Name:'minecraft:nether_bricks'}", "{Name:'minecraft:nether_brick'}"); ++ register(1808, "{Name:'minecraft:nether_brick_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'true',north:'true',south:'true',west:'true'}}"); ++ register(1824, "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); ++ register(1825, "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); ++ register(1826, "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); ++ register(1827, "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); ++ register(1828, "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); ++ register(1829, "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); ++ register(1830, "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); ++ register(1831, "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); ++ register(1840, "{Name:'minecraft:nether_wart',Properties:{age:'0'}}", "{Name:'minecraft:nether_wart',Properties:{age:'0'}}"); ++ register(1841, "{Name:'minecraft:nether_wart',Properties:{age:'1'}}", "{Name:'minecraft:nether_wart',Properties:{age:'1'}}"); ++ register(1842, "{Name:'minecraft:nether_wart',Properties:{age:'2'}}", "{Name:'minecraft:nether_wart',Properties:{age:'2'}}"); ++ register(1843, "{Name:'minecraft:nether_wart',Properties:{age:'3'}}", "{Name:'minecraft:nether_wart',Properties:{age:'3'}}"); ++ register(1856, "{Name:'minecraft:enchanting_table'}", "{Name:'minecraft:enchanting_table'}"); ++ register(1872, "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'false',has_bottle_1:'false',has_bottle_2:'false'}}", "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'false',has_bottle_1:'false',has_bottle_2:'false'}}"); ++ register(1873, "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'true',has_bottle_1:'false',has_bottle_2:'false'}}", "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'true',has_bottle_1:'false',has_bottle_2:'false'}}"); ++ register(1874, "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'false',has_bottle_1:'true',has_bottle_2:'false'}}", "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'false',has_bottle_1:'true',has_bottle_2:'false'}}"); ++ register(1875, "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'true',has_bottle_1:'true',has_bottle_2:'false'}}", "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'true',has_bottle_1:'true',has_bottle_2:'false'}}"); ++ register(1876, "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'false',has_bottle_1:'false',has_bottle_2:'true'}}", "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'false',has_bottle_1:'false',has_bottle_2:'true'}}"); ++ register(1877, "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'true',has_bottle_1:'false',has_bottle_2:'true'}}", "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'true',has_bottle_1:'false',has_bottle_2:'true'}}"); ++ register(1878, "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'false',has_bottle_1:'true',has_bottle_2:'true'}}", "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'false',has_bottle_1:'true',has_bottle_2:'true'}}"); ++ register(1879, "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'true',has_bottle_1:'true',has_bottle_2:'true'}}", "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'true',has_bottle_1:'true',has_bottle_2:'true'}}"); ++ register(1888, "{Name:'minecraft:cauldron',Properties:{level:'0'}}", "{Name:'minecraft:cauldron',Properties:{level:'0'}}"); ++ register(1889, "{Name:'minecraft:cauldron',Properties:{level:'1'}}", "{Name:'minecraft:cauldron',Properties:{level:'1'}}"); ++ register(1890, "{Name:'minecraft:cauldron',Properties:{level:'2'}}", "{Name:'minecraft:cauldron',Properties:{level:'2'}}"); ++ register(1891, "{Name:'minecraft:cauldron',Properties:{level:'3'}}", "{Name:'minecraft:cauldron',Properties:{level:'3'}}"); ++ register(1904, "{Name:'minecraft:end_portal'}", "{Name:'minecraft:end_portal'}"); ++ register(1920, "{Name:'minecraft:end_portal_frame',Properties:{eye:'false',facing:'south'}}", "{Name:'minecraft:end_portal_frame',Properties:{eye:'false',facing:'south'}}"); ++ register(1921, "{Name:'minecraft:end_portal_frame',Properties:{eye:'false',facing:'west'}}", "{Name:'minecraft:end_portal_frame',Properties:{eye:'false',facing:'west'}}"); ++ register(1922, "{Name:'minecraft:end_portal_frame',Properties:{eye:'false',facing:'north'}}", "{Name:'minecraft:end_portal_frame',Properties:{eye:'false',facing:'north'}}"); ++ register(1923, "{Name:'minecraft:end_portal_frame',Properties:{eye:'false',facing:'east'}}", "{Name:'minecraft:end_portal_frame',Properties:{eye:'false',facing:'east'}}"); ++ register(1924, "{Name:'minecraft:end_portal_frame',Properties:{eye:'true',facing:'south'}}", "{Name:'minecraft:end_portal_frame',Properties:{eye:'true',facing:'south'}}"); ++ register(1925, "{Name:'minecraft:end_portal_frame',Properties:{eye:'true',facing:'west'}}", "{Name:'minecraft:end_portal_frame',Properties:{eye:'true',facing:'west'}}"); ++ register(1926, "{Name:'minecraft:end_portal_frame',Properties:{eye:'true',facing:'north'}}", "{Name:'minecraft:end_portal_frame',Properties:{eye:'true',facing:'north'}}"); ++ register(1927, "{Name:'minecraft:end_portal_frame',Properties:{eye:'true',facing:'east'}}", "{Name:'minecraft:end_portal_frame',Properties:{eye:'true',facing:'east'}}"); ++ register(1936, "{Name:'minecraft:end_stone'}", "{Name:'minecraft:end_stone'}"); ++ register(1952, "{Name:'minecraft:dragon_egg'}", "{Name:'minecraft:dragon_egg'}"); ++ register(1968, "{Name:'minecraft:redstone_lamp',Properties:{lit:'false'}}", "{Name:'minecraft:redstone_lamp'}"); ++ register(1984, "{Name:'minecraft:redstone_lamp',Properties:{lit:'true'}}", "{Name:'minecraft:lit_redstone_lamp'}"); ++ register(2000, "{Name:'minecraft:oak_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_wooden_slab',Properties:{variant:'oak'}}"); ++ register(2001, "{Name:'minecraft:spruce_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_wooden_slab',Properties:{variant:'spruce'}}"); ++ register(2002, "{Name:'minecraft:birch_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_wooden_slab',Properties:{variant:'birch'}}"); ++ register(2003, "{Name:'minecraft:jungle_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_wooden_slab',Properties:{variant:'jungle'}}"); ++ register(2004, "{Name:'minecraft:acacia_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_wooden_slab',Properties:{variant:'acacia'}}"); ++ register(2005, "{Name:'minecraft:dark_oak_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_wooden_slab',Properties:{variant:'dark_oak'}}"); ++ register(2016, "{Name:'minecraft:oak_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'bottom',variant:'oak'}}"); ++ register(2017, "{Name:'minecraft:spruce_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'bottom',variant:'spruce'}}"); ++ register(2018, "{Name:'minecraft:birch_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'bottom',variant:'birch'}}"); ++ register(2019, "{Name:'minecraft:jungle_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'bottom',variant:'jungle'}}"); ++ register(2020, "{Name:'minecraft:acacia_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'bottom',variant:'acacia'}}"); ++ register(2021, "{Name:'minecraft:dark_oak_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'bottom',variant:'dark_oak'}}"); ++ register(2024, "{Name:'minecraft:oak_slab',Properties:{type:'top'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'top',variant:'oak'}}"); ++ register(2025, "{Name:'minecraft:spruce_slab',Properties:{type:'top'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'top',variant:'spruce'}}"); ++ register(2026, "{Name:'minecraft:birch_slab',Properties:{type:'top'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'top',variant:'birch'}}"); ++ register(2027, "{Name:'minecraft:jungle_slab',Properties:{type:'top'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'top',variant:'jungle'}}"); ++ register(2028, "{Name:'minecraft:acacia_slab',Properties:{type:'top'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'top',variant:'acacia'}}"); ++ register(2029, "{Name:'minecraft:dark_oak_slab',Properties:{type:'top'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'top',variant:'dark_oak'}}"); ++ register(2032, "{Name:'minecraft:cocoa',Properties:{age:'0',facing:'south'}}", "{Name:'minecraft:cocoa',Properties:{age:'0',facing:'south'}}"); ++ register(2033, "{Name:'minecraft:cocoa',Properties:{age:'0',facing:'west'}}", "{Name:'minecraft:cocoa',Properties:{age:'0',facing:'west'}}"); ++ register(2034, "{Name:'minecraft:cocoa',Properties:{age:'0',facing:'north'}}", "{Name:'minecraft:cocoa',Properties:{age:'0',facing:'north'}}"); ++ register(2035, "{Name:'minecraft:cocoa',Properties:{age:'0',facing:'east'}}", "{Name:'minecraft:cocoa',Properties:{age:'0',facing:'east'}}"); ++ register(2036, "{Name:'minecraft:cocoa',Properties:{age:'1',facing:'south'}}", "{Name:'minecraft:cocoa',Properties:{age:'1',facing:'south'}}"); ++ register(2037, "{Name:'minecraft:cocoa',Properties:{age:'1',facing:'west'}}", "{Name:'minecraft:cocoa',Properties:{age:'1',facing:'west'}}"); ++ register(2038, "{Name:'minecraft:cocoa',Properties:{age:'1',facing:'north'}}", "{Name:'minecraft:cocoa',Properties:{age:'1',facing:'north'}}"); ++ register(2039, "{Name:'minecraft:cocoa',Properties:{age:'1',facing:'east'}}", "{Name:'minecraft:cocoa',Properties:{age:'1',facing:'east'}}"); ++ register(2040, "{Name:'minecraft:cocoa',Properties:{age:'2',facing:'south'}}", "{Name:'minecraft:cocoa',Properties:{age:'2',facing:'south'}}"); ++ register(2041, "{Name:'minecraft:cocoa',Properties:{age:'2',facing:'west'}}", "{Name:'minecraft:cocoa',Properties:{age:'2',facing:'west'}}"); ++ register(2042, "{Name:'minecraft:cocoa',Properties:{age:'2',facing:'north'}}", "{Name:'minecraft:cocoa',Properties:{age:'2',facing:'north'}}"); ++ register(2043, "{Name:'minecraft:cocoa',Properties:{age:'2',facing:'east'}}", "{Name:'minecraft:cocoa',Properties:{age:'2',facing:'east'}}"); ++ register(2048, "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); ++ register(2049, "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); ++ register(2050, "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); ++ register(2051, "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); ++ register(2052, "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); ++ register(2053, "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); ++ register(2054, "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); ++ register(2055, "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); ++ register(2064, "{Name:'minecraft:emerald_ore'}", "{Name:'minecraft:emerald_ore'}"); ++ register(2082, "{Name:'minecraft:ender_chest',Properties:{facing:'north'}}", "{Name:'minecraft:ender_chest',Properties:{facing:'north'}}"); ++ register(2083, "{Name:'minecraft:ender_chest',Properties:{facing:'south'}}", "{Name:'minecraft:ender_chest',Properties:{facing:'south'}}"); ++ register(2084, "{Name:'minecraft:ender_chest',Properties:{facing:'west'}}", "{Name:'minecraft:ender_chest',Properties:{facing:'west'}}"); ++ register(2085, "{Name:'minecraft:ender_chest',Properties:{facing:'east'}}", "{Name:'minecraft:ender_chest',Properties:{facing:'east'}}"); ++ register(2096, "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'south',powered:'false'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'south',powered:'false'}}"); ++ register(2097, "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'west',powered:'false'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'west',powered:'false'}}"); ++ register(2098, "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'north',powered:'false'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'north',powered:'false'}}"); ++ register(2099, "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'east',powered:'false'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'east',powered:'false'}}"); ++ register(2100, "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'south',powered:'false'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'south',powered:'false'}}"); ++ register(2101, "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'west',powered:'false'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'west',powered:'false'}}"); ++ register(2102, "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'north',powered:'false'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'north',powered:'false'}}"); ++ register(2103, "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'east',powered:'false'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'east',powered:'false'}}"); ++ register(2104, "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'south',powered:'true'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'south',powered:'true'}}"); ++ register(2105, "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'west',powered:'true'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'west',powered:'true'}}"); ++ register(2106, "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'north',powered:'true'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'north',powered:'true'}}"); ++ register(2107, "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'east',powered:'true'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'east',powered:'true'}}"); ++ register(2108, "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'south',powered:'true'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'south',powered:'true'}}"); ++ register(2109, "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'west',powered:'true'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'west',powered:'true'}}"); ++ register(2110, "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'north',powered:'true'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'north',powered:'true'}}"); ++ register(2111, "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'east',powered:'true'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'east',powered:'true'}}"); ++ register(2112, "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'true',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'true',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'true',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'true',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'false',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'false',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'false',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'true',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'true',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'true',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'true',powered:'false',south:'true',west:'true'}}"); ++ register(2113, "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'true',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'true',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'true',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'true',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'false',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'false',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'false',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'true',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'true',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'true',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'true',powered:'true',south:'true',west:'true'}}"); ++ register(2114, "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'false',south:'false',west:'false'}}"); ++ register(2115, "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'true',south:'false',west:'false'}}"); ++ register(2116, "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'true',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'true',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'true',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'true',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'false',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'false',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'false',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'true',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'true',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'true',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'true',powered:'false',south:'true',west:'true'}}"); ++ register(2117, "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'true',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'true',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'true',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'true',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'false',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'false',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'false',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'true',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'true',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'true',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'true',powered:'true',south:'true',west:'true'}}"); ++ register(2118, "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'false',south:'false',west:'false'}}"); ++ register(2119, "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'true',south:'false',west:'false'}}"); ++ register(2120, "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'true',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'true',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'true',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'true',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'false',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'false',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'false',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'true',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'true',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'true',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'true',powered:'false',south:'true',west:'true'}}"); ++ register(2121, "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'true',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'true',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'true',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'true',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'false',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'false',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'false',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'true',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'true',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'true',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'true',powered:'true',south:'true',west:'true'}}"); ++ register(2122, "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'false',south:'false',west:'false'}}"); ++ register(2123, "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'true',south:'false',west:'false'}}"); ++ register(2124, "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'true',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'true',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'true',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'true',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'false',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'false',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'false',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'true',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'true',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'true',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'true',powered:'false',south:'true',west:'true'}}"); ++ register(2125, "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'true',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'true',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'true',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'true',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'false',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'false',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'false',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'true',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'true',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'true',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'true',powered:'true',south:'true',west:'true'}}"); ++ register(2126, "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'false',south:'false',west:'false'}}"); ++ register(2128, "{Name:'minecraft:emerald_block'}", "{Name:'minecraft:emerald_block'}"); ++ register(2144, "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); ++ register(2145, "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); ++ register(2146, "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); ++ register(2147, "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); ++ register(2148, "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); ++ register(2149, "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); ++ register(2150, "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); ++ register(2151, "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); ++ register(2160, "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); ++ register(2161, "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); ++ register(2162, "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); ++ register(2163, "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); ++ register(2164, "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); ++ register(2165, "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); ++ register(2166, "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); ++ register(2167, "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); ++ register(2176, "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); ++ register(2177, "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); ++ register(2178, "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); ++ register(2179, "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); ++ register(2180, "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); ++ register(2181, "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); ++ register(2182, "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); ++ register(2183, "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); ++ register(2192, "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'down'}}", "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'down'}}"); ++ register(2193, "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'up'}}", "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'up'}}"); ++ register(2194, "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'north'}}", "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'north'}}"); ++ register(2195, "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'south'}}", "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'south'}}"); ++ register(2196, "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'west'}}", "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'west'}}"); ++ register(2197, "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'east'}}", "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'east'}}"); ++ register(2200, "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'down'}}", "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'down'}}"); ++ register(2201, "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'up'}}", "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'up'}}"); ++ register(2202, "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'north'}}", "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'north'}}"); ++ register(2203, "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'south'}}", "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'south'}}"); ++ register(2204, "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'west'}}", "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'west'}}"); ++ register(2205, "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'east'}}", "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'east'}}"); ++ register(2208, "{Name:'minecraft:beacon'}", "{Name:'minecraft:beacon'}"); ++ register(2224, "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'false',up:'false',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'false',up:'false',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'false',up:'true',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'false',up:'true',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'true',up:'false',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'true',up:'false',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'true',up:'true',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'true',up:'true',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'false',up:'false',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'false',up:'false',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'false',up:'true',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'false',up:'true',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'true',up:'false',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'true',up:'false',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'true',up:'true',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'true',up:'true',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'false',up:'false',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'false',up:'false',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'false',up:'true',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'false',up:'true',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'true',up:'false',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'true',up:'false',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'true',up:'true',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'true',up:'true',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'false',up:'false',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'false',up:'false',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'false',up:'true',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'false',up:'true',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'true',up:'false',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'true',up:'false',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'true',up:'true',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'true',up:'true',variant:'cobblestone',west:'true'}}"); ++ register(2225, "{Name:'minecraft:mossy_cobblestone_wall',Properties:{east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'false',up:'false',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'false',up:'false',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'false',up:'true',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'false',up:'true',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'true',up:'false',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'true',up:'false',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'true',up:'true',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'true',up:'true',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'false',up:'false',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'false',up:'false',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'false',up:'true',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'false',up:'true',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'true',up:'false',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'true',up:'false',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'true',up:'true',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'true',up:'true',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'false',up:'false',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'false',up:'false',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'false',up:'true',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'false',up:'true',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'true',up:'false',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'true',up:'false',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'true',up:'true',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'true',up:'true',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'false',up:'false',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'false',up:'false',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'false',up:'true',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'false',up:'true',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'true',up:'false',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'true',up:'false',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'true',up:'true',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'true',up:'true',variant:'mossy_cobblestone',west:'true'}}"); ++ // There are a few changes made to flower pot here, notably handling how legacy data is handled. ++ // The TE itself should contain the target item and from there the proper state can be determined. However, there are ++ // blocks that do not contain a TE. So we need to make sure there is a default to fall on. ++ // I simply followed the legacy handling from BlockFlowerPot from 1.8.8 to find what legacy data mapped to what. ++ // It's better than defaulting everything to a cactus. ++ register(2240, "{Name:'minecraft:flower_pot'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'0'}}"); ++ register(2241, "{Name:'minecraft:potted_poppy'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'1'}}"); ++ register(2242, "{Name:'minecraft:potted_dandelion'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'2'}}"); ++ register(2243, "{Name:'minecraft:potted_oak_sapling'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'3'}}"); ++ register(2244, "{Name:'minecraft:potted_spruce_sapling'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'4'}}"); ++ register(2245, "{Name:'minecraft:potted_birch_sapling'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'5'}}"); ++ register(2246, "{Name:'minecraft:potted_jungle_sapling'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'6'}}"); ++ register(2247, "{Name:'minecraft:potted_red_mushroom'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'7'}}"); ++ register(2248, "{Name:'minecraft:potted_brown_mushroom'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'8'}}"); ++ register(2249, "{Name:'minecraft:potted_cactus'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'9'}}"); ++ register(2250, "{Name:'minecraft:potted_dead_bush'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'10'}}"); ++ register(2251, "{Name:'minecraft:potted_fern'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'11'}}"); ++ register(2252, "{Name:'minecraft:potted_acacia_sapling'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'12'}}"); ++ register(2253, "{Name:'minecraft:potted_dark_oak_sapling'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'13'}}"); ++ register(2254, "{Name:'minecraft:flower_pot'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'14'}}"); ++ register(2255, "{Name:'minecraft:flower_pot'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'15'}}"); ++ register(2256, "{Name:'minecraft:carrots',Properties:{age:'0'}}", "{Name:'minecraft:carrots',Properties:{age:'0'}}"); ++ register(2257, "{Name:'minecraft:carrots',Properties:{age:'1'}}", "{Name:'minecraft:carrots',Properties:{age:'1'}}"); ++ register(2258, "{Name:'minecraft:carrots',Properties:{age:'2'}}", "{Name:'minecraft:carrots',Properties:{age:'2'}}"); ++ register(2259, "{Name:'minecraft:carrots',Properties:{age:'3'}}", "{Name:'minecraft:carrots',Properties:{age:'3'}}"); ++ register(2260, "{Name:'minecraft:carrots',Properties:{age:'4'}}", "{Name:'minecraft:carrots',Properties:{age:'4'}}"); ++ register(2261, "{Name:'minecraft:carrots',Properties:{age:'5'}}", "{Name:'minecraft:carrots',Properties:{age:'5'}}"); ++ register(2262, "{Name:'minecraft:carrots',Properties:{age:'6'}}", "{Name:'minecraft:carrots',Properties:{age:'6'}}"); ++ register(2263, "{Name:'minecraft:carrots',Properties:{age:'7'}}", "{Name:'minecraft:carrots',Properties:{age:'7'}}"); ++ register(2272, "{Name:'minecraft:potatoes',Properties:{age:'0'}}", "{Name:'minecraft:potatoes',Properties:{age:'0'}}"); ++ register(2273, "{Name:'minecraft:potatoes',Properties:{age:'1'}}", "{Name:'minecraft:potatoes',Properties:{age:'1'}}"); ++ register(2274, "{Name:'minecraft:potatoes',Properties:{age:'2'}}", "{Name:'minecraft:potatoes',Properties:{age:'2'}}"); ++ register(2275, "{Name:'minecraft:potatoes',Properties:{age:'3'}}", "{Name:'minecraft:potatoes',Properties:{age:'3'}}"); ++ register(2276, "{Name:'minecraft:potatoes',Properties:{age:'4'}}", "{Name:'minecraft:potatoes',Properties:{age:'4'}}"); ++ register(2277, "{Name:'minecraft:potatoes',Properties:{age:'5'}}", "{Name:'minecraft:potatoes',Properties:{age:'5'}}"); ++ register(2278, "{Name:'minecraft:potatoes',Properties:{age:'6'}}", "{Name:'minecraft:potatoes',Properties:{age:'6'}}"); ++ register(2279, "{Name:'minecraft:potatoes',Properties:{age:'7'}}", "{Name:'minecraft:potatoes',Properties:{age:'7'}}"); ++ register(2288, "{Name:'minecraft:oak_button',Properties:{face:'ceiling',facing:'north',powered:'false'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'down',powered:'false'}}"); ++ register(2289, "{Name:'minecraft:oak_button',Properties:{face:'wall',facing:'east',powered:'false'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'east',powered:'false'}}"); ++ register(2290, "{Name:'minecraft:oak_button',Properties:{face:'wall',facing:'west',powered:'false'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'west',powered:'false'}}"); ++ register(2291, "{Name:'minecraft:oak_button',Properties:{face:'wall',facing:'south',powered:'false'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'south',powered:'false'}}"); ++ register(2292, "{Name:'minecraft:oak_button',Properties:{face:'wall',facing:'north',powered:'false'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'north',powered:'false'}}"); ++ register(2293, "{Name:'minecraft:oak_button',Properties:{face:'floor',facing:'north',powered:'false'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'up',powered:'false'}}"); ++ register(2296, "{Name:'minecraft:oak_button',Properties:{face:'ceiling',facing:'north',powered:'true'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'down',powered:'true'}}"); ++ register(2297, "{Name:'minecraft:oak_button',Properties:{face:'wall',facing:'east',powered:'true'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'east',powered:'true'}}"); ++ register(2298, "{Name:'minecraft:oak_button',Properties:{face:'wall',facing:'west',powered:'true'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'west',powered:'true'}}"); ++ register(2299, "{Name:'minecraft:oak_button',Properties:{face:'wall',facing:'south',powered:'true'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'south',powered:'true'}}"); ++ register(2300, "{Name:'minecraft:oak_button',Properties:{face:'wall',facing:'north',powered:'true'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'north',powered:'true'}}"); ++ register(2301, "{Name:'minecraft:oak_button',Properties:{face:'floor',facing:'north',powered:'true'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'up',powered:'true'}}"); ++ register(2304, "{Name:'%%FILTER_ME%%',Properties:{facing:'down',nodrop:'false'}}", "{Name:'minecraft:skull',Properties:{facing:'down',nodrop:'false'}}"); ++ register(2305, "{Name:'%%FILTER_ME%%',Properties:{facing:'up',nodrop:'false'}}", "{Name:'minecraft:skull',Properties:{facing:'up',nodrop:'false'}}"); ++ register(2306, "{Name:'%%FILTER_ME%%',Properties:{facing:'north',nodrop:'false'}}", "{Name:'minecraft:skull',Properties:{facing:'north',nodrop:'false'}}"); ++ register(2307, "{Name:'%%FILTER_ME%%',Properties:{facing:'south',nodrop:'false'}}", "{Name:'minecraft:skull',Properties:{facing:'south',nodrop:'false'}}"); ++ register(2308, "{Name:'%%FILTER_ME%%',Properties:{facing:'west',nodrop:'false'}}", "{Name:'minecraft:skull',Properties:{facing:'west',nodrop:'false'}}"); ++ register(2309, "{Name:'%%FILTER_ME%%',Properties:{facing:'east',nodrop:'false'}}", "{Name:'minecraft:skull',Properties:{facing:'east',nodrop:'false'}}"); ++ register(2312, "{Name:'%%FILTER_ME%%',Properties:{facing:'down',nodrop:'true'}}", "{Name:'minecraft:skull',Properties:{facing:'down',nodrop:'true'}}"); ++ register(2313, "{Name:'%%FILTER_ME%%',Properties:{facing:'up',nodrop:'true'}}", "{Name:'minecraft:skull',Properties:{facing:'up',nodrop:'true'}}"); ++ register(2314, "{Name:'%%FILTER_ME%%',Properties:{facing:'north',nodrop:'true'}}", "{Name:'minecraft:skull',Properties:{facing:'north',nodrop:'true'}}"); ++ register(2315, "{Name:'%%FILTER_ME%%',Properties:{facing:'south',nodrop:'true'}}", "{Name:'minecraft:skull',Properties:{facing:'south',nodrop:'true'}}"); ++ register(2316, "{Name:'%%FILTER_ME%%',Properties:{facing:'west',nodrop:'true'}}", "{Name:'minecraft:skull',Properties:{facing:'west',nodrop:'true'}}"); ++ register(2317, "{Name:'%%FILTER_ME%%',Properties:{facing:'east',nodrop:'true'}}", "{Name:'minecraft:skull',Properties:{facing:'east',nodrop:'true'}}"); ++ register(2320, "{Name:'minecraft:anvil',Properties:{facing:'south'}}", "{Name:'minecraft:anvil',Properties:{damage:'0',facing:'south'}}"); ++ register(2321, "{Name:'minecraft:anvil',Properties:{facing:'west'}}", "{Name:'minecraft:anvil',Properties:{damage:'0',facing:'west'}}"); ++ register(2322, "{Name:'minecraft:anvil',Properties:{facing:'north'}}", "{Name:'minecraft:anvil',Properties:{damage:'0',facing:'north'}}"); ++ register(2323, "{Name:'minecraft:anvil',Properties:{facing:'east'}}", "{Name:'minecraft:anvil',Properties:{damage:'0',facing:'east'}}"); ++ register(2324, "{Name:'minecraft:chipped_anvil',Properties:{facing:'south'}}", "{Name:'minecraft:anvil',Properties:{damage:'1',facing:'south'}}"); ++ register(2325, "{Name:'minecraft:chipped_anvil',Properties:{facing:'west'}}", "{Name:'minecraft:anvil',Properties:{damage:'1',facing:'west'}}"); ++ register(2326, "{Name:'minecraft:chipped_anvil',Properties:{facing:'north'}}", "{Name:'minecraft:anvil',Properties:{damage:'1',facing:'north'}}"); ++ register(2327, "{Name:'minecraft:chipped_anvil',Properties:{facing:'east'}}", "{Name:'minecraft:anvil',Properties:{damage:'1',facing:'east'}}"); ++ register(2328, "{Name:'minecraft:damaged_anvil',Properties:{facing:'south'}}", "{Name:'minecraft:anvil',Properties:{damage:'2',facing:'south'}}"); ++ register(2329, "{Name:'minecraft:damaged_anvil',Properties:{facing:'west'}}", "{Name:'minecraft:anvil',Properties:{damage:'2',facing:'west'}}"); ++ register(2330, "{Name:'minecraft:damaged_anvil',Properties:{facing:'north'}}", "{Name:'minecraft:anvil',Properties:{damage:'2',facing:'north'}}"); ++ register(2331, "{Name:'minecraft:damaged_anvil',Properties:{facing:'east'}}", "{Name:'minecraft:anvil',Properties:{damage:'2',facing:'east'}}"); ++ register(2338, "{Name:'minecraft:trapped_chest',Properties:{facing:'north',type:'single'}}", "{Name:'minecraft:trapped_chest',Properties:{facing:'north'}}"); ++ register(2339, "{Name:'minecraft:trapped_chest',Properties:{facing:'south',type:'single'}}", "{Name:'minecraft:trapped_chest',Properties:{facing:'south'}}"); ++ register(2340, "{Name:'minecraft:trapped_chest',Properties:{facing:'west',type:'single'}}", "{Name:'minecraft:trapped_chest',Properties:{facing:'west'}}"); ++ register(2341, "{Name:'minecraft:trapped_chest',Properties:{facing:'east',type:'single'}}", "{Name:'minecraft:trapped_chest',Properties:{facing:'east'}}"); ++ register(2352, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'0'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'0'}}"); ++ register(2353, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'1'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'1'}}"); ++ register(2354, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'2'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'2'}}"); ++ register(2355, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'3'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'3'}}"); ++ register(2356, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'4'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'4'}}"); ++ register(2357, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'5'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'5'}}"); ++ register(2358, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'6'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'6'}}"); ++ register(2359, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'7'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'7'}}"); ++ register(2360, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'8'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'8'}}"); ++ register(2361, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'9'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'9'}}"); ++ register(2362, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'10'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'10'}}"); ++ register(2363, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'11'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'11'}}"); ++ register(2364, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'12'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'12'}}"); ++ register(2365, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'13'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'13'}}"); ++ register(2366, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'14'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'14'}}"); ++ register(2367, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'15'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'15'}}"); ++ register(2368, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'0'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'0'}}"); ++ register(2369, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'1'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'1'}}"); ++ register(2370, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'2'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'2'}}"); ++ register(2371, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'3'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'3'}}"); ++ register(2372, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'4'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'4'}}"); ++ register(2373, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'5'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'5'}}"); ++ register(2374, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'6'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'6'}}"); ++ register(2375, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'7'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'7'}}"); ++ register(2376, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'8'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'8'}}"); ++ register(2377, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'9'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'9'}}"); ++ register(2378, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'10'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'10'}}"); ++ register(2379, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'11'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'11'}}"); ++ register(2380, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'12'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'12'}}"); ++ register(2381, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'13'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'13'}}"); ++ register(2382, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'14'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'14'}}"); ++ register(2383, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'15'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'15'}}"); ++ register(2384, "{Name:'minecraft:comparator',Properties:{facing:'south',mode:'compare',powered:'false'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'south',mode:'compare',powered:'false'}}"); ++ register(2385, "{Name:'minecraft:comparator',Properties:{facing:'west',mode:'compare',powered:'false'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'west',mode:'compare',powered:'false'}}"); ++ register(2386, "{Name:'minecraft:comparator',Properties:{facing:'north',mode:'compare',powered:'false'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'north',mode:'compare',powered:'false'}}"); ++ register(2387, "{Name:'minecraft:comparator',Properties:{facing:'east',mode:'compare',powered:'false'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'east',mode:'compare',powered:'false'}}"); ++ register(2388, "{Name:'minecraft:comparator',Properties:{facing:'south',mode:'subtract',powered:'false'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'south',mode:'subtract',powered:'false'}}"); ++ register(2389, "{Name:'minecraft:comparator',Properties:{facing:'west',mode:'subtract',powered:'false'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'west',mode:'subtract',powered:'false'}}"); ++ register(2390, "{Name:'minecraft:comparator',Properties:{facing:'north',mode:'subtract',powered:'false'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'north',mode:'subtract',powered:'false'}}"); ++ register(2391, "{Name:'minecraft:comparator',Properties:{facing:'east',mode:'subtract',powered:'false'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'east',mode:'subtract',powered:'false'}}"); ++ register(2392, "{Name:'minecraft:comparator',Properties:{facing:'south',mode:'compare',powered:'true'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'south',mode:'compare',powered:'true'}}"); ++ register(2393, "{Name:'minecraft:comparator',Properties:{facing:'west',mode:'compare',powered:'true'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'west',mode:'compare',powered:'true'}}"); ++ register(2394, "{Name:'minecraft:comparator',Properties:{facing:'north',mode:'compare',powered:'true'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'north',mode:'compare',powered:'true'}}"); ++ register(2395, "{Name:'minecraft:comparator',Properties:{facing:'east',mode:'compare',powered:'true'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'east',mode:'compare',powered:'true'}}"); ++ register(2396, "{Name:'minecraft:comparator',Properties:{facing:'south',mode:'subtract',powered:'true'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'south',mode:'subtract',powered:'true'}}"); ++ register(2397, "{Name:'minecraft:comparator',Properties:{facing:'west',mode:'subtract',powered:'true'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'west',mode:'subtract',powered:'true'}}"); ++ register(2398, "{Name:'minecraft:comparator',Properties:{facing:'north',mode:'subtract',powered:'true'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'north',mode:'subtract',powered:'true'}}"); ++ register(2399, "{Name:'minecraft:comparator',Properties:{facing:'east',mode:'subtract',powered:'true'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'east',mode:'subtract',powered:'true'}}"); ++ register(2400, "{Name:'minecraft:comparator',Properties:{facing:'south',mode:'compare',powered:'false'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'south',mode:'compare',powered:'false'}}"); ++ register(2401, "{Name:'minecraft:comparator',Properties:{facing:'west',mode:'compare',powered:'false'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'west',mode:'compare',powered:'false'}}"); ++ register(2402, "{Name:'minecraft:comparator',Properties:{facing:'north',mode:'compare',powered:'false'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'north',mode:'compare',powered:'false'}}"); ++ register(2403, "{Name:'minecraft:comparator',Properties:{facing:'east',mode:'compare',powered:'false'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'east',mode:'compare',powered:'false'}}"); ++ register(2404, "{Name:'minecraft:comparator',Properties:{facing:'south',mode:'subtract',powered:'false'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'south',mode:'subtract',powered:'false'}}"); ++ register(2405, "{Name:'minecraft:comparator',Properties:{facing:'west',mode:'subtract',powered:'false'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'west',mode:'subtract',powered:'false'}}"); ++ register(2406, "{Name:'minecraft:comparator',Properties:{facing:'north',mode:'subtract',powered:'false'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'north',mode:'subtract',powered:'false'}}"); ++ register(2407, "{Name:'minecraft:comparator',Properties:{facing:'east',mode:'subtract',powered:'false'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'east',mode:'subtract',powered:'false'}}"); ++ register(2408, "{Name:'minecraft:comparator',Properties:{facing:'south',mode:'compare',powered:'true'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'south',mode:'compare',powered:'true'}}"); ++ register(2409, "{Name:'minecraft:comparator',Properties:{facing:'west',mode:'compare',powered:'true'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'west',mode:'compare',powered:'true'}}"); ++ register(2410, "{Name:'minecraft:comparator',Properties:{facing:'north',mode:'compare',powered:'true'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'north',mode:'compare',powered:'true'}}"); ++ register(2411, "{Name:'minecraft:comparator',Properties:{facing:'east',mode:'compare',powered:'true'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'east',mode:'compare',powered:'true'}}"); ++ register(2412, "{Name:'minecraft:comparator',Properties:{facing:'south',mode:'subtract',powered:'true'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'south',mode:'subtract',powered:'true'}}"); ++ register(2413, "{Name:'minecraft:comparator',Properties:{facing:'west',mode:'subtract',powered:'true'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'west',mode:'subtract',powered:'true'}}"); ++ register(2414, "{Name:'minecraft:comparator',Properties:{facing:'north',mode:'subtract',powered:'true'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'north',mode:'subtract',powered:'true'}}"); ++ register(2415, "{Name:'minecraft:comparator',Properties:{facing:'east',mode:'subtract',powered:'true'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'east',mode:'subtract',powered:'true'}}"); ++ register(2416, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'0'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'0'}}"); ++ register(2417, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'1'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'1'}}"); ++ register(2418, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'2'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'2'}}"); ++ register(2419, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'3'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'3'}}"); ++ register(2420, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'4'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'4'}}"); ++ register(2421, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'5'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'5'}}"); ++ register(2422, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'6'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'6'}}"); ++ register(2423, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'7'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'7'}}"); ++ register(2424, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'8'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'8'}}"); ++ register(2425, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'9'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'9'}}"); ++ register(2426, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'10'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'10'}}"); ++ register(2427, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'11'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'11'}}"); ++ register(2428, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'12'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'12'}}"); ++ register(2429, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'13'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'13'}}"); ++ register(2430, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'14'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'14'}}"); ++ register(2431, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'15'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'15'}}"); ++ register(2432, "{Name:'minecraft:redstone_block'}", "{Name:'minecraft:redstone_block'}"); ++ register(2448, "{Name:'minecraft:nether_quartz_ore'}", "{Name:'minecraft:quartz_ore'}"); ++ register(2464, "{Name:'minecraft:hopper',Properties:{enabled:'true',facing:'down'}}", "{Name:'minecraft:hopper',Properties:{enabled:'true',facing:'down'}}"); ++ register(2466, "{Name:'minecraft:hopper',Properties:{enabled:'true',facing:'north'}}", "{Name:'minecraft:hopper',Properties:{enabled:'true',facing:'north'}}"); ++ register(2467, "{Name:'minecraft:hopper',Properties:{enabled:'true',facing:'south'}}", "{Name:'minecraft:hopper',Properties:{enabled:'true',facing:'south'}}"); ++ register(2468, "{Name:'minecraft:hopper',Properties:{enabled:'true',facing:'west'}}", "{Name:'minecraft:hopper',Properties:{enabled:'true',facing:'west'}}"); ++ register(2469, "{Name:'minecraft:hopper',Properties:{enabled:'true',facing:'east'}}", "{Name:'minecraft:hopper',Properties:{enabled:'true',facing:'east'}}"); ++ register(2472, "{Name:'minecraft:hopper',Properties:{enabled:'false',facing:'down'}}", "{Name:'minecraft:hopper',Properties:{enabled:'false',facing:'down'}}"); ++ register(2474, "{Name:'minecraft:hopper',Properties:{enabled:'false',facing:'north'}}", "{Name:'minecraft:hopper',Properties:{enabled:'false',facing:'north'}}"); ++ register(2475, "{Name:'minecraft:hopper',Properties:{enabled:'false',facing:'south'}}", "{Name:'minecraft:hopper',Properties:{enabled:'false',facing:'south'}}"); ++ register(2476, "{Name:'minecraft:hopper',Properties:{enabled:'false',facing:'west'}}", "{Name:'minecraft:hopper',Properties:{enabled:'false',facing:'west'}}"); ++ register(2477, "{Name:'minecraft:hopper',Properties:{enabled:'false',facing:'east'}}", "{Name:'minecraft:hopper',Properties:{enabled:'false',facing:'east'}}"); ++ register(2480, "{Name:'minecraft:quartz_block'}", "{Name:'minecraft:quartz_block',Properties:{variant:'default'}}"); ++ register(2481, "{Name:'minecraft:chiseled_quartz_block'}", "{Name:'minecraft:quartz_block',Properties:{variant:'chiseled'}}"); ++ register(2482, "{Name:'minecraft:quartz_pillar',Properties:{axis:'y'}}", "{Name:'minecraft:quartz_block',Properties:{variant:'lines_y'}}"); ++ register(2483, "{Name:'minecraft:quartz_pillar',Properties:{axis:'x'}}", "{Name:'minecraft:quartz_block',Properties:{variant:'lines_x'}}"); ++ register(2484, "{Name:'minecraft:quartz_pillar',Properties:{axis:'z'}}", "{Name:'minecraft:quartz_block',Properties:{variant:'lines_z'}}"); ++ register(2496, "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); ++ register(2497, "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); ++ register(2498, "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); ++ register(2499, "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); ++ register(2500, "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); ++ register(2501, "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); ++ register(2502, "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); ++ register(2503, "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); ++ register(2512, "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'north_south'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'north_south'}}"); ++ register(2513, "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'east_west'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'east_west'}}"); ++ register(2514, "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'ascending_east'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'ascending_east'}}"); ++ register(2515, "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'ascending_west'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'ascending_west'}}"); ++ register(2516, "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'ascending_north'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'ascending_north'}}"); ++ register(2517, "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'ascending_south'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'ascending_south'}}"); ++ register(2520, "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'north_south'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'north_south'}}"); ++ register(2521, "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'east_west'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'east_west'}}"); ++ register(2522, "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'ascending_east'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'ascending_east'}}"); ++ register(2523, "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'ascending_west'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'ascending_west'}}"); ++ register(2524, "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'ascending_north'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'ascending_north'}}"); ++ register(2525, "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'ascending_south'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'ascending_south'}}"); ++ register(2528, "{Name:'minecraft:dropper',Properties:{facing:'down',triggered:'false'}}", "{Name:'minecraft:dropper',Properties:{facing:'down',triggered:'false'}}"); ++ register(2529, "{Name:'minecraft:dropper',Properties:{facing:'up',triggered:'false'}}", "{Name:'minecraft:dropper',Properties:{facing:'up',triggered:'false'}}"); ++ register(2530, "{Name:'minecraft:dropper',Properties:{facing:'north',triggered:'false'}}", "{Name:'minecraft:dropper',Properties:{facing:'north',triggered:'false'}}"); ++ register(2531, "{Name:'minecraft:dropper',Properties:{facing:'south',triggered:'false'}}", "{Name:'minecraft:dropper',Properties:{facing:'south',triggered:'false'}}"); ++ register(2532, "{Name:'minecraft:dropper',Properties:{facing:'west',triggered:'false'}}", "{Name:'minecraft:dropper',Properties:{facing:'west',triggered:'false'}}"); ++ register(2533, "{Name:'minecraft:dropper',Properties:{facing:'east',triggered:'false'}}", "{Name:'minecraft:dropper',Properties:{facing:'east',triggered:'false'}}"); ++ register(2536, "{Name:'minecraft:dropper',Properties:{facing:'down',triggered:'true'}}", "{Name:'minecraft:dropper',Properties:{facing:'down',triggered:'true'}}"); ++ register(2537, "{Name:'minecraft:dropper',Properties:{facing:'up',triggered:'true'}}", "{Name:'minecraft:dropper',Properties:{facing:'up',triggered:'true'}}"); ++ register(2538, "{Name:'minecraft:dropper',Properties:{facing:'north',triggered:'true'}}", "{Name:'minecraft:dropper',Properties:{facing:'north',triggered:'true'}}"); ++ register(2539, "{Name:'minecraft:dropper',Properties:{facing:'south',triggered:'true'}}", "{Name:'minecraft:dropper',Properties:{facing:'south',triggered:'true'}}"); ++ register(2540, "{Name:'minecraft:dropper',Properties:{facing:'west',triggered:'true'}}", "{Name:'minecraft:dropper',Properties:{facing:'west',triggered:'true'}}"); ++ register(2541, "{Name:'minecraft:dropper',Properties:{facing:'east',triggered:'true'}}", "{Name:'minecraft:dropper',Properties:{facing:'east',triggered:'true'}}"); ++ register(2544, "{Name:'minecraft:white_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'white'}}"); ++ register(2545, "{Name:'minecraft:orange_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'orange'}}"); ++ register(2546, "{Name:'minecraft:magenta_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'magenta'}}"); ++ register(2547, "{Name:'minecraft:light_blue_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'light_blue'}}"); ++ register(2548, "{Name:'minecraft:yellow_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'yellow'}}"); ++ register(2549, "{Name:'minecraft:lime_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'lime'}}"); ++ register(2550, "{Name:'minecraft:pink_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'pink'}}"); ++ register(2551, "{Name:'minecraft:gray_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'gray'}}"); ++ register(2552, "{Name:'minecraft:light_gray_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'silver'}}"); ++ register(2553, "{Name:'minecraft:cyan_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'cyan'}}"); ++ register(2554, "{Name:'minecraft:purple_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'purple'}}"); ++ register(2555, "{Name:'minecraft:blue_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'blue'}}"); ++ register(2556, "{Name:'minecraft:brown_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'brown'}}"); ++ register(2557, "{Name:'minecraft:green_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'green'}}"); ++ register(2558, "{Name:'minecraft:red_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'red'}}"); ++ register(2559, "{Name:'minecraft:black_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'black'}}"); ++ register(2560, "{Name:'minecraft:white_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'true',north:'true',south:'true',west:'true'}}"); ++ register(2561, "{Name:'minecraft:orange_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'true',north:'true',south:'true',west:'true'}}"); ++ register(2562, "{Name:'minecraft:magenta_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'true',north:'true',south:'true',west:'true'}}"); ++ register(2563, "{Name:'minecraft:light_blue_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'true',north:'true',south:'true',west:'true'}}"); ++ register(2564, "{Name:'minecraft:yellow_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'true',north:'true',south:'true',west:'true'}}"); ++ register(2565, "{Name:'minecraft:lime_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'true',north:'true',south:'true',west:'true'}}"); ++ register(2566, "{Name:'minecraft:pink_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'true',north:'true',south:'true',west:'true'}}"); ++ register(2567, "{Name:'minecraft:gray_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'true',north:'true',south:'true',west:'true'}}"); ++ register(2568, "{Name:'minecraft:light_gray_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'true',north:'true',south:'true',west:'true'}}"); ++ register(2569, "{Name:'minecraft:cyan_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'true',north:'true',south:'true',west:'true'}}"); ++ register(2570, "{Name:'minecraft:purple_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'true',north:'true',south:'true',west:'true'}}"); ++ register(2571, "{Name:'minecraft:blue_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'true',north:'true',south:'true',west:'true'}}"); ++ register(2572, "{Name:'minecraft:brown_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'true',north:'true',south:'true',west:'true'}}"); ++ register(2573, "{Name:'minecraft:green_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'true',north:'true',south:'true',west:'true'}}"); ++ register(2574, "{Name:'minecraft:red_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'true',north:'true',south:'true',west:'true'}}"); ++ register(2575, "{Name:'minecraft:black_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'true',north:'true',south:'true',west:'true'}}"); ++ register(2576, "{Name:'minecraft:acacia_leaves',Properties:{check_decay:'false',decayable:'true'}}", "{Name:'minecraft:leaves2',Properties:{check_decay:'false',decayable:'true',variant:'acacia'}}"); ++ register(2577, "{Name:'minecraft:dark_oak_leaves',Properties:{check_decay:'false',decayable:'true'}}", "{Name:'minecraft:leaves2',Properties:{check_decay:'false',decayable:'true',variant:'dark_oak'}}"); ++ register(2580, "{Name:'minecraft:acacia_leaves',Properties:{check_decay:'false',decayable:'false'}}", "{Name:'minecraft:leaves2',Properties:{check_decay:'false',decayable:'false',variant:'acacia'}}"); ++ register(2581, "{Name:'minecraft:dark_oak_leaves',Properties:{check_decay:'false',decayable:'false'}}", "{Name:'minecraft:leaves2',Properties:{check_decay:'false',decayable:'false',variant:'dark_oak'}}"); ++ register(2584, "{Name:'minecraft:acacia_leaves',Properties:{check_decay:'true',decayable:'true'}}", "{Name:'minecraft:leaves2',Properties:{check_decay:'true',decayable:'true',variant:'acacia'}}"); ++ register(2585, "{Name:'minecraft:dark_oak_leaves',Properties:{check_decay:'true',decayable:'true'}}", "{Name:'minecraft:leaves2',Properties:{check_decay:'true',decayable:'true',variant:'dark_oak'}}"); ++ register(2588, "{Name:'minecraft:acacia_leaves',Properties:{check_decay:'true',decayable:'false'}}", "{Name:'minecraft:leaves2',Properties:{check_decay:'true',decayable:'false',variant:'acacia'}}"); ++ register(2589, "{Name:'minecraft:dark_oak_leaves',Properties:{check_decay:'true',decayable:'false'}}", "{Name:'minecraft:leaves2',Properties:{check_decay:'true',decayable:'false',variant:'dark_oak'}}"); ++ register(2592, "{Name:'minecraft:acacia_log',Properties:{axis:'y'}}", "{Name:'minecraft:log2',Properties:{axis:'y',variant:'acacia'}}"); ++ register(2593, "{Name:'minecraft:dark_oak_log',Properties:{axis:'y'}}", "{Name:'minecraft:log2',Properties:{axis:'y',variant:'dark_oak'}}"); ++ register(2596, "{Name:'minecraft:acacia_log',Properties:{axis:'x'}}", "{Name:'minecraft:log2',Properties:{axis:'x',variant:'acacia'}}"); ++ register(2597, "{Name:'minecraft:dark_oak_log',Properties:{axis:'x'}}", "{Name:'minecraft:log2',Properties:{axis:'x',variant:'dark_oak'}}"); ++ register(2600, "{Name:'minecraft:acacia_log',Properties:{axis:'z'}}", "{Name:'minecraft:log2',Properties:{axis:'z',variant:'acacia'}}"); ++ register(2601, "{Name:'minecraft:dark_oak_log',Properties:{axis:'z'}}", "{Name:'minecraft:log2',Properties:{axis:'z',variant:'dark_oak'}}"); ++ register(2604, "{Name:'minecraft:acacia_bark'}", "{Name:'minecraft:log2',Properties:{axis:'none',variant:'acacia'}}"); ++ register(2605, "{Name:'minecraft:dark_oak_bark'}", "{Name:'minecraft:log2',Properties:{axis:'none',variant:'dark_oak'}}"); ++ register(2608, "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); ++ register(2609, "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); ++ register(2610, "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); ++ register(2611, "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); ++ register(2612, "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); ++ register(2613, "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); ++ register(2614, "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); ++ register(2615, "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); ++ register(2624, "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); ++ register(2625, "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); ++ register(2626, "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); ++ register(2627, "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); ++ register(2628, "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); ++ register(2629, "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); ++ register(2630, "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); ++ register(2631, "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); ++ register(2640, "{Name:'minecraft:slime_block'}", "{Name:'minecraft:slime'}"); ++ register(2656, "{Name:'minecraft:barrier'}", "{Name:'minecraft:barrier'}"); ++ register(2672, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'north',half:'bottom',open:'false'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'north',half:'bottom',open:'false'}}"); ++ register(2673, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'south',half:'bottom',open:'false'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'south',half:'bottom',open:'false'}}"); ++ register(2674, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'west',half:'bottom',open:'false'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'west',half:'bottom',open:'false'}}"); ++ register(2675, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'east',half:'bottom',open:'false'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'east',half:'bottom',open:'false'}}"); ++ register(2676, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'north',half:'bottom',open:'true'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'north',half:'bottom',open:'true'}}"); ++ register(2677, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'south',half:'bottom',open:'true'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'south',half:'bottom',open:'true'}}"); ++ register(2678, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'west',half:'bottom',open:'true'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'west',half:'bottom',open:'true'}}"); ++ register(2679, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'east',half:'bottom',open:'true'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'east',half:'bottom',open:'true'}}"); ++ register(2680, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'north',half:'top',open:'false'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'north',half:'top',open:'false'}}"); ++ register(2681, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'south',half:'top',open:'false'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'south',half:'top',open:'false'}}"); ++ register(2682, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'west',half:'top',open:'false'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'west',half:'top',open:'false'}}"); ++ register(2683, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'east',half:'top',open:'false'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'east',half:'top',open:'false'}}"); ++ register(2684, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'north',half:'top',open:'true'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'north',half:'top',open:'true'}}"); ++ register(2685, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'south',half:'top',open:'true'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'south',half:'top',open:'true'}}"); ++ register(2686, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'west',half:'top',open:'true'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'west',half:'top',open:'true'}}"); ++ register(2687, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'east',half:'top',open:'true'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'east',half:'top',open:'true'}}"); ++ register(2688, "{Name:'minecraft:prismarine'}", "{Name:'minecraft:prismarine',Properties:{variant:'prismarine'}}"); ++ register(2689, "{Name:'minecraft:prismarine_bricks'}", "{Name:'minecraft:prismarine',Properties:{variant:'prismarine_bricks'}}"); ++ register(2690, "{Name:'minecraft:dark_prismarine'}", "{Name:'minecraft:prismarine',Properties:{variant:'dark_prismarine'}}"); ++ register(2704, "{Name:'minecraft:sea_lantern'}", "{Name:'minecraft:sea_lantern'}"); ++ register(2720, "{Name:'minecraft:hay_block',Properties:{axis:'y'}}", "{Name:'minecraft:hay_block',Properties:{axis:'y'}}"); ++ register(2724, "{Name:'minecraft:hay_block',Properties:{axis:'x'}}", "{Name:'minecraft:hay_block',Properties:{axis:'x'}}"); ++ register(2728, "{Name:'minecraft:hay_block',Properties:{axis:'z'}}", "{Name:'minecraft:hay_block',Properties:{axis:'z'}}"); ++ register(2736, "{Name:'minecraft:white_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'white'}}"); ++ register(2737, "{Name:'minecraft:orange_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'orange'}}"); ++ register(2738, "{Name:'minecraft:magenta_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'magenta'}}"); ++ register(2739, "{Name:'minecraft:light_blue_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'light_blue'}}"); ++ register(2740, "{Name:'minecraft:yellow_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'yellow'}}"); ++ register(2741, "{Name:'minecraft:lime_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'lime'}}"); ++ register(2742, "{Name:'minecraft:pink_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'pink'}}"); ++ register(2743, "{Name:'minecraft:gray_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'gray'}}"); ++ register(2744, "{Name:'minecraft:light_gray_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'silver'}}"); ++ register(2745, "{Name:'minecraft:cyan_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'cyan'}}"); ++ register(2746, "{Name:'minecraft:purple_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'purple'}}"); ++ register(2747, "{Name:'minecraft:blue_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'blue'}}"); ++ register(2748, "{Name:'minecraft:brown_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'brown'}}"); ++ register(2749, "{Name:'minecraft:green_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'green'}}"); ++ register(2750, "{Name:'minecraft:red_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'red'}}"); ++ register(2751, "{Name:'minecraft:black_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'black'}}"); ++ register(2752, "{Name:'minecraft:terracotta'}", "{Name:'minecraft:hardened_clay'}"); ++ register(2768, "{Name:'minecraft:coal_block'}", "{Name:'minecraft:coal_block'}"); ++ register(2784, "{Name:'minecraft:packed_ice'}", "{Name:'minecraft:packed_ice'}"); ++ register(2800, "{Name:'minecraft:sunflower',Properties:{half:'lower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'lower',variant:'sunflower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'lower',variant:'sunflower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'lower',variant:'sunflower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'lower',variant:'sunflower'}}"); ++ register(2801, "{Name:'minecraft:lilac',Properties:{half:'lower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'lower',variant:'syringa'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'lower',variant:'syringa'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'lower',variant:'syringa'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'lower',variant:'syringa'}}"); ++ register(2802, "{Name:'minecraft:tall_grass',Properties:{half:'lower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'lower',variant:'double_grass'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'lower',variant:'double_grass'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'lower',variant:'double_grass'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'lower',variant:'double_grass'}}"); ++ register(2803, "{Name:'minecraft:large_fern',Properties:{half:'lower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'lower',variant:'double_fern'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'lower',variant:'double_fern'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'lower',variant:'double_fern'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'lower',variant:'double_fern'}}"); ++ register(2804, "{Name:'minecraft:rose_bush',Properties:{half:'lower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'lower',variant:'double_rose'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'lower',variant:'double_rose'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'lower',variant:'double_rose'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'lower',variant:'double_rose'}}"); ++ register(2805, "{Name:'minecraft:peony',Properties:{half:'lower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'lower',variant:'paeonia'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'lower',variant:'paeonia'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'lower',variant:'paeonia'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'lower',variant:'paeonia'}}"); ++ register(2808, "{Name:'minecraft:peony',Properties:{half:'upper'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'upper',variant:'double_fern'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'upper',variant:'double_grass'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'upper',variant:'double_rose'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'upper',variant:'paeonia'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'upper',variant:'sunflower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'upper',variant:'syringa'}}"); ++ register(2809, "{Name:'minecraft:peony',Properties:{half:'upper'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'upper',variant:'double_fern'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'upper',variant:'double_grass'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'upper',variant:'double_rose'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'upper',variant:'paeonia'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'upper',variant:'sunflower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'upper',variant:'syringa'}}"); ++ register(2810, "{Name:'minecraft:peony',Properties:{half:'upper'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'upper',variant:'double_fern'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'upper',variant:'double_grass'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'upper',variant:'double_rose'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'upper',variant:'paeonia'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'upper',variant:'sunflower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'upper',variant:'syringa'}}"); ++ register(2811, "{Name:'minecraft:peony',Properties:{half:'upper'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'upper',variant:'double_fern'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'upper',variant:'double_grass'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'upper',variant:'double_rose'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'upper',variant:'paeonia'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'upper',variant:'sunflower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'upper',variant:'syringa'}}"); ++ register(2816, "{Name:'minecraft:white_banner',Properties:{rotation:'0'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'0'}}"); ++ register(2817, "{Name:'minecraft:white_banner',Properties:{rotation:'1'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'1'}}"); ++ register(2818, "{Name:'minecraft:white_banner',Properties:{rotation:'2'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'2'}}"); ++ register(2819, "{Name:'minecraft:white_banner',Properties:{rotation:'3'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'3'}}"); ++ register(2820, "{Name:'minecraft:white_banner',Properties:{rotation:'4'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'4'}}"); ++ register(2821, "{Name:'minecraft:white_banner',Properties:{rotation:'5'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'5'}}"); ++ register(2822, "{Name:'minecraft:white_banner',Properties:{rotation:'6'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'6'}}"); ++ register(2823, "{Name:'minecraft:white_banner',Properties:{rotation:'7'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'7'}}"); ++ register(2824, "{Name:'minecraft:white_banner',Properties:{rotation:'8'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'8'}}"); ++ register(2825, "{Name:'minecraft:white_banner',Properties:{rotation:'9'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'9'}}"); ++ register(2826, "{Name:'minecraft:white_banner',Properties:{rotation:'10'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'10'}}"); ++ register(2827, "{Name:'minecraft:white_banner',Properties:{rotation:'11'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'11'}}"); ++ register(2828, "{Name:'minecraft:white_banner',Properties:{rotation:'12'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'12'}}"); ++ register(2829, "{Name:'minecraft:white_banner',Properties:{rotation:'13'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'13'}}"); ++ register(2830, "{Name:'minecraft:white_banner',Properties:{rotation:'14'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'14'}}"); ++ register(2831, "{Name:'minecraft:white_banner',Properties:{rotation:'15'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'15'}}"); ++ register(2834, "{Name:'minecraft:white_wall_banner',Properties:{facing:'north'}}", "{Name:'minecraft:wall_banner',Properties:{facing:'north'}}"); ++ register(2835, "{Name:'minecraft:white_wall_banner',Properties:{facing:'south'}}", "{Name:'minecraft:wall_banner',Properties:{facing:'south'}}"); ++ register(2836, "{Name:'minecraft:white_wall_banner',Properties:{facing:'west'}}", "{Name:'minecraft:wall_banner',Properties:{facing:'west'}}"); ++ register(2837, "{Name:'minecraft:white_wall_banner',Properties:{facing:'east'}}", "{Name:'minecraft:wall_banner',Properties:{facing:'east'}}"); ++ register(2848, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'0'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'0'}}"); ++ register(2849, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'1'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'1'}}"); ++ register(2850, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'2'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'2'}}"); ++ register(2851, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'3'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'3'}}"); ++ register(2852, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'4'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'4'}}"); ++ register(2853, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'5'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'5'}}"); ++ register(2854, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'6'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'6'}}"); ++ register(2855, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'7'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'7'}}"); ++ register(2856, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'8'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'8'}}"); ++ register(2857, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'9'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'9'}}"); ++ register(2858, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'10'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'10'}}"); ++ register(2859, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'11'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'11'}}"); ++ register(2860, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'12'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'12'}}"); ++ register(2861, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'13'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'13'}}"); ++ register(2862, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'14'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'14'}}"); ++ register(2863, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'15'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'15'}}"); ++ register(2864, "{Name:'minecraft:red_sandstone'}", "{Name:'minecraft:red_sandstone',Properties:{type:'red_sandstone'}}"); ++ register(2865, "{Name:'minecraft:chiseled_red_sandstone'}", "{Name:'minecraft:red_sandstone',Properties:{type:'chiseled_red_sandstone'}}"); ++ register(2866, "{Name:'minecraft:cut_red_sandstone'}", "{Name:'minecraft:red_sandstone',Properties:{type:'smooth_red_sandstone'}}"); ++ register(2880, "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); ++ register(2881, "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); ++ register(2882, "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); ++ register(2883, "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); ++ register(2884, "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); ++ register(2885, "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); ++ register(2886, "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); ++ register(2887, "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); ++ register(2896, "{Name:'minecraft:red_sandstone_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab2',Properties:{seamless:'false',variant:'red_sandstone'}}"); ++ register(2904, "{Name:'minecraft:smooth_red_sandstone'}", "{Name:'minecraft:double_stone_slab2',Properties:{seamless:'true',variant:'red_sandstone'}}"); ++ register(2912, "{Name:'minecraft:red_sandstone_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:stone_slab2',Properties:{half:'bottom',variant:'red_sandstone'}}"); ++ register(2920, "{Name:'minecraft:red_sandstone_slab',Properties:{type:'top'}}", "{Name:'minecraft:stone_slab2',Properties:{half:'top',variant:'red_sandstone'}}"); ++ register(2928, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'false'}}"); ++ register(2929, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'false'}}"); ++ register(2930, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'false'}}"); ++ register(2931, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'false'}}"); ++ register(2932, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'false'}}"); ++ register(2933, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'false'}}"); ++ register(2934, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'false'}}"); ++ register(2935, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'false'}}"); ++ register(2936, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'true'}}"); ++ register(2937, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'true'}}"); ++ register(2938, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'true'}}"); ++ register(2939, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'true'}}"); ++ register(2940, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'true'}}"); ++ register(2941, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'true'}}"); ++ register(2942, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'true'}}"); ++ register(2943, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'true'}}"); ++ register(2944, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'false'}}"); ++ register(2945, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'false'}}"); ++ register(2946, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'false'}}"); ++ register(2947, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'false'}}"); ++ register(2948, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'false'}}"); ++ register(2949, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'false'}}"); ++ register(2950, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'false'}}"); ++ register(2951, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'false'}}"); ++ register(2952, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'true'}}"); ++ register(2953, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'true'}}"); ++ register(2954, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'true'}}"); ++ register(2955, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'true'}}"); ++ register(2956, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'true'}}"); ++ register(2957, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'true'}}"); ++ register(2958, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'true'}}"); ++ register(2959, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'true'}}"); ++ register(2960, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'false'}}"); ++ register(2961, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'false'}}"); ++ register(2962, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'false'}}"); ++ register(2963, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'false'}}"); ++ register(2964, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'false'}}"); ++ register(2965, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'false'}}"); ++ register(2966, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'false'}}"); ++ register(2967, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'false'}}"); ++ register(2968, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'true'}}"); ++ register(2969, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'true'}}"); ++ register(2970, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'true'}}"); ++ register(2971, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'true'}}"); ++ register(2972, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'true'}}"); ++ register(2973, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'true'}}"); ++ register(2974, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'true'}}"); ++ register(2975, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'true'}}"); ++ register(2976, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'false'}}"); ++ register(2977, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'false'}}"); ++ register(2978, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'false'}}"); ++ register(2979, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'false'}}"); ++ register(2980, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'false'}}"); ++ register(2981, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'false'}}"); ++ register(2982, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'false'}}"); ++ register(2983, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'false'}}"); ++ register(2984, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'true'}}"); ++ register(2985, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'true'}}"); ++ register(2986, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'true'}}"); ++ register(2987, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'true'}}"); ++ register(2988, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'true'}}"); ++ register(2989, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'true'}}"); ++ register(2990, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'true'}}"); ++ register(2991, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'true'}}"); ++ register(2992, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'false'}}"); ++ register(2993, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'false'}}"); ++ register(2994, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'false'}}"); ++ register(2995, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'false'}}"); ++ register(2996, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'false'}}"); ++ register(2997, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'false'}}"); ++ register(2998, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'false'}}"); ++ register(2999, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'false'}}"); ++ register(3000, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'true'}}"); ++ register(3001, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'true'}}"); ++ register(3002, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'true'}}"); ++ register(3003, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'true'}}"); ++ register(3004, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'true'}}"); ++ register(3005, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'true'}}"); ++ register(3006, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'true'}}"); ++ register(3007, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'true'}}"); ++ register(3008, "{Name:'minecraft:spruce_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'true',north:'true',south:'true',west:'true'}}"); ++ register(3024, "{Name:'minecraft:birch_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:birch_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:birch_fence',Properties:{east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:birch_fence',Properties:{east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:birch_fence',Properties:{east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:birch_fence',Properties:{east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:birch_fence',Properties:{east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:birch_fence',Properties:{east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:birch_fence',Properties:{east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:birch_fence',Properties:{east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:birch_fence',Properties:{east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:birch_fence',Properties:{east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:birch_fence',Properties:{east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:birch_fence',Properties:{east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:birch_fence',Properties:{east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:birch_fence',Properties:{east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:birch_fence',Properties:{east:'true',north:'true',south:'true',west:'true'}}"); ++ register(3040, "{Name:'minecraft:jungle_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'true',north:'true',south:'true',west:'true'}}"); ++ register(3056, "{Name:'minecraft:dark_oak_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'true',north:'true',south:'true',west:'true'}}"); ++ register(3072, "{Name:'minecraft:acacia_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'true',north:'true',south:'true',west:'true'}}"); ++ register(3088, "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(3089, "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(3090, "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(3091, "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(3092, "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(3093, "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(3094, "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(3095, "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(3096, "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'false'}}"); ++ register(3097, "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'false'}}"); ++ register(3098, "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'true'}}"); ++ register(3099, "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'true'}}"); ++ register(3104, "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(3105, "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(3106, "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(3107, "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(3108, "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(3109, "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(3110, "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(3111, "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(3112, "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'false'}}"); ++ register(3113, "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'false'}}"); ++ register(3114, "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'true'}}"); ++ register(3115, "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'true'}}"); ++ register(3120, "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(3121, "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(3122, "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(3123, "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(3124, "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(3125, "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(3126, "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(3127, "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(3128, "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'false'}}"); ++ register(3129, "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'false'}}"); ++ register(3130, "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'true'}}"); ++ register(3131, "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'true'}}"); ++ register(3136, "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(3137, "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(3138, "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(3139, "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(3140, "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(3141, "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(3142, "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(3143, "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(3144, "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'false'}}"); ++ register(3145, "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'false'}}"); ++ register(3146, "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'true'}}"); ++ register(3147, "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'true'}}"); ++ register(3152, "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(3153, "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(3154, "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(3155, "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'true'}}"); ++ register(3156, "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(3157, "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(3158, "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(3159, "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'true'}}"); ++ register(3160, "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'false'}}"); ++ register(3161, "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'false'}}"); ++ register(3162, "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'true'}}"); ++ register(3163, "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'true'}}"); ++ register(3168, "{Name:'minecraft:end_rod',Properties:{facing:'down'}}", "{Name:'minecraft:end_rod',Properties:{facing:'down'}}"); ++ register(3169, "{Name:'minecraft:end_rod',Properties:{facing:'up'}}", "{Name:'minecraft:end_rod',Properties:{facing:'up'}}"); ++ register(3170, "{Name:'minecraft:end_rod',Properties:{facing:'north'}}", "{Name:'minecraft:end_rod',Properties:{facing:'north'}}"); ++ register(3171, "{Name:'minecraft:end_rod',Properties:{facing:'south'}}", "{Name:'minecraft:end_rod',Properties:{facing:'south'}}"); ++ register(3172, "{Name:'minecraft:end_rod',Properties:{facing:'west'}}", "{Name:'minecraft:end_rod',Properties:{facing:'west'}}"); ++ register(3173, "{Name:'minecraft:end_rod',Properties:{facing:'east'}}", "{Name:'minecraft:end_rod',Properties:{facing:'east'}}"); ++ register(3184, "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); ++ register(3200, "{Name:'minecraft:chorus_flower',Properties:{age:'0'}}", "{Name:'minecraft:chorus_flower',Properties:{age:'0'}}"); ++ register(3201, "{Name:'minecraft:chorus_flower',Properties:{age:'1'}}", "{Name:'minecraft:chorus_flower',Properties:{age:'1'}}"); ++ register(3202, "{Name:'minecraft:chorus_flower',Properties:{age:'2'}}", "{Name:'minecraft:chorus_flower',Properties:{age:'2'}}"); ++ register(3203, "{Name:'minecraft:chorus_flower',Properties:{age:'3'}}", "{Name:'minecraft:chorus_flower',Properties:{age:'3'}}"); ++ register(3204, "{Name:'minecraft:chorus_flower',Properties:{age:'4'}}", "{Name:'minecraft:chorus_flower',Properties:{age:'4'}}"); ++ register(3205, "{Name:'minecraft:chorus_flower',Properties:{age:'5'}}", "{Name:'minecraft:chorus_flower',Properties:{age:'5'}}"); ++ register(3216, "{Name:'minecraft:purpur_block'}", "{Name:'minecraft:purpur_block'}"); ++ register(3232, "{Name:'minecraft:purpur_pillar',Properties:{axis:'y'}}", "{Name:'minecraft:purpur_pillar',Properties:{axis:'y'}}"); ++ register(3236, "{Name:'minecraft:purpur_pillar',Properties:{axis:'x'}}", "{Name:'minecraft:purpur_pillar',Properties:{axis:'x'}}"); ++ register(3240, "{Name:'minecraft:purpur_pillar',Properties:{axis:'z'}}", "{Name:'minecraft:purpur_pillar',Properties:{axis:'z'}}"); ++ register(3248, "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); ++ register(3249, "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); ++ register(3250, "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); ++ register(3251, "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); ++ register(3252, "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); ++ register(3253, "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); ++ register(3254, "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); ++ register(3255, "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); ++ register(3264, "{Name:'minecraft:purpur_slab',Properties:{type:'double'}}", "{Name:'minecraft:purpur_double_slab',Properties:{variant:'default'}}"); ++ register(3280, "{Name:'minecraft:purpur_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:purpur_slab',Properties:{half:'bottom',variant:'default'}}"); ++ register(3288, "{Name:'minecraft:purpur_slab',Properties:{type:'top'}}", "{Name:'minecraft:purpur_slab',Properties:{half:'top',variant:'default'}}"); ++ register(3296, "{Name:'minecraft:end_stone_bricks'}", "{Name:'minecraft:end_bricks'}"); ++ register(3312, "{Name:'minecraft:beetroots',Properties:{age:'0'}}", "{Name:'minecraft:beetroots',Properties:{age:'0'}}"); ++ register(3313, "{Name:'minecraft:beetroots',Properties:{age:'1'}}", "{Name:'minecraft:beetroots',Properties:{age:'1'}}"); ++ register(3314, "{Name:'minecraft:beetroots',Properties:{age:'2'}}", "{Name:'minecraft:beetroots',Properties:{age:'2'}}"); ++ register(3315, "{Name:'minecraft:beetroots',Properties:{age:'3'}}", "{Name:'minecraft:beetroots',Properties:{age:'3'}}"); ++ register(3328, "{Name:'minecraft:grass_path'}", "{Name:'minecraft:grass_path'}"); ++ register(3344, "{Name:'minecraft:end_gateway'}", "{Name:'minecraft:end_gateway'}"); ++ register(3360, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'down'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'down'}}"); ++ register(3361, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'up'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'up'}}"); ++ register(3362, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'north'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'north'}}"); ++ register(3363, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'south'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'south'}}"); ++ register(3364, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'west'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'west'}}"); ++ register(3365, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'east'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'east'}}"); ++ register(3368, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'down'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'down'}}"); ++ register(3369, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'up'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'up'}}"); ++ register(3370, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'north'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'north'}}"); ++ register(3371, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'south'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'south'}}"); ++ register(3372, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'west'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'west'}}"); ++ register(3373, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'east'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'east'}}"); ++ register(3376, "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'down'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'down'}}"); ++ register(3377, "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'up'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'up'}}"); ++ register(3378, "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'north'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'north'}}"); ++ register(3379, "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'south'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'south'}}"); ++ register(3380, "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'west'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'west'}}"); ++ register(3381, "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'east'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'east'}}"); ++ register(3384, "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'down'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'down'}}"); ++ register(3385, "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'up'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'up'}}"); ++ register(3386, "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'north'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'north'}}"); ++ register(3387, "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'south'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'south'}}"); ++ register(3388, "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'west'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'west'}}"); ++ register(3389, "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'east'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'east'}}"); ++ register(3392, "{Name:'minecraft:frosted_ice',Properties:{age:'0'}}", "{Name:'minecraft:frosted_ice',Properties:{age:'0'}}"); ++ register(3393, "{Name:'minecraft:frosted_ice',Properties:{age:'1'}}", "{Name:'minecraft:frosted_ice',Properties:{age:'1'}}"); ++ register(3394, "{Name:'minecraft:frosted_ice',Properties:{age:'2'}}", "{Name:'minecraft:frosted_ice',Properties:{age:'2'}}"); ++ register(3395, "{Name:'minecraft:frosted_ice',Properties:{age:'3'}}", "{Name:'minecraft:frosted_ice',Properties:{age:'3'}}"); ++ register(3408, "{Name:'minecraft:magma_block'}", "{Name:'minecraft:magma'}"); ++ register(3424, "{Name:'minecraft:nether_wart_block'}", "{Name:'minecraft:nether_wart_block'}"); ++ register(3440, "{Name:'minecraft:red_nether_bricks'}", "{Name:'minecraft:red_nether_brick'}"); ++ register(3456, "{Name:'minecraft:bone_block',Properties:{axis:'y'}}", "{Name:'minecraft:bone_block',Properties:{axis:'y'}}"); ++ register(3460, "{Name:'minecraft:bone_block',Properties:{axis:'x'}}", "{Name:'minecraft:bone_block',Properties:{axis:'x'}}"); ++ register(3464, "{Name:'minecraft:bone_block',Properties:{axis:'z'}}", "{Name:'minecraft:bone_block',Properties:{axis:'z'}}"); ++ register(3472, "{Name:'minecraft:structure_void'}", "{Name:'minecraft:structure_void'}"); ++ register(3488, "{Name:'minecraft:observer',Properties:{facing:'down',powered:'false'}}", "{Name:'minecraft:observer',Properties:{facing:'down',powered:'false'}}"); ++ register(3489, "{Name:'minecraft:observer',Properties:{facing:'up',powered:'false'}}", "{Name:'minecraft:observer',Properties:{facing:'up',powered:'false'}}"); ++ register(3490, "{Name:'minecraft:observer',Properties:{facing:'north',powered:'false'}}", "{Name:'minecraft:observer',Properties:{facing:'north',powered:'false'}}"); ++ register(3491, "{Name:'minecraft:observer',Properties:{facing:'south',powered:'false'}}", "{Name:'minecraft:observer',Properties:{facing:'south',powered:'false'}}"); ++ register(3492, "{Name:'minecraft:observer',Properties:{facing:'west',powered:'false'}}", "{Name:'minecraft:observer',Properties:{facing:'west',powered:'false'}}"); ++ register(3493, "{Name:'minecraft:observer',Properties:{facing:'east',powered:'false'}}", "{Name:'minecraft:observer',Properties:{facing:'east',powered:'false'}}"); ++ register(3496, "{Name:'minecraft:observer',Properties:{facing:'down',powered:'true'}}", "{Name:'minecraft:observer',Properties:{facing:'down',powered:'true'}}"); ++ register(3497, "{Name:'minecraft:observer',Properties:{facing:'up',powered:'true'}}", "{Name:'minecraft:observer',Properties:{facing:'up',powered:'true'}}"); ++ register(3498, "{Name:'minecraft:observer',Properties:{facing:'north',powered:'true'}}", "{Name:'minecraft:observer',Properties:{facing:'north',powered:'true'}}"); ++ register(3499, "{Name:'minecraft:observer',Properties:{facing:'south',powered:'true'}}", "{Name:'minecraft:observer',Properties:{facing:'south',powered:'true'}}"); ++ register(3500, "{Name:'minecraft:observer',Properties:{facing:'west',powered:'true'}}", "{Name:'minecraft:observer',Properties:{facing:'west',powered:'true'}}"); ++ register(3501, "{Name:'minecraft:observer',Properties:{facing:'east',powered:'true'}}", "{Name:'minecraft:observer',Properties:{facing:'east',powered:'true'}}"); ++ register(3504, "{Name:'minecraft:white_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:white_shulker_box',Properties:{facing:'down'}}"); ++ register(3505, "{Name:'minecraft:white_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:white_shulker_box',Properties:{facing:'up'}}"); ++ register(3506, "{Name:'minecraft:white_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:white_shulker_box',Properties:{facing:'north'}}"); ++ register(3507, "{Name:'minecraft:white_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:white_shulker_box',Properties:{facing:'south'}}"); ++ register(3508, "{Name:'minecraft:white_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:white_shulker_box',Properties:{facing:'west'}}"); ++ register(3509, "{Name:'minecraft:white_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:white_shulker_box',Properties:{facing:'east'}}"); ++ register(3520, "{Name:'minecraft:orange_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:orange_shulker_box',Properties:{facing:'down'}}"); ++ register(3521, "{Name:'minecraft:orange_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:orange_shulker_box',Properties:{facing:'up'}}"); ++ register(3522, "{Name:'minecraft:orange_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:orange_shulker_box',Properties:{facing:'north'}}"); ++ register(3523, "{Name:'minecraft:orange_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:orange_shulker_box',Properties:{facing:'south'}}"); ++ register(3524, "{Name:'minecraft:orange_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:orange_shulker_box',Properties:{facing:'west'}}"); ++ register(3525, "{Name:'minecraft:orange_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:orange_shulker_box',Properties:{facing:'east'}}"); ++ register(3536, "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'down'}}"); ++ register(3537, "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'up'}}"); ++ register(3538, "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'north'}}"); ++ register(3539, "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'south'}}"); ++ register(3540, "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'west'}}"); ++ register(3541, "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'east'}}"); ++ register(3552, "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'down'}}"); ++ register(3553, "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'up'}}"); ++ register(3554, "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'north'}}"); ++ register(3555, "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'south'}}"); ++ register(3556, "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'west'}}"); ++ register(3557, "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'east'}}"); ++ register(3568, "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'down'}}"); ++ register(3569, "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'up'}}"); ++ register(3570, "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'north'}}"); ++ register(3571, "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'south'}}"); ++ register(3572, "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'west'}}"); ++ register(3573, "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'east'}}"); ++ register(3584, "{Name:'minecraft:lime_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:lime_shulker_box',Properties:{facing:'down'}}"); ++ register(3585, "{Name:'minecraft:lime_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:lime_shulker_box',Properties:{facing:'up'}}"); ++ register(3586, "{Name:'minecraft:lime_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:lime_shulker_box',Properties:{facing:'north'}}"); ++ register(3587, "{Name:'minecraft:lime_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:lime_shulker_box',Properties:{facing:'south'}}"); ++ register(3588, "{Name:'minecraft:lime_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:lime_shulker_box',Properties:{facing:'west'}}"); ++ register(3589, "{Name:'minecraft:lime_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:lime_shulker_box',Properties:{facing:'east'}}"); ++ register(3600, "{Name:'minecraft:pink_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:pink_shulker_box',Properties:{facing:'down'}}"); ++ register(3601, "{Name:'minecraft:pink_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:pink_shulker_box',Properties:{facing:'up'}}"); ++ register(3602, "{Name:'minecraft:pink_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:pink_shulker_box',Properties:{facing:'north'}}"); ++ register(3603, "{Name:'minecraft:pink_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:pink_shulker_box',Properties:{facing:'south'}}"); ++ register(3604, "{Name:'minecraft:pink_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:pink_shulker_box',Properties:{facing:'west'}}"); ++ register(3605, "{Name:'minecraft:pink_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:pink_shulker_box',Properties:{facing:'east'}}"); ++ register(3616, "{Name:'minecraft:gray_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:gray_shulker_box',Properties:{facing:'down'}}"); ++ register(3617, "{Name:'minecraft:gray_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:gray_shulker_box',Properties:{facing:'up'}}"); ++ register(3618, "{Name:'minecraft:gray_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:gray_shulker_box',Properties:{facing:'north'}}"); ++ register(3619, "{Name:'minecraft:gray_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:gray_shulker_box',Properties:{facing:'south'}}"); ++ register(3620, "{Name:'minecraft:gray_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:gray_shulker_box',Properties:{facing:'west'}}"); ++ register(3621, "{Name:'minecraft:gray_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:gray_shulker_box',Properties:{facing:'east'}}"); ++ register(3632, "{Name:'minecraft:light_gray_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:silver_shulker_box',Properties:{facing:'down'}}"); ++ register(3633, "{Name:'minecraft:light_gray_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:silver_shulker_box',Properties:{facing:'up'}}"); ++ register(3634, "{Name:'minecraft:light_gray_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:silver_shulker_box',Properties:{facing:'north'}}"); ++ register(3635, "{Name:'minecraft:light_gray_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:silver_shulker_box',Properties:{facing:'south'}}"); ++ register(3636, "{Name:'minecraft:light_gray_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:silver_shulker_box',Properties:{facing:'west'}}"); ++ register(3637, "{Name:'minecraft:light_gray_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:silver_shulker_box',Properties:{facing:'east'}}"); ++ register(3648, "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'down'}}"); ++ register(3649, "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'up'}}"); ++ register(3650, "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'north'}}"); ++ register(3651, "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'south'}}"); ++ register(3652, "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'west'}}"); ++ register(3653, "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'east'}}"); ++ register(3664, "{Name:'minecraft:purple_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:purple_shulker_box',Properties:{facing:'down'}}"); ++ register(3665, "{Name:'minecraft:purple_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:purple_shulker_box',Properties:{facing:'up'}}"); ++ register(3666, "{Name:'minecraft:purple_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:purple_shulker_box',Properties:{facing:'north'}}"); ++ register(3667, "{Name:'minecraft:purple_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:purple_shulker_box',Properties:{facing:'south'}}"); ++ register(3668, "{Name:'minecraft:purple_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:purple_shulker_box',Properties:{facing:'west'}}"); ++ register(3669, "{Name:'minecraft:purple_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:purple_shulker_box',Properties:{facing:'east'}}"); ++ register(3680, "{Name:'minecraft:blue_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:blue_shulker_box',Properties:{facing:'down'}}"); ++ register(3681, "{Name:'minecraft:blue_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:blue_shulker_box',Properties:{facing:'up'}}"); ++ register(3682, "{Name:'minecraft:blue_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:blue_shulker_box',Properties:{facing:'north'}}"); ++ register(3683, "{Name:'minecraft:blue_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:blue_shulker_box',Properties:{facing:'south'}}"); ++ register(3684, "{Name:'minecraft:blue_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:blue_shulker_box',Properties:{facing:'west'}}"); ++ register(3685, "{Name:'minecraft:blue_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:blue_shulker_box',Properties:{facing:'east'}}"); ++ register(3696, "{Name:'minecraft:brown_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:brown_shulker_box',Properties:{facing:'down'}}"); ++ register(3697, "{Name:'minecraft:brown_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:brown_shulker_box',Properties:{facing:'up'}}"); ++ register(3698, "{Name:'minecraft:brown_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:brown_shulker_box',Properties:{facing:'north'}}"); ++ register(3699, "{Name:'minecraft:brown_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:brown_shulker_box',Properties:{facing:'south'}}"); ++ register(3700, "{Name:'minecraft:brown_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:brown_shulker_box',Properties:{facing:'west'}}"); ++ register(3701, "{Name:'minecraft:brown_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:brown_shulker_box',Properties:{facing:'east'}}"); ++ register(3712, "{Name:'minecraft:green_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:green_shulker_box',Properties:{facing:'down'}}"); ++ register(3713, "{Name:'minecraft:green_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:green_shulker_box',Properties:{facing:'up'}}"); ++ register(3714, "{Name:'minecraft:green_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:green_shulker_box',Properties:{facing:'north'}}"); ++ register(3715, "{Name:'minecraft:green_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:green_shulker_box',Properties:{facing:'south'}}"); ++ register(3716, "{Name:'minecraft:green_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:green_shulker_box',Properties:{facing:'west'}}"); ++ register(3717, "{Name:'minecraft:green_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:green_shulker_box',Properties:{facing:'east'}}"); ++ register(3728, "{Name:'minecraft:red_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:red_shulker_box',Properties:{facing:'down'}}"); ++ register(3729, "{Name:'minecraft:red_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:red_shulker_box',Properties:{facing:'up'}}"); ++ register(3730, "{Name:'minecraft:red_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:red_shulker_box',Properties:{facing:'north'}}"); ++ register(3731, "{Name:'minecraft:red_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:red_shulker_box',Properties:{facing:'south'}}"); ++ register(3732, "{Name:'minecraft:red_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:red_shulker_box',Properties:{facing:'west'}}"); ++ register(3733, "{Name:'minecraft:red_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:red_shulker_box',Properties:{facing:'east'}}"); ++ register(3744, "{Name:'minecraft:black_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:black_shulker_box',Properties:{facing:'down'}}"); ++ register(3745, "{Name:'minecraft:black_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:black_shulker_box',Properties:{facing:'up'}}"); ++ register(3746, "{Name:'minecraft:black_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:black_shulker_box',Properties:{facing:'north'}}"); ++ register(3747, "{Name:'minecraft:black_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:black_shulker_box',Properties:{facing:'south'}}"); ++ register(3748, "{Name:'minecraft:black_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:black_shulker_box',Properties:{facing:'west'}}"); ++ register(3749, "{Name:'minecraft:black_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:black_shulker_box',Properties:{facing:'east'}}"); ++ register(3760, "{Name:'minecraft:white_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:white_glazed_terracotta',Properties:{facing:'south'}}"); ++ register(3761, "{Name:'minecraft:white_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:white_glazed_terracotta',Properties:{facing:'west'}}"); ++ register(3762, "{Name:'minecraft:white_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:white_glazed_terracotta',Properties:{facing:'north'}}"); ++ register(3763, "{Name:'minecraft:white_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:white_glazed_terracotta',Properties:{facing:'east'}}"); ++ register(3776, "{Name:'minecraft:orange_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:orange_glazed_terracotta',Properties:{facing:'south'}}"); ++ register(3777, "{Name:'minecraft:orange_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:orange_glazed_terracotta',Properties:{facing:'west'}}"); ++ register(3778, "{Name:'minecraft:orange_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:orange_glazed_terracotta',Properties:{facing:'north'}}"); ++ register(3779, "{Name:'minecraft:orange_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:orange_glazed_terracotta',Properties:{facing:'east'}}"); ++ register(3792, "{Name:'minecraft:magenta_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:magenta_glazed_terracotta',Properties:{facing:'south'}}"); ++ register(3793, "{Name:'minecraft:magenta_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:magenta_glazed_terracotta',Properties:{facing:'west'}}"); ++ register(3794, "{Name:'minecraft:magenta_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:magenta_glazed_terracotta',Properties:{facing:'north'}}"); ++ register(3795, "{Name:'minecraft:magenta_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:magenta_glazed_terracotta',Properties:{facing:'east'}}"); ++ register(3808, "{Name:'minecraft:light_blue_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:light_blue_glazed_terracotta',Properties:{facing:'south'}}"); ++ register(3809, "{Name:'minecraft:light_blue_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:light_blue_glazed_terracotta',Properties:{facing:'west'}}"); ++ register(3810, "{Name:'minecraft:light_blue_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:light_blue_glazed_terracotta',Properties:{facing:'north'}}"); ++ register(3811, "{Name:'minecraft:light_blue_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:light_blue_glazed_terracotta',Properties:{facing:'east'}}"); ++ register(3824, "{Name:'minecraft:yellow_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:yellow_glazed_terracotta',Properties:{facing:'south'}}"); ++ register(3825, "{Name:'minecraft:yellow_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:yellow_glazed_terracotta',Properties:{facing:'west'}}"); ++ register(3826, "{Name:'minecraft:yellow_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:yellow_glazed_terracotta',Properties:{facing:'north'}}"); ++ register(3827, "{Name:'minecraft:yellow_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:yellow_glazed_terracotta',Properties:{facing:'east'}}"); ++ register(3840, "{Name:'minecraft:lime_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:lime_glazed_terracotta',Properties:{facing:'south'}}"); ++ register(3841, "{Name:'minecraft:lime_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:lime_glazed_terracotta',Properties:{facing:'west'}}"); ++ register(3842, "{Name:'minecraft:lime_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:lime_glazed_terracotta',Properties:{facing:'north'}}"); ++ register(3843, "{Name:'minecraft:lime_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:lime_glazed_terracotta',Properties:{facing:'east'}}"); ++ register(3856, "{Name:'minecraft:pink_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:pink_glazed_terracotta',Properties:{facing:'south'}}"); ++ register(3857, "{Name:'minecraft:pink_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:pink_glazed_terracotta',Properties:{facing:'west'}}"); ++ register(3858, "{Name:'minecraft:pink_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:pink_glazed_terracotta',Properties:{facing:'north'}}"); ++ register(3859, "{Name:'minecraft:pink_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:pink_glazed_terracotta',Properties:{facing:'east'}}"); ++ register(3872, "{Name:'minecraft:gray_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:gray_glazed_terracotta',Properties:{facing:'south'}}"); ++ register(3873, "{Name:'minecraft:gray_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:gray_glazed_terracotta',Properties:{facing:'west'}}"); ++ register(3874, "{Name:'minecraft:gray_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:gray_glazed_terracotta',Properties:{facing:'north'}}"); ++ register(3875, "{Name:'minecraft:gray_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:gray_glazed_terracotta',Properties:{facing:'east'}}"); ++ register(3888, "{Name:'minecraft:light_gray_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:silver_glazed_terracotta',Properties:{facing:'south'}}"); ++ register(3889, "{Name:'minecraft:light_gray_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:silver_glazed_terracotta',Properties:{facing:'west'}}"); ++ register(3890, "{Name:'minecraft:light_gray_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:silver_glazed_terracotta',Properties:{facing:'north'}}"); ++ register(3891, "{Name:'minecraft:light_gray_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:silver_glazed_terracotta',Properties:{facing:'east'}}"); ++ register(3904, "{Name:'minecraft:cyan_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:cyan_glazed_terracotta',Properties:{facing:'south'}}"); ++ register(3905, "{Name:'minecraft:cyan_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:cyan_glazed_terracotta',Properties:{facing:'west'}}"); ++ register(3906, "{Name:'minecraft:cyan_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:cyan_glazed_terracotta',Properties:{facing:'north'}}"); ++ register(3907, "{Name:'minecraft:cyan_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:cyan_glazed_terracotta',Properties:{facing:'east'}}"); ++ register(3920, "{Name:'minecraft:purple_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:purple_glazed_terracotta',Properties:{facing:'south'}}"); ++ register(3921, "{Name:'minecraft:purple_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:purple_glazed_terracotta',Properties:{facing:'west'}}"); ++ register(3922, "{Name:'minecraft:purple_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:purple_glazed_terracotta',Properties:{facing:'north'}}"); ++ register(3923, "{Name:'minecraft:purple_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:purple_glazed_terracotta',Properties:{facing:'east'}}"); ++ register(3936, "{Name:'minecraft:blue_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:blue_glazed_terracotta',Properties:{facing:'south'}}"); ++ register(3937, "{Name:'minecraft:blue_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:blue_glazed_terracotta',Properties:{facing:'west'}}"); ++ register(3938, "{Name:'minecraft:blue_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:blue_glazed_terracotta',Properties:{facing:'north'}}"); ++ register(3939, "{Name:'minecraft:blue_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:blue_glazed_terracotta',Properties:{facing:'east'}}"); ++ register(3952, "{Name:'minecraft:brown_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:brown_glazed_terracotta',Properties:{facing:'south'}}"); ++ register(3953, "{Name:'minecraft:brown_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:brown_glazed_terracotta',Properties:{facing:'west'}}"); ++ register(3954, "{Name:'minecraft:brown_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:brown_glazed_terracotta',Properties:{facing:'north'}}"); ++ register(3955, "{Name:'minecraft:brown_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:brown_glazed_terracotta',Properties:{facing:'east'}}"); ++ register(3968, "{Name:'minecraft:green_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:green_glazed_terracotta',Properties:{facing:'south'}}"); ++ register(3969, "{Name:'minecraft:green_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:green_glazed_terracotta',Properties:{facing:'west'}}"); ++ register(3970, "{Name:'minecraft:green_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:green_glazed_terracotta',Properties:{facing:'north'}}"); ++ register(3971, "{Name:'minecraft:green_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:green_glazed_terracotta',Properties:{facing:'east'}}"); ++ register(3984, "{Name:'minecraft:red_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:red_glazed_terracotta',Properties:{facing:'south'}}"); ++ register(3985, "{Name:'minecraft:red_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:red_glazed_terracotta',Properties:{facing:'west'}}"); ++ register(3986, "{Name:'minecraft:red_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:red_glazed_terracotta',Properties:{facing:'north'}}"); ++ register(3987, "{Name:'minecraft:red_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:red_glazed_terracotta',Properties:{facing:'east'}}"); ++ register(4000, "{Name:'minecraft:black_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:black_glazed_terracotta',Properties:{facing:'south'}}"); ++ register(4001, "{Name:'minecraft:black_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:black_glazed_terracotta',Properties:{facing:'west'}}"); ++ register(4002, "{Name:'minecraft:black_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:black_glazed_terracotta',Properties:{facing:'north'}}"); ++ register(4003, "{Name:'minecraft:black_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:black_glazed_terracotta',Properties:{facing:'east'}}"); ++ register(4016, "{Name:'minecraft:white_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'white'}}"); ++ register(4017, "{Name:'minecraft:orange_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'orange'}}"); ++ register(4018, "{Name:'minecraft:magenta_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'magenta'}}"); ++ register(4019, "{Name:'minecraft:light_blue_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'light_blue'}}"); ++ register(4020, "{Name:'minecraft:yellow_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'yellow'}}"); ++ register(4021, "{Name:'minecraft:lime_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'lime'}}"); ++ register(4022, "{Name:'minecraft:pink_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'pink'}}"); ++ register(4023, "{Name:'minecraft:gray_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'gray'}}"); ++ register(4024, "{Name:'minecraft:light_gray_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'silver'}}"); ++ register(4025, "{Name:'minecraft:cyan_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'cyan'}}"); ++ register(4026, "{Name:'minecraft:purple_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'purple'}}"); ++ register(4027, "{Name:'minecraft:blue_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'blue'}}"); ++ register(4028, "{Name:'minecraft:brown_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'brown'}}"); ++ register(4029, "{Name:'minecraft:green_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'green'}}"); ++ register(4030, "{Name:'minecraft:red_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'red'}}"); ++ register(4031, "{Name:'minecraft:black_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'black'}}"); ++ register(4032, "{Name:'minecraft:white_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'white'}}"); ++ register(4033, "{Name:'minecraft:orange_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'orange'}}"); ++ register(4034, "{Name:'minecraft:magenta_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'magenta'}}"); ++ register(4035, "{Name:'minecraft:light_blue_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'light_blue'}}"); ++ register(4036, "{Name:'minecraft:yellow_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'yellow'}}"); ++ register(4037, "{Name:'minecraft:lime_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'lime'}}"); ++ register(4038, "{Name:'minecraft:pink_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'pink'}}"); ++ register(4039, "{Name:'minecraft:gray_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'gray'}}"); ++ register(4040, "{Name:'minecraft:light_gray_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'silver'}}"); ++ register(4041, "{Name:'minecraft:cyan_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'cyan'}}"); ++ register(4042, "{Name:'minecraft:purple_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'purple'}}"); ++ register(4043, "{Name:'minecraft:blue_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'blue'}}"); ++ register(4044, "{Name:'minecraft:brown_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'brown'}}"); ++ register(4045, "{Name:'minecraft:green_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'green'}}"); ++ register(4046, "{Name:'minecraft:red_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'red'}}"); ++ register(4047, "{Name:'minecraft:black_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'black'}}"); ++ register(4080, "{Name:'minecraft:structure_block',Properties:{mode:'save'}}", "{Name:'minecraft:structure_block',Properties:{mode:'save'}}"); ++ register(4081, "{Name:'minecraft:structure_block',Properties:{mode:'load'}}", "{Name:'minecraft:structure_block',Properties:{mode:'load'}}"); ++ register(4082, "{Name:'minecraft:structure_block',Properties:{mode:'corner'}}", "{Name:'minecraft:structure_block',Properties:{mode:'corner'}}"); ++ register(4083, "{Name:'minecraft:structure_block',Properties:{mode:'data'}}", "{Name:'minecraft:structure_block',Properties:{mode:'data'}}"); ++ finalizeMaps(); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperItemNameV102.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperItemNameV102.java +new file mode 100644 +index 0000000000000000000000000000000000000000..86f6aa3e3fa886976809f350fc5eb16f6a026ed9 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperItemNameV102.java +@@ -0,0 +1,533 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.helpers; ++ ++import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; ++ ++public final class HelperItemNameV102 { ++ ++ // This class is responsible for mapping the id -> string update in itemstacks and potions ++ ++ private static final Int2ObjectOpenHashMap ITEM_NAMES = new Int2ObjectOpenHashMap() { ++ @Override ++ public String put(final int k, final String o) { ++ final String ret = super.put(k, o); ++ ++ if (ret != null) { ++ throw new IllegalStateException("Mapping already exists for " + k + ": prev: " + ret + ", new: " + o); ++ } ++ ++ return ret; ++ } ++ }; ++ ++ static { ++ ITEM_NAMES.put(0, "minecraft:air"); ++ ITEM_NAMES.put(1, "minecraft:stone"); ++ ITEM_NAMES.put(2, "minecraft:grass"); ++ ITEM_NAMES.put(3, "minecraft:dirt"); ++ ITEM_NAMES.put(4, "minecraft:cobblestone"); ++ ITEM_NAMES.put(5, "minecraft:planks"); ++ ITEM_NAMES.put(6, "minecraft:sapling"); ++ ITEM_NAMES.put(7, "minecraft:bedrock"); ++ ITEM_NAMES.put(8, "minecraft:flowing_water"); ++ ITEM_NAMES.put(9, "minecraft:water"); ++ ITEM_NAMES.put(10, "minecraft:flowing_lava"); ++ ITEM_NAMES.put(11, "minecraft:lava"); ++ ITEM_NAMES.put(12, "minecraft:sand"); ++ ITEM_NAMES.put(13, "minecraft:gravel"); ++ ITEM_NAMES.put(14, "minecraft:gold_ore"); ++ ITEM_NAMES.put(15, "minecraft:iron_ore"); ++ ITEM_NAMES.put(16, "minecraft:coal_ore"); ++ ITEM_NAMES.put(17, "minecraft:log"); ++ ITEM_NAMES.put(18, "minecraft:leaves"); ++ ITEM_NAMES.put(19, "minecraft:sponge"); ++ ITEM_NAMES.put(20, "minecraft:glass"); ++ ITEM_NAMES.put(21, "minecraft:lapis_ore"); ++ ITEM_NAMES.put(22, "minecraft:lapis_block"); ++ ITEM_NAMES.put(23, "minecraft:dispenser"); ++ ITEM_NAMES.put(24, "minecraft:sandstone"); ++ ITEM_NAMES.put(25, "minecraft:noteblock"); ++ ITEM_NAMES.put(27, "minecraft:golden_rail"); ++ ITEM_NAMES.put(28, "minecraft:detector_rail"); ++ ITEM_NAMES.put(29, "minecraft:sticky_piston"); ++ ITEM_NAMES.put(30, "minecraft:web"); ++ ITEM_NAMES.put(31, "minecraft:tallgrass"); ++ ITEM_NAMES.put(32, "minecraft:deadbush"); ++ ITEM_NAMES.put(33, "minecraft:piston"); ++ ITEM_NAMES.put(35, "minecraft:wool"); ++ ITEM_NAMES.put(37, "minecraft:yellow_flower"); ++ ITEM_NAMES.put(38, "minecraft:red_flower"); ++ ITEM_NAMES.put(39, "minecraft:brown_mushroom"); ++ ITEM_NAMES.put(40, "minecraft:red_mushroom"); ++ ITEM_NAMES.put(41, "minecraft:gold_block"); ++ ITEM_NAMES.put(42, "minecraft:iron_block"); ++ ITEM_NAMES.put(43, "minecraft:double_stone_slab"); ++ ITEM_NAMES.put(44, "minecraft:stone_slab"); ++ ITEM_NAMES.put(45, "minecraft:brick_block"); ++ ITEM_NAMES.put(46, "minecraft:tnt"); ++ ITEM_NAMES.put(47, "minecraft:bookshelf"); ++ ITEM_NAMES.put(48, "minecraft:mossy_cobblestone"); ++ ITEM_NAMES.put(49, "minecraft:obsidian"); ++ ITEM_NAMES.put(50, "minecraft:torch"); ++ ITEM_NAMES.put(51, "minecraft:fire"); ++ ITEM_NAMES.put(52, "minecraft:mob_spawner"); ++ ITEM_NAMES.put(53, "minecraft:oak_stairs"); ++ ITEM_NAMES.put(54, "minecraft:chest"); ++ ITEM_NAMES.put(56, "minecraft:diamond_ore"); ++ ITEM_NAMES.put(57, "minecraft:diamond_block"); ++ ITEM_NAMES.put(58, "minecraft:crafting_table"); ++ ITEM_NAMES.put(60, "minecraft:farmland"); ++ ITEM_NAMES.put(61, "minecraft:furnace"); ++ ITEM_NAMES.put(62, "minecraft:lit_furnace"); ++ ITEM_NAMES.put(65, "minecraft:ladder"); ++ ITEM_NAMES.put(66, "minecraft:rail"); ++ ITEM_NAMES.put(67, "minecraft:stone_stairs"); ++ ITEM_NAMES.put(69, "minecraft:lever"); ++ ITEM_NAMES.put(70, "minecraft:stone_pressure_plate"); ++ ITEM_NAMES.put(72, "minecraft:wooden_pressure_plate"); ++ ITEM_NAMES.put(73, "minecraft:redstone_ore"); ++ ITEM_NAMES.put(76, "minecraft:redstone_torch"); ++ ITEM_NAMES.put(77, "minecraft:stone_button"); ++ ITEM_NAMES.put(78, "minecraft:snow_layer"); ++ ITEM_NAMES.put(79, "minecraft:ice"); ++ ITEM_NAMES.put(80, "minecraft:snow"); ++ ITEM_NAMES.put(81, "minecraft:cactus"); ++ ITEM_NAMES.put(82, "minecraft:clay"); ++ ITEM_NAMES.put(84, "minecraft:jukebox"); ++ ITEM_NAMES.put(85, "minecraft:fence"); ++ ITEM_NAMES.put(86, "minecraft:pumpkin"); ++ ITEM_NAMES.put(87, "minecraft:netherrack"); ++ ITEM_NAMES.put(88, "minecraft:soul_sand"); ++ ITEM_NAMES.put(89, "minecraft:glowstone"); ++ ITEM_NAMES.put(90, "minecraft:portal"); ++ ITEM_NAMES.put(91, "minecraft:lit_pumpkin"); ++ ITEM_NAMES.put(95, "minecraft:stained_glass"); ++ ITEM_NAMES.put(96, "minecraft:trapdoor"); ++ ITEM_NAMES.put(97, "minecraft:monster_egg"); ++ ITEM_NAMES.put(98, "minecraft:stonebrick"); ++ ITEM_NAMES.put(99, "minecraft:brown_mushroom_block"); ++ ITEM_NAMES.put(100, "minecraft:red_mushroom_block"); ++ ITEM_NAMES.put(101, "minecraft:iron_bars"); ++ ITEM_NAMES.put(102, "minecraft:glass_pane"); ++ ITEM_NAMES.put(103, "minecraft:melon_block"); ++ ITEM_NAMES.put(106, "minecraft:vine"); ++ ITEM_NAMES.put(107, "minecraft:fence_gate"); ++ ITEM_NAMES.put(108, "minecraft:brick_stairs"); ++ ITEM_NAMES.put(109, "minecraft:stone_brick_stairs"); ++ ITEM_NAMES.put(110, "minecraft:mycelium"); ++ ITEM_NAMES.put(111, "minecraft:waterlily"); ++ ITEM_NAMES.put(112, "minecraft:nether_brick"); ++ ITEM_NAMES.put(113, "minecraft:nether_brick_fence"); ++ ITEM_NAMES.put(114, "minecraft:nether_brick_stairs"); ++ ITEM_NAMES.put(116, "minecraft:enchanting_table"); ++ ITEM_NAMES.put(119, "minecraft:end_portal"); ++ ITEM_NAMES.put(120, "minecraft:end_portal_frame"); ++ ITEM_NAMES.put(121, "minecraft:end_stone"); ++ ITEM_NAMES.put(122, "minecraft:dragon_egg"); ++ ITEM_NAMES.put(123, "minecraft:redstone_lamp"); ++ ITEM_NAMES.put(125, "minecraft:double_wooden_slab"); ++ ITEM_NAMES.put(126, "minecraft:wooden_slab"); ++ ITEM_NAMES.put(127, "minecraft:cocoa"); ++ ITEM_NAMES.put(128, "minecraft:sandstone_stairs"); ++ ITEM_NAMES.put(129, "minecraft:emerald_ore"); ++ ITEM_NAMES.put(130, "minecraft:ender_chest"); ++ ITEM_NAMES.put(131, "minecraft:tripwire_hook"); ++ ITEM_NAMES.put(133, "minecraft:emerald_block"); ++ ITEM_NAMES.put(134, "minecraft:spruce_stairs"); ++ ITEM_NAMES.put(135, "minecraft:birch_stairs"); ++ ITEM_NAMES.put(136, "minecraft:jungle_stairs"); ++ ITEM_NAMES.put(137, "minecraft:command_block"); ++ ITEM_NAMES.put(138, "minecraft:beacon"); ++ ITEM_NAMES.put(139, "minecraft:cobblestone_wall"); ++ ITEM_NAMES.put(141, "minecraft:carrots"); ++ ITEM_NAMES.put(142, "minecraft:potatoes"); ++ ITEM_NAMES.put(143, "minecraft:wooden_button"); ++ ITEM_NAMES.put(145, "minecraft:anvil"); ++ ITEM_NAMES.put(146, "minecraft:trapped_chest"); ++ ITEM_NAMES.put(147, "minecraft:light_weighted_pressure_plate"); ++ ITEM_NAMES.put(148, "minecraft:heavy_weighted_pressure_plate"); ++ ITEM_NAMES.put(151, "minecraft:daylight_detector"); ++ ITEM_NAMES.put(152, "minecraft:redstone_block"); ++ ITEM_NAMES.put(153, "minecraft:quartz_ore"); ++ ITEM_NAMES.put(154, "minecraft:hopper"); ++ ITEM_NAMES.put(155, "minecraft:quartz_block"); ++ ITEM_NAMES.put(156, "minecraft:quartz_stairs"); ++ ITEM_NAMES.put(157, "minecraft:activator_rail"); ++ ITEM_NAMES.put(158, "minecraft:dropper"); ++ ITEM_NAMES.put(159, "minecraft:stained_hardened_clay"); ++ ITEM_NAMES.put(160, "minecraft:stained_glass_pane"); ++ ITEM_NAMES.put(161, "minecraft:leaves2"); ++ ITEM_NAMES.put(162, "minecraft:log2"); ++ ITEM_NAMES.put(163, "minecraft:acacia_stairs"); ++ ITEM_NAMES.put(164, "minecraft:dark_oak_stairs"); ++ ITEM_NAMES.put(170, "minecraft:hay_block"); ++ ITEM_NAMES.put(171, "minecraft:carpet"); ++ ITEM_NAMES.put(172, "minecraft:hardened_clay"); ++ ITEM_NAMES.put(173, "minecraft:coal_block"); ++ ITEM_NAMES.put(174, "minecraft:packed_ice"); ++ ITEM_NAMES.put(175, "minecraft:double_plant"); ++ ITEM_NAMES.put(256, "minecraft:iron_shovel"); ++ ITEM_NAMES.put(257, "minecraft:iron_pickaxe"); ++ ITEM_NAMES.put(258, "minecraft:iron_axe"); ++ ITEM_NAMES.put(259, "minecraft:flint_and_steel"); ++ ITEM_NAMES.put(260, "minecraft:apple"); ++ ITEM_NAMES.put(261, "minecraft:bow"); ++ ITEM_NAMES.put(262, "minecraft:arrow"); ++ ITEM_NAMES.put(263, "minecraft:coal"); ++ ITEM_NAMES.put(264, "minecraft:diamond"); ++ ITEM_NAMES.put(265, "minecraft:iron_ingot"); ++ ITEM_NAMES.put(266, "minecraft:gold_ingot"); ++ ITEM_NAMES.put(267, "minecraft:iron_sword"); ++ ITEM_NAMES.put(268, "minecraft:wooden_sword"); ++ ITEM_NAMES.put(269, "minecraft:wooden_shovel"); ++ ITEM_NAMES.put(270, "minecraft:wooden_pickaxe"); ++ ITEM_NAMES.put(271, "minecraft:wooden_axe"); ++ ITEM_NAMES.put(272, "minecraft:stone_sword"); ++ ITEM_NAMES.put(273, "minecraft:stone_shovel"); ++ ITEM_NAMES.put(274, "minecraft:stone_pickaxe"); ++ ITEM_NAMES.put(275, "minecraft:stone_axe"); ++ ITEM_NAMES.put(276, "minecraft:diamond_sword"); ++ ITEM_NAMES.put(277, "minecraft:diamond_shovel"); ++ ITEM_NAMES.put(278, "minecraft:diamond_pickaxe"); ++ ITEM_NAMES.put(279, "minecraft:diamond_axe"); ++ ITEM_NAMES.put(280, "minecraft:stick"); ++ ITEM_NAMES.put(281, "minecraft:bowl"); ++ ITEM_NAMES.put(282, "minecraft:mushroom_stew"); ++ ITEM_NAMES.put(283, "minecraft:golden_sword"); ++ ITEM_NAMES.put(284, "minecraft:golden_shovel"); ++ ITEM_NAMES.put(285, "minecraft:golden_pickaxe"); ++ ITEM_NAMES.put(286, "minecraft:golden_axe"); ++ ITEM_NAMES.put(287, "minecraft:string"); ++ ITEM_NAMES.put(288, "minecraft:feather"); ++ ITEM_NAMES.put(289, "minecraft:gunpowder"); ++ ITEM_NAMES.put(290, "minecraft:wooden_hoe"); ++ ITEM_NAMES.put(291, "minecraft:stone_hoe"); ++ ITEM_NAMES.put(292, "minecraft:iron_hoe"); ++ ITEM_NAMES.put(293, "minecraft:diamond_hoe"); ++ ITEM_NAMES.put(294, "minecraft:golden_hoe"); ++ ITEM_NAMES.put(295, "minecraft:wheat_seeds"); ++ ITEM_NAMES.put(296, "minecraft:wheat"); ++ ITEM_NAMES.put(297, "minecraft:bread"); ++ ITEM_NAMES.put(298, "minecraft:leather_helmet"); ++ ITEM_NAMES.put(299, "minecraft:leather_chestplate"); ++ ITEM_NAMES.put(300, "minecraft:leather_leggings"); ++ ITEM_NAMES.put(301, "minecraft:leather_boots"); ++ ITEM_NAMES.put(302, "minecraft:chainmail_helmet"); ++ ITEM_NAMES.put(303, "minecraft:chainmail_chestplate"); ++ ITEM_NAMES.put(304, "minecraft:chainmail_leggings"); ++ ITEM_NAMES.put(305, "minecraft:chainmail_boots"); ++ ITEM_NAMES.put(306, "minecraft:iron_helmet"); ++ ITEM_NAMES.put(307, "minecraft:iron_chestplate"); ++ ITEM_NAMES.put(308, "minecraft:iron_leggings"); ++ ITEM_NAMES.put(309, "minecraft:iron_boots"); ++ ITEM_NAMES.put(310, "minecraft:diamond_helmet"); ++ ITEM_NAMES.put(311, "minecraft:diamond_chestplate"); ++ ITEM_NAMES.put(312, "minecraft:diamond_leggings"); ++ ITEM_NAMES.put(313, "minecraft:diamond_boots"); ++ ITEM_NAMES.put(314, "minecraft:golden_helmet"); ++ ITEM_NAMES.put(315, "minecraft:golden_chestplate"); ++ ITEM_NAMES.put(316, "minecraft:golden_leggings"); ++ ITEM_NAMES.put(317, "minecraft:golden_boots"); ++ ITEM_NAMES.put(318, "minecraft:flint"); ++ ITEM_NAMES.put(319, "minecraft:porkchop"); ++ ITEM_NAMES.put(320, "minecraft:cooked_porkchop"); ++ ITEM_NAMES.put(321, "minecraft:painting"); ++ ITEM_NAMES.put(322, "minecraft:golden_apple"); ++ ITEM_NAMES.put(323, "minecraft:sign"); ++ ITEM_NAMES.put(324, "minecraft:wooden_door"); ++ ITEM_NAMES.put(325, "minecraft:bucket"); ++ ITEM_NAMES.put(326, "minecraft:water_bucket"); ++ ITEM_NAMES.put(327, "minecraft:lava_bucket"); ++ ITEM_NAMES.put(328, "minecraft:minecart"); ++ ITEM_NAMES.put(329, "minecraft:saddle"); ++ ITEM_NAMES.put(330, "minecraft:iron_door"); ++ ITEM_NAMES.put(331, "minecraft:redstone"); ++ ITEM_NAMES.put(332, "minecraft:snowball"); ++ ITEM_NAMES.put(333, "minecraft:boat"); ++ ITEM_NAMES.put(334, "minecraft:leather"); ++ ITEM_NAMES.put(335, "minecraft:milk_bucket"); ++ ITEM_NAMES.put(336, "minecraft:brick"); ++ ITEM_NAMES.put(337, "minecraft:clay_ball"); ++ ITEM_NAMES.put(338, "minecraft:reeds"); ++ ITEM_NAMES.put(339, "minecraft:paper"); ++ ITEM_NAMES.put(340, "minecraft:book"); ++ ITEM_NAMES.put(341, "minecraft:slime_ball"); ++ ITEM_NAMES.put(342, "minecraft:chest_minecart"); ++ ITEM_NAMES.put(343, "minecraft:furnace_minecart"); ++ ITEM_NAMES.put(344, "minecraft:egg"); ++ ITEM_NAMES.put(345, "minecraft:compass"); ++ ITEM_NAMES.put(346, "minecraft:fishing_rod"); ++ ITEM_NAMES.put(347, "minecraft:clock"); ++ ITEM_NAMES.put(348, "minecraft:glowstone_dust"); ++ ITEM_NAMES.put(349, "minecraft:fish"); ++ ITEM_NAMES.put(350, "minecraft:cooked_fish"); // Fix typo, the game never recognized cooked_fished ++ ITEM_NAMES.put(351, "minecraft:dye"); ++ ITEM_NAMES.put(352, "minecraft:bone"); ++ ITEM_NAMES.put(353, "minecraft:sugar"); ++ ITEM_NAMES.put(354, "minecraft:cake"); ++ ITEM_NAMES.put(355, "minecraft:bed"); ++ ITEM_NAMES.put(356, "minecraft:repeater"); ++ ITEM_NAMES.put(357, "minecraft:cookie"); ++ ITEM_NAMES.put(358, "minecraft:filled_map"); ++ ITEM_NAMES.put(359, "minecraft:shears"); ++ ITEM_NAMES.put(360, "minecraft:melon"); ++ ITEM_NAMES.put(361, "minecraft:pumpkin_seeds"); ++ ITEM_NAMES.put(362, "minecraft:melon_seeds"); ++ ITEM_NAMES.put(363, "minecraft:beef"); ++ ITEM_NAMES.put(364, "minecraft:cooked_beef"); ++ ITEM_NAMES.put(365, "minecraft:chicken"); ++ ITEM_NAMES.put(366, "minecraft:cooked_chicken"); ++ ITEM_NAMES.put(367, "minecraft:rotten_flesh"); ++ ITEM_NAMES.put(368, "minecraft:ender_pearl"); ++ ITEM_NAMES.put(369, "minecraft:blaze_rod"); ++ ITEM_NAMES.put(370, "minecraft:ghast_tear"); ++ ITEM_NAMES.put(371, "minecraft:gold_nugget"); ++ ITEM_NAMES.put(372, "minecraft:nether_wart"); ++ ITEM_NAMES.put(373, "minecraft:potion"); ++ ITEM_NAMES.put(374, "minecraft:glass_bottle"); ++ ITEM_NAMES.put(375, "minecraft:spider_eye"); ++ ITEM_NAMES.put(376, "minecraft:fermented_spider_eye"); ++ ITEM_NAMES.put(377, "minecraft:blaze_powder"); ++ ITEM_NAMES.put(378, "minecraft:magma_cream"); ++ ITEM_NAMES.put(379, "minecraft:brewing_stand"); ++ ITEM_NAMES.put(380, "minecraft:cauldron"); ++ ITEM_NAMES.put(381, "minecraft:ender_eye"); ++ ITEM_NAMES.put(382, "minecraft:speckled_melon"); ++ ITEM_NAMES.put(383, "minecraft:spawn_egg"); ++ ITEM_NAMES.put(384, "minecraft:experience_bottle"); ++ ITEM_NAMES.put(385, "minecraft:fire_charge"); ++ ITEM_NAMES.put(386, "minecraft:writable_book"); ++ ITEM_NAMES.put(387, "minecraft:written_book"); ++ ITEM_NAMES.put(388, "minecraft:emerald"); ++ ITEM_NAMES.put(389, "minecraft:item_frame"); ++ ITEM_NAMES.put(390, "minecraft:flower_pot"); ++ ITEM_NAMES.put(391, "minecraft:carrot"); ++ ITEM_NAMES.put(392, "minecraft:potato"); ++ ITEM_NAMES.put(393, "minecraft:baked_potato"); ++ ITEM_NAMES.put(394, "minecraft:poisonous_potato"); ++ ITEM_NAMES.put(395, "minecraft:map"); ++ ITEM_NAMES.put(396, "minecraft:golden_carrot"); ++ ITEM_NAMES.put(397, "minecraft:skull"); ++ ITEM_NAMES.put(398, "minecraft:carrot_on_a_stick"); ++ ITEM_NAMES.put(399, "minecraft:nether_star"); ++ ITEM_NAMES.put(400, "minecraft:pumpkin_pie"); ++ ITEM_NAMES.put(401, "minecraft:fireworks"); ++ ITEM_NAMES.put(402, "minecraft:firework_charge"); ++ ITEM_NAMES.put(403, "minecraft:enchanted_book"); ++ ITEM_NAMES.put(404, "minecraft:comparator"); ++ ITEM_NAMES.put(405, "minecraft:netherbrick"); ++ ITEM_NAMES.put(406, "minecraft:quartz"); ++ ITEM_NAMES.put(407, "minecraft:tnt_minecart"); ++ ITEM_NAMES.put(408, "minecraft:hopper_minecart"); ++ ITEM_NAMES.put(417, "minecraft:iron_horse_armor"); ++ ITEM_NAMES.put(418, "minecraft:golden_horse_armor"); ++ ITEM_NAMES.put(419, "minecraft:diamond_horse_armor"); ++ ITEM_NAMES.put(420, "minecraft:lead"); ++ ITEM_NAMES.put(421, "minecraft:name_tag"); ++ ITEM_NAMES.put(422, "minecraft:command_block_minecart"); ++ ITEM_NAMES.put(2256, "minecraft:record_13"); ++ ITEM_NAMES.put(2257, "minecraft:record_cat"); ++ ITEM_NAMES.put(2258, "minecraft:record_blocks"); ++ ITEM_NAMES.put(2259, "minecraft:record_chirp"); ++ ITEM_NAMES.put(2260, "minecraft:record_far"); ++ ITEM_NAMES.put(2261, "minecraft:record_mall"); ++ ITEM_NAMES.put(2262, "minecraft:record_mellohi"); ++ ITEM_NAMES.put(2263, "minecraft:record_stal"); ++ ITEM_NAMES.put(2264, "minecraft:record_strad"); ++ ITEM_NAMES.put(2265, "minecraft:record_ward"); ++ ITEM_NAMES.put(2266, "minecraft:record_11"); ++ ITEM_NAMES.put(2267, "minecraft:record_wait"); ++ // https://github.com/starlis/empirecraft/commit/2da59d1901407fc0c135ef0458c0fe9b016570b3 ++ // It's likely that this is a result of old CB/Spigot behavior still writing ids into items as ints. ++ // These ids do not appear to be used by regular MC anyways, so I do not see the harm of porting it here. ++ // Extras can be added if needed ++ String[] extra = new String[4_000]; ++ // EMC start ++ extra[409] = "minecraft:prismarine_shard"; ++ extra[410] = "minecraft:prismarine_crystals"; ++ extra[411] = "minecraft:rabbit"; ++ extra[412] = "minecraft:cooked_rabbit"; ++ extra[413] = "minecraft:rabbit_stew"; ++ extra[414] = "minecraft:rabbit_foot"; ++ extra[415] = "minecraft:rabbit_hide"; ++ extra[416] = "minecraft:armor_stand"; ++ extra[423] = "minecraft:mutton"; ++ extra[424] = "minecraft:cooked_mutton"; ++ extra[425] = "minecraft:banner"; ++ extra[426] = "minecraft:end_crystal"; ++ extra[427] = "minecraft:spruce_door"; ++ extra[428] = "minecraft:birch_door"; ++ extra[429] = "minecraft:jungle_door"; ++ extra[430] = "minecraft:acacia_door"; ++ extra[431] = "minecraft:dark_oak_door"; ++ extra[432] = "minecraft:chorus_fruit"; ++ extra[433] = "minecraft:chorus_fruit_popped"; ++ extra[434] = "minecraft:beetroot"; ++ extra[435] = "minecraft:beetroot_seeds"; ++ extra[436] = "minecraft:beetroot_soup"; ++ extra[437] = "minecraft:dragon_breath"; ++ extra[438] = "minecraft:splash_potion"; ++ extra[439] = "minecraft:spectral_arrow"; ++ extra[440] = "minecraft:tipped_arrow"; ++ extra[441] = "minecraft:lingering_potion"; ++ extra[442] = "minecraft:shield"; ++ extra[443] = "minecraft:elytra"; ++ extra[444] = "minecraft:spruce_boat"; ++ extra[445] = "minecraft:birch_boat"; ++ extra[446] = "minecraft:jungle_boat"; ++ extra[447] = "minecraft:acacia_boat"; ++ extra[448] = "minecraft:dark_oak_boat"; ++ extra[449] = "minecraft:totem_of_undying"; ++ extra[450] = "minecraft:shulker_shell"; ++ extra[452] = "minecraft:iron_nugget"; ++ extra[453] = "minecraft:knowledge_book"; ++ // EMC end ++ ++ // dump extra into map ++ for (int i = 0; i < extra.length; ++i) { ++ if (extra[i] != null) { ++ ITEM_NAMES.put(i, extra[i]); ++ } ++ } ++ } ++ ++ private static final String[] POTION_NAMES = new String[128]; ++ static { ++ POTION_NAMES[0] = "minecraft:water"; ++ POTION_NAMES[1] = "minecraft:regeneration"; ++ POTION_NAMES[2] = "minecraft:swiftness"; ++ POTION_NAMES[3] = "minecraft:fire_resistance"; ++ POTION_NAMES[4] = "minecraft:poison"; ++ POTION_NAMES[5] = "minecraft:healing"; ++ POTION_NAMES[6] = "minecraft:night_vision"; ++ POTION_NAMES[7] = null; ++ POTION_NAMES[8] = "minecraft:weakness"; ++ POTION_NAMES[9] = "minecraft:strength"; ++ POTION_NAMES[10] = "minecraft:slowness"; ++ POTION_NAMES[11] = "minecraft:leaping"; ++ POTION_NAMES[12] = "minecraft:harming"; ++ POTION_NAMES[13] = "minecraft:water_breathing"; ++ POTION_NAMES[14] = "minecraft:invisibility"; ++ POTION_NAMES[15] = null; ++ POTION_NAMES[16] = "minecraft:awkward"; ++ POTION_NAMES[17] = "minecraft:regeneration"; ++ POTION_NAMES[18] = "minecraft:swiftness"; ++ POTION_NAMES[19] = "minecraft:fire_resistance"; ++ POTION_NAMES[20] = "minecraft:poison"; ++ POTION_NAMES[21] = "minecraft:healing"; ++ POTION_NAMES[22] = "minecraft:night_vision"; ++ POTION_NAMES[23] = null; ++ POTION_NAMES[24] = "minecraft:weakness"; ++ POTION_NAMES[25] = "minecraft:strength"; ++ POTION_NAMES[26] = "minecraft:slowness"; ++ POTION_NAMES[27] = "minecraft:leaping"; ++ POTION_NAMES[28] = "minecraft:harming"; ++ POTION_NAMES[29] = "minecraft:water_breathing"; ++ POTION_NAMES[30] = "minecraft:invisibility"; ++ POTION_NAMES[31] = null; ++ POTION_NAMES[32] = "minecraft:thick"; ++ POTION_NAMES[33] = "minecraft:strong_regeneration"; ++ POTION_NAMES[34] = "minecraft:strong_swiftness"; ++ POTION_NAMES[35] = "minecraft:fire_resistance"; ++ POTION_NAMES[36] = "minecraft:strong_poison"; ++ POTION_NAMES[37] = "minecraft:strong_healing"; ++ POTION_NAMES[38] = "minecraft:night_vision"; ++ POTION_NAMES[39] = null; ++ POTION_NAMES[40] = "minecraft:weakness"; ++ POTION_NAMES[41] = "minecraft:strong_strength"; ++ POTION_NAMES[42] = "minecraft:slowness"; ++ POTION_NAMES[43] = "minecraft:strong_leaping"; ++ POTION_NAMES[44] = "minecraft:strong_harming"; ++ POTION_NAMES[45] = "minecraft:water_breathing"; ++ POTION_NAMES[46] = "minecraft:invisibility"; ++ POTION_NAMES[47] = null; ++ POTION_NAMES[48] = null; ++ POTION_NAMES[49] = "minecraft:strong_regeneration"; ++ POTION_NAMES[50] = "minecraft:strong_swiftness"; ++ POTION_NAMES[51] = "minecraft:fire_resistance"; ++ POTION_NAMES[52] = "minecraft:strong_poison"; ++ POTION_NAMES[53] = "minecraft:strong_healing"; ++ POTION_NAMES[54] = "minecraft:night_vision"; ++ POTION_NAMES[55] = null; ++ POTION_NAMES[56] = "minecraft:weakness"; ++ POTION_NAMES[57] = "minecraft:strong_strength"; ++ POTION_NAMES[58] = "minecraft:slowness"; ++ POTION_NAMES[59] = "minecraft:strong_leaping"; ++ POTION_NAMES[60] = "minecraft:strong_harming"; ++ POTION_NAMES[61] = "minecraft:water_breathing"; ++ POTION_NAMES[62] = "minecraft:invisibility"; ++ POTION_NAMES[63] = null; ++ POTION_NAMES[64] = "minecraft:mundane"; ++ POTION_NAMES[65] = "minecraft:long_regeneration"; ++ POTION_NAMES[66] = "minecraft:long_swiftness"; ++ POTION_NAMES[67] = "minecraft:long_fire_resistance"; ++ POTION_NAMES[68] = "minecraft:long_poison"; ++ POTION_NAMES[69] = "minecraft:healing"; ++ POTION_NAMES[70] = "minecraft:long_night_vision"; ++ POTION_NAMES[71] = null; ++ POTION_NAMES[72] = "minecraft:long_weakness"; ++ POTION_NAMES[73] = "minecraft:long_strength"; ++ POTION_NAMES[74] = "minecraft:long_slowness"; ++ POTION_NAMES[75] = "minecraft:long_leaping"; ++ POTION_NAMES[76] = "minecraft:harming"; ++ POTION_NAMES[77] = "minecraft:long_water_breathing"; ++ POTION_NAMES[78] = "minecraft:long_invisibility"; ++ POTION_NAMES[79] = null; ++ POTION_NAMES[80] = "minecraft:awkward"; ++ POTION_NAMES[81] = "minecraft:long_regeneration"; ++ POTION_NAMES[82] = "minecraft:long_swiftness"; ++ POTION_NAMES[83] = "minecraft:long_fire_resistance"; ++ POTION_NAMES[84] = "minecraft:long_poison"; ++ POTION_NAMES[85] = "minecraft:healing"; ++ POTION_NAMES[86] = "minecraft:long_night_vision"; ++ POTION_NAMES[87] = null; ++ POTION_NAMES[88] = "minecraft:long_weakness"; ++ POTION_NAMES[89] = "minecraft:long_strength"; ++ POTION_NAMES[90] = "minecraft:long_slowness"; ++ POTION_NAMES[91] = "minecraft:long_leaping"; ++ POTION_NAMES[92] = "minecraft:harming"; ++ POTION_NAMES[93] = "minecraft:long_water_breathing"; ++ POTION_NAMES[94] = "minecraft:long_invisibility"; ++ POTION_NAMES[95] = null; ++ POTION_NAMES[96] = "minecraft:thick"; ++ POTION_NAMES[97] = "minecraft:regeneration"; ++ POTION_NAMES[98] = "minecraft:swiftness"; ++ POTION_NAMES[99] = "minecraft:long_fire_resistance"; ++ POTION_NAMES[100] = "minecraft:poison"; ++ POTION_NAMES[101] = "minecraft:strong_healing"; ++ POTION_NAMES[102] = "minecraft:long_night_vision"; ++ POTION_NAMES[103] = null; ++ POTION_NAMES[104] = "minecraft:long_weakness"; ++ POTION_NAMES[105] = "minecraft:strength"; ++ POTION_NAMES[106] = "minecraft:long_slowness"; ++ POTION_NAMES[107] = "minecraft:leaping"; ++ POTION_NAMES[108] = "minecraft:strong_harming"; ++ POTION_NAMES[109] = "minecraft:long_water_breathing"; ++ POTION_NAMES[110] = "minecraft:long_invisibility"; ++ POTION_NAMES[111] = null; ++ POTION_NAMES[112] = null; ++ POTION_NAMES[113] = "minecraft:regeneration"; ++ POTION_NAMES[114] = "minecraft:swiftness"; ++ POTION_NAMES[115] = "minecraft:long_fire_resistance"; ++ POTION_NAMES[116] = "minecraft:poison"; ++ POTION_NAMES[117] = "minecraft:strong_healing"; ++ POTION_NAMES[118] = "minecraft:long_night_vision"; ++ POTION_NAMES[119] = null; ++ POTION_NAMES[120] = "minecraft:long_weakness"; ++ POTION_NAMES[121] = "minecraft:strength"; ++ POTION_NAMES[122] = "minecraft:long_slowness"; ++ POTION_NAMES[123] = "minecraft:leaping"; ++ POTION_NAMES[124] = "minecraft:strong_harming"; ++ POTION_NAMES[125] = "minecraft:long_water_breathing"; ++ POTION_NAMES[126] = "minecraft:long_invisibility"; ++ POTION_NAMES[127] = null; ++ } ++ ++ // ret is nullable, you are supposed to log when it does not exist, NOT HIDE IT! ++ public static String getNameFromId(final int id) { ++ return ITEM_NAMES.get(id); ++ } ++ ++ public static String getPotionNameFromId(final short id) { ++ return POTION_NAMES[id & 127]; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperSpawnEggNameV105.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperSpawnEggNameV105.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bcc586cb68148fd960dd685eecce853169a92ed5 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperSpawnEggNameV105.java +@@ -0,0 +1,77 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.helpers; ++ ++public final class HelperSpawnEggNameV105 { ++ ++ private static final String[] ID_TO_STRING = new String[256]; ++ static { ++ ID_TO_STRING[1] = "Item"; ++ ID_TO_STRING[2] = "XPOrb"; ++ ID_TO_STRING[7] = "ThrownEgg"; ++ ID_TO_STRING[8] = "LeashKnot"; ++ ID_TO_STRING[9] = "Painting"; ++ ID_TO_STRING[10] = "Arrow"; ++ ID_TO_STRING[11] = "Snowball"; ++ ID_TO_STRING[12] = "Fireball"; ++ ID_TO_STRING[13] = "SmallFireball"; ++ ID_TO_STRING[14] = "ThrownEnderpearl"; ++ ID_TO_STRING[15] = "EyeOfEnderSignal"; ++ ID_TO_STRING[16] = "ThrownPotion"; ++ ID_TO_STRING[17] = "ThrownExpBottle"; ++ ID_TO_STRING[18] = "ItemFrame"; ++ ID_TO_STRING[19] = "WitherSkull"; ++ ID_TO_STRING[20] = "PrimedTnt"; ++ ID_TO_STRING[21] = "FallingSand"; ++ ID_TO_STRING[22] = "FireworksRocketEntity"; ++ ID_TO_STRING[23] = "TippedArrow"; ++ ID_TO_STRING[24] = "SpectralArrow"; ++ ID_TO_STRING[25] = "ShulkerBullet"; ++ ID_TO_STRING[26] = "DragonFireball"; ++ ID_TO_STRING[30] = "ArmorStand"; ++ ID_TO_STRING[41] = "Boat"; ++ ID_TO_STRING[42] = "MinecartRideable"; ++ ID_TO_STRING[43] = "MinecartChest"; ++ ID_TO_STRING[44] = "MinecartFurnace"; ++ ID_TO_STRING[45] = "MinecartTNT"; ++ ID_TO_STRING[46] = "MinecartHopper"; ++ ID_TO_STRING[47] = "MinecartSpawner"; ++ ID_TO_STRING[40] = "MinecartCommandBlock"; ++ ID_TO_STRING[50] = "Creeper"; ++ ID_TO_STRING[51] = "Skeleton"; ++ ID_TO_STRING[52] = "Spider"; ++ ID_TO_STRING[53] = "Giant"; ++ ID_TO_STRING[54] = "Zombie"; ++ ID_TO_STRING[55] = "Slime"; ++ ID_TO_STRING[56] = "Ghast"; ++ ID_TO_STRING[57] = "PigZombie"; ++ ID_TO_STRING[58] = "Enderman"; ++ ID_TO_STRING[59] = "CaveSpider"; ++ ID_TO_STRING[60] = "Silverfish"; ++ ID_TO_STRING[61] = "Blaze"; ++ ID_TO_STRING[62] = "LavaSlime"; ++ ID_TO_STRING[63] = "EnderDragon"; ++ ID_TO_STRING[64] = "WitherBoss"; ++ ID_TO_STRING[65] = "Bat"; ++ ID_TO_STRING[66] = "Witch"; ++ ID_TO_STRING[67] = "Endermite"; ++ ID_TO_STRING[68] = "Guardian"; ++ ID_TO_STRING[69] = "Shulker"; ++ ID_TO_STRING[90] = "Pig"; ++ ID_TO_STRING[91] = "Sheep"; ++ ID_TO_STRING[92] = "Cow"; ++ ID_TO_STRING[93] = "Chicken"; ++ ID_TO_STRING[94] = "Squid"; ++ ID_TO_STRING[95] = "Wolf"; ++ ID_TO_STRING[96] = "MushroomCow"; ++ ID_TO_STRING[97] = "SnowMan"; ++ ID_TO_STRING[98] = "Ozelot"; ++ ID_TO_STRING[99] = "VillagerGolem"; ++ ID_TO_STRING[100] = "EntityHorse"; ++ ID_TO_STRING[101] = "Rabbit"; ++ ID_TO_STRING[120] = "Villager"; ++ ID_TO_STRING[200] = "EnderCrystal"; ++ } ++ ++ public static String getSpawnNameFromId(final short id) { ++ return ID_TO_STRING[id & 255]; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/RenameHelper.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/RenameHelper.java +new file mode 100644 +index 0000000000000000000000000000000000000000..28dcc6f1425a46c6c76dd16a67aeab0ec72d1d6a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/RenameHelper.java +@@ -0,0 +1,106 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.helpers; ++ ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.function.Function; ++ ++public final class RenameHelper { ++ ++ // assumes no two or more entries are renamed to a single value, otherwise result will be only one of them will win ++ // and there is no defined winner in such a case ++ public static void renameKeys(final MapType data, final Function renamer) { ++ if (data == null) { ++ return; ++ } ++ ++ List newKeys = null; ++ List newValues = null; ++ boolean needsRename = false; ++ for (final String key : data.keys()) { ++ final String renamed = renamer.apply(key); ++ if (renamed != null) { ++ newKeys = new ArrayList<>(); ++ newValues = new ArrayList<>(); ++ newValues.add(data.getGeneric(key)); ++ newKeys.add(renamed); ++ data.remove(key); ++ needsRename = true; ++ break; ++ } ++ } ++ ++ if (!needsRename) { ++ return; ++ } ++ ++ for (final String key : new ArrayList<>(data.keys())) { ++ final String renamed = renamer.apply(key); ++ ++ if (renamed != null) { ++ newValues.add(data.getGeneric(key)); ++ newKeys.add(renamed); ++ data.remove(key); ++ } ++ } ++ ++ // insert new keys ++ for (int i = 0, len = newKeys.size(); i < len; ++i) { ++ final String key = newKeys.get(i); ++ final Object value = newValues.get(i); ++ ++ data.setGeneric(key, value); ++ } ++ } ++ ++ // Clobbers anything in toKey if fromKey exists ++ public static void renameSingle(final MapType data, final String fromKey, final String toKey) { ++ if (data == null) { ++ return; ++ } ++ ++ final Object value = data.getGeneric(fromKey); ++ if (value != null) { ++ data.remove(fromKey); ++ data.setGeneric(toKey, value); ++ } ++ } ++ ++ public static void renameString(final MapType data, final String key, final Function renamer) { ++ if (data == null) { ++ return; ++ } ++ ++ final String value = data.getString(key); ++ if (value == null) { ++ return; ++ } ++ ++ final String renamed = renamer.apply(value); ++ if (renamed == null) { ++ return; ++ } ++ ++ data.setString(key, renamed); ++ } ++ ++ public static void renameListMapItems(final MapType data, final String listPath, final String mapPath, ++ final Function renamer) { ++ if (data == null) { ++ return; ++ } ++ ++ final ListType list = data.getList(listPath, ObjectType.MAP); ++ if (list == null) { ++ return; ++ } ++ ++ for (int i = 0, len = list.size(); i < len; ++i) { ++ RenameHelper.renameString(list.getMap(i), mapPath, renamer); ++ } ++ } ++ ++ private RenameHelper() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemname/ConverterAbstractItemRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemname/ConverterAbstractItemRename.java +new file mode 100644 +index 0000000000000000000000000000000000000000..94569f0ccff0d3a09eafd4ba73572d9db0a0ac5b +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemname/ConverterAbstractItemRename.java +@@ -0,0 +1,18 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.itemname; ++ ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import java.util.function.Function; ++ ++public final class ConverterAbstractItemRename { ++ ++ private ConverterAbstractItemRename() {} ++ ++ public static void register(final int version, final Function renamer) { ++ register(version, 0, renamer); ++ } ++ public static void register(final int version, final int subVersion, final Function renamer) { ++ ConverterAbstractStringValueTypeRename.register(version, subVersion, MCTypeRegistry.ITEM_NAME, renamer); ++ } ++ ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterEnchantmentsRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterEnchantmentsRename.java +new file mode 100644 +index 0000000000000000000000000000000000000000..154ba98bfb587fb4b880e39cd573701b1b049cfe +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterEnchantmentsRename.java +@@ -0,0 +1,35 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.itemstack; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.types.MapType; ++import java.util.function.Function; ++ ++public final class ConverterEnchantmentsRename extends DataConverter, MapType> { ++ ++ private final Function renamer; ++ ++ public ConverterEnchantmentsRename(final int toVersion, final Function renamer) { ++ this(toVersion, 0, renamer); ++ } ++ ++ public ConverterEnchantmentsRename(final int toVersion, final int versionStep, final Function renamer) { ++ super(toVersion, versionStep); ++ ++ this.renamer = renamer; ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType tag = data.getMap("tag"); ++ ++ if (tag == null) { ++ return null; ++ } ++ ++ RenameHelper.renameListMapItems(tag, "Enchantments", "id", this.renamer); ++ RenameHelper.renameListMapItems(tag, "StoredEnchantments", "id", this.renamer); ++ ++ return null; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenItemStack.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenItemStack.java +new file mode 100644 +index 0000000000000000000000000000000000000000..21176b8b96be6cb93d3dc1a74ae9f53f1ad4740c +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenItemStack.java +@@ -0,0 +1,460 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.itemstack; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.Types; ++import com.mojang.logging.LogUtils; ++import org.slf4j.Logger; ++import java.util.Arrays; ++import java.util.HashMap; ++import java.util.HashSet; ++import java.util.Map; ++import java.util.Set; ++ ++public final class ConverterFlattenItemStack extends DataConverter, MapType> { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ // Map of "id.damage" -> "flattened id" ++ private static final Map FLATTEN_MAP = new HashMap<>(); ++ static { ++ FLATTEN_MAP.put("minecraft:stone.0", "minecraft:stone"); ++ FLATTEN_MAP.put("minecraft:stone.1", "minecraft:granite"); ++ FLATTEN_MAP.put("minecraft:stone.2", "minecraft:polished_granite"); ++ FLATTEN_MAP.put("minecraft:stone.3", "minecraft:diorite"); ++ FLATTEN_MAP.put("minecraft:stone.4", "minecraft:polished_diorite"); ++ FLATTEN_MAP.put("minecraft:stone.5", "minecraft:andesite"); ++ FLATTEN_MAP.put("minecraft:stone.6", "minecraft:polished_andesite"); ++ FLATTEN_MAP.put("minecraft:dirt.0", "minecraft:dirt"); ++ FLATTEN_MAP.put("minecraft:dirt.1", "minecraft:coarse_dirt"); ++ FLATTEN_MAP.put("minecraft:dirt.2", "minecraft:podzol"); ++ FLATTEN_MAP.put("minecraft:leaves.0", "minecraft:oak_leaves"); ++ FLATTEN_MAP.put("minecraft:leaves.1", "minecraft:spruce_leaves"); ++ FLATTEN_MAP.put("minecraft:leaves.2", "minecraft:birch_leaves"); ++ FLATTEN_MAP.put("minecraft:leaves.3", "minecraft:jungle_leaves"); ++ FLATTEN_MAP.put("minecraft:leaves2.0", "minecraft:acacia_leaves"); ++ FLATTEN_MAP.put("minecraft:leaves2.1", "minecraft:dark_oak_leaves"); ++ FLATTEN_MAP.put("minecraft:log.0", "minecraft:oak_log"); ++ FLATTEN_MAP.put("minecraft:log.1", "minecraft:spruce_log"); ++ FLATTEN_MAP.put("minecraft:log.2", "minecraft:birch_log"); ++ FLATTEN_MAP.put("minecraft:log.3", "minecraft:jungle_log"); ++ FLATTEN_MAP.put("minecraft:log2.0", "minecraft:acacia_log"); ++ FLATTEN_MAP.put("minecraft:log2.1", "minecraft:dark_oak_log"); ++ FLATTEN_MAP.put("minecraft:sapling.0", "minecraft:oak_sapling"); ++ FLATTEN_MAP.put("minecraft:sapling.1", "minecraft:spruce_sapling"); ++ FLATTEN_MAP.put("minecraft:sapling.2", "minecraft:birch_sapling"); ++ FLATTEN_MAP.put("minecraft:sapling.3", "minecraft:jungle_sapling"); ++ FLATTEN_MAP.put("minecraft:sapling.4", "minecraft:acacia_sapling"); ++ FLATTEN_MAP.put("minecraft:sapling.5", "minecraft:dark_oak_sapling"); ++ FLATTEN_MAP.put("minecraft:planks.0", "minecraft:oak_planks"); ++ FLATTEN_MAP.put("minecraft:planks.1", "minecraft:spruce_planks"); ++ FLATTEN_MAP.put("minecraft:planks.2", "minecraft:birch_planks"); ++ FLATTEN_MAP.put("minecraft:planks.3", "minecraft:jungle_planks"); ++ FLATTEN_MAP.put("minecraft:planks.4", "minecraft:acacia_planks"); ++ FLATTEN_MAP.put("minecraft:planks.5", "minecraft:dark_oak_planks"); ++ FLATTEN_MAP.put("minecraft:sand.0", "minecraft:sand"); ++ FLATTEN_MAP.put("minecraft:sand.1", "minecraft:red_sand"); ++ FLATTEN_MAP.put("minecraft:quartz_block.0", "minecraft:quartz_block"); ++ FLATTEN_MAP.put("minecraft:quartz_block.1", "minecraft:chiseled_quartz_block"); ++ FLATTEN_MAP.put("minecraft:quartz_block.2", "minecraft:quartz_pillar"); ++ FLATTEN_MAP.put("minecraft:anvil.0", "minecraft:anvil"); ++ FLATTEN_MAP.put("minecraft:anvil.1", "minecraft:chipped_anvil"); ++ FLATTEN_MAP.put("minecraft:anvil.2", "minecraft:damaged_anvil"); ++ FLATTEN_MAP.put("minecraft:wool.0", "minecraft:white_wool"); ++ FLATTEN_MAP.put("minecraft:wool.1", "minecraft:orange_wool"); ++ FLATTEN_MAP.put("minecraft:wool.2", "minecraft:magenta_wool"); ++ FLATTEN_MAP.put("minecraft:wool.3", "minecraft:light_blue_wool"); ++ FLATTEN_MAP.put("minecraft:wool.4", "minecraft:yellow_wool"); ++ FLATTEN_MAP.put("minecraft:wool.5", "minecraft:lime_wool"); ++ FLATTEN_MAP.put("minecraft:wool.6", "minecraft:pink_wool"); ++ FLATTEN_MAP.put("minecraft:wool.7", "minecraft:gray_wool"); ++ FLATTEN_MAP.put("minecraft:wool.8", "minecraft:light_gray_wool"); ++ FLATTEN_MAP.put("minecraft:wool.9", "minecraft:cyan_wool"); ++ FLATTEN_MAP.put("minecraft:wool.10", "minecraft:purple_wool"); ++ FLATTEN_MAP.put("minecraft:wool.11", "minecraft:blue_wool"); ++ FLATTEN_MAP.put("minecraft:wool.12", "minecraft:brown_wool"); ++ FLATTEN_MAP.put("minecraft:wool.13", "minecraft:green_wool"); ++ FLATTEN_MAP.put("minecraft:wool.14", "minecraft:red_wool"); ++ FLATTEN_MAP.put("minecraft:wool.15", "minecraft:black_wool"); ++ FLATTEN_MAP.put("minecraft:carpet.0", "minecraft:white_carpet"); ++ FLATTEN_MAP.put("minecraft:carpet.1", "minecraft:orange_carpet"); ++ FLATTEN_MAP.put("minecraft:carpet.2", "minecraft:magenta_carpet"); ++ FLATTEN_MAP.put("minecraft:carpet.3", "minecraft:light_blue_carpet"); ++ FLATTEN_MAP.put("minecraft:carpet.4", "minecraft:yellow_carpet"); ++ FLATTEN_MAP.put("minecraft:carpet.5", "minecraft:lime_carpet"); ++ FLATTEN_MAP.put("minecraft:carpet.6", "minecraft:pink_carpet"); ++ FLATTEN_MAP.put("minecraft:carpet.7", "minecraft:gray_carpet"); ++ FLATTEN_MAP.put("minecraft:carpet.8", "minecraft:light_gray_carpet"); ++ FLATTEN_MAP.put("minecraft:carpet.9", "minecraft:cyan_carpet"); ++ FLATTEN_MAP.put("minecraft:carpet.10", "minecraft:purple_carpet"); ++ FLATTEN_MAP.put("minecraft:carpet.11", "minecraft:blue_carpet"); ++ FLATTEN_MAP.put("minecraft:carpet.12", "minecraft:brown_carpet"); ++ FLATTEN_MAP.put("minecraft:carpet.13", "minecraft:green_carpet"); ++ FLATTEN_MAP.put("minecraft:carpet.14", "minecraft:red_carpet"); ++ FLATTEN_MAP.put("minecraft:carpet.15", "minecraft:black_carpet"); ++ FLATTEN_MAP.put("minecraft:hardened_clay.0", "minecraft:terracotta"); ++ FLATTEN_MAP.put("minecraft:stained_hardened_clay.0", "minecraft:white_terracotta"); ++ FLATTEN_MAP.put("minecraft:stained_hardened_clay.1", "minecraft:orange_terracotta"); ++ FLATTEN_MAP.put("minecraft:stained_hardened_clay.2", "minecraft:magenta_terracotta"); ++ FLATTEN_MAP.put("minecraft:stained_hardened_clay.3", "minecraft:light_blue_terracotta"); ++ FLATTEN_MAP.put("minecraft:stained_hardened_clay.4", "minecraft:yellow_terracotta"); ++ FLATTEN_MAP.put("minecraft:stained_hardened_clay.5", "minecraft:lime_terracotta"); ++ FLATTEN_MAP.put("minecraft:stained_hardened_clay.6", "minecraft:pink_terracotta"); ++ FLATTEN_MAP.put("minecraft:stained_hardened_clay.7", "minecraft:gray_terracotta"); ++ FLATTEN_MAP.put("minecraft:stained_hardened_clay.8", "minecraft:light_gray_terracotta"); ++ FLATTEN_MAP.put("minecraft:stained_hardened_clay.9", "minecraft:cyan_terracotta"); ++ FLATTEN_MAP.put("minecraft:stained_hardened_clay.10", "minecraft:purple_terracotta"); ++ FLATTEN_MAP.put("minecraft:stained_hardened_clay.11", "minecraft:blue_terracotta"); ++ FLATTEN_MAP.put("minecraft:stained_hardened_clay.12", "minecraft:brown_terracotta"); ++ FLATTEN_MAP.put("minecraft:stained_hardened_clay.13", "minecraft:green_terracotta"); ++ FLATTEN_MAP.put("minecraft:stained_hardened_clay.14", "minecraft:red_terracotta"); ++ FLATTEN_MAP.put("minecraft:stained_hardened_clay.15", "minecraft:black_terracotta"); ++ FLATTEN_MAP.put("minecraft:silver_glazed_terracotta.0", "minecraft:light_gray_glazed_terracotta"); ++ FLATTEN_MAP.put("minecraft:stained_glass.0", "minecraft:white_stained_glass"); ++ FLATTEN_MAP.put("minecraft:stained_glass.1", "minecraft:orange_stained_glass"); ++ FLATTEN_MAP.put("minecraft:stained_glass.2", "minecraft:magenta_stained_glass"); ++ FLATTEN_MAP.put("minecraft:stained_glass.3", "minecraft:light_blue_stained_glass"); ++ FLATTEN_MAP.put("minecraft:stained_glass.4", "minecraft:yellow_stained_glass"); ++ FLATTEN_MAP.put("minecraft:stained_glass.5", "minecraft:lime_stained_glass"); ++ FLATTEN_MAP.put("minecraft:stained_glass.6", "minecraft:pink_stained_glass"); ++ FLATTEN_MAP.put("minecraft:stained_glass.7", "minecraft:gray_stained_glass"); ++ FLATTEN_MAP.put("minecraft:stained_glass.8", "minecraft:light_gray_stained_glass"); ++ FLATTEN_MAP.put("minecraft:stained_glass.9", "minecraft:cyan_stained_glass"); ++ FLATTEN_MAP.put("minecraft:stained_glass.10", "minecraft:purple_stained_glass"); ++ FLATTEN_MAP.put("minecraft:stained_glass.11", "minecraft:blue_stained_glass"); ++ FLATTEN_MAP.put("minecraft:stained_glass.12", "minecraft:brown_stained_glass"); ++ FLATTEN_MAP.put("minecraft:stained_glass.13", "minecraft:green_stained_glass"); ++ FLATTEN_MAP.put("minecraft:stained_glass.14", "minecraft:red_stained_glass"); ++ FLATTEN_MAP.put("minecraft:stained_glass.15", "minecraft:black_stained_glass"); ++ FLATTEN_MAP.put("minecraft:stained_glass_pane.0", "minecraft:white_stained_glass_pane"); ++ FLATTEN_MAP.put("minecraft:stained_glass_pane.1", "minecraft:orange_stained_glass_pane"); ++ FLATTEN_MAP.put("minecraft:stained_glass_pane.2", "minecraft:magenta_stained_glass_pane"); ++ FLATTEN_MAP.put("minecraft:stained_glass_pane.3", "minecraft:light_blue_stained_glass_pane"); ++ FLATTEN_MAP.put("minecraft:stained_glass_pane.4", "minecraft:yellow_stained_glass_pane"); ++ FLATTEN_MAP.put("minecraft:stained_glass_pane.5", "minecraft:lime_stained_glass_pane"); ++ FLATTEN_MAP.put("minecraft:stained_glass_pane.6", "minecraft:pink_stained_glass_pane"); ++ FLATTEN_MAP.put("minecraft:stained_glass_pane.7", "minecraft:gray_stained_glass_pane"); ++ FLATTEN_MAP.put("minecraft:stained_glass_pane.8", "minecraft:light_gray_stained_glass_pane"); ++ FLATTEN_MAP.put("minecraft:stained_glass_pane.9", "minecraft:cyan_stained_glass_pane"); ++ FLATTEN_MAP.put("minecraft:stained_glass_pane.10", "minecraft:purple_stained_glass_pane"); ++ FLATTEN_MAP.put("minecraft:stained_glass_pane.11", "minecraft:blue_stained_glass_pane"); ++ FLATTEN_MAP.put("minecraft:stained_glass_pane.12", "minecraft:brown_stained_glass_pane"); ++ FLATTEN_MAP.put("minecraft:stained_glass_pane.13", "minecraft:green_stained_glass_pane"); ++ FLATTEN_MAP.put("minecraft:stained_glass_pane.14", "minecraft:red_stained_glass_pane"); ++ FLATTEN_MAP.put("minecraft:stained_glass_pane.15", "minecraft:black_stained_glass_pane"); ++ FLATTEN_MAP.put("minecraft:prismarine.0", "minecraft:prismarine"); ++ FLATTEN_MAP.put("minecraft:prismarine.1", "minecraft:prismarine_bricks"); ++ FLATTEN_MAP.put("minecraft:prismarine.2", "minecraft:dark_prismarine"); ++ FLATTEN_MAP.put("minecraft:concrete.0", "minecraft:white_concrete"); ++ FLATTEN_MAP.put("minecraft:concrete.1", "minecraft:orange_concrete"); ++ FLATTEN_MAP.put("minecraft:concrete.2", "minecraft:magenta_concrete"); ++ FLATTEN_MAP.put("minecraft:concrete.3", "minecraft:light_blue_concrete"); ++ FLATTEN_MAP.put("minecraft:concrete.4", "minecraft:yellow_concrete"); ++ FLATTEN_MAP.put("minecraft:concrete.5", "minecraft:lime_concrete"); ++ FLATTEN_MAP.put("minecraft:concrete.6", "minecraft:pink_concrete"); ++ FLATTEN_MAP.put("minecraft:concrete.7", "minecraft:gray_concrete"); ++ FLATTEN_MAP.put("minecraft:concrete.8", "minecraft:light_gray_concrete"); ++ FLATTEN_MAP.put("minecraft:concrete.9", "minecraft:cyan_concrete"); ++ FLATTEN_MAP.put("minecraft:concrete.10", "minecraft:purple_concrete"); ++ FLATTEN_MAP.put("minecraft:concrete.11", "minecraft:blue_concrete"); ++ FLATTEN_MAP.put("minecraft:concrete.12", "minecraft:brown_concrete"); ++ FLATTEN_MAP.put("minecraft:concrete.13", "minecraft:green_concrete"); ++ FLATTEN_MAP.put("minecraft:concrete.14", "minecraft:red_concrete"); ++ FLATTEN_MAP.put("minecraft:concrete.15", "minecraft:black_concrete"); ++ FLATTEN_MAP.put("minecraft:concrete_powder.0", "minecraft:white_concrete_powder"); ++ FLATTEN_MAP.put("minecraft:concrete_powder.1", "minecraft:orange_concrete_powder"); ++ FLATTEN_MAP.put("minecraft:concrete_powder.2", "minecraft:magenta_concrete_powder"); ++ FLATTEN_MAP.put("minecraft:concrete_powder.3", "minecraft:light_blue_concrete_powder"); ++ FLATTEN_MAP.put("minecraft:concrete_powder.4", "minecraft:yellow_concrete_powder"); ++ FLATTEN_MAP.put("minecraft:concrete_powder.5", "minecraft:lime_concrete_powder"); ++ FLATTEN_MAP.put("minecraft:concrete_powder.6", "minecraft:pink_concrete_powder"); ++ FLATTEN_MAP.put("minecraft:concrete_powder.7", "minecraft:gray_concrete_powder"); ++ FLATTEN_MAP.put("minecraft:concrete_powder.8", "minecraft:light_gray_concrete_powder"); ++ FLATTEN_MAP.put("minecraft:concrete_powder.9", "minecraft:cyan_concrete_powder"); ++ FLATTEN_MAP.put("minecraft:concrete_powder.10", "minecraft:purple_concrete_powder"); ++ FLATTEN_MAP.put("minecraft:concrete_powder.11", "minecraft:blue_concrete_powder"); ++ FLATTEN_MAP.put("minecraft:concrete_powder.12", "minecraft:brown_concrete_powder"); ++ FLATTEN_MAP.put("minecraft:concrete_powder.13", "minecraft:green_concrete_powder"); ++ FLATTEN_MAP.put("minecraft:concrete_powder.14", "minecraft:red_concrete_powder"); ++ FLATTEN_MAP.put("minecraft:concrete_powder.15", "minecraft:black_concrete_powder"); ++ FLATTEN_MAP.put("minecraft:cobblestone_wall.0", "minecraft:cobblestone_wall"); ++ FLATTEN_MAP.put("minecraft:cobblestone_wall.1", "minecraft:mossy_cobblestone_wall"); ++ FLATTEN_MAP.put("minecraft:sandstone.0", "minecraft:sandstone"); ++ FLATTEN_MAP.put("minecraft:sandstone.1", "minecraft:chiseled_sandstone"); ++ FLATTEN_MAP.put("minecraft:sandstone.2", "minecraft:cut_sandstone"); ++ FLATTEN_MAP.put("minecraft:red_sandstone.0", "minecraft:red_sandstone"); ++ FLATTEN_MAP.put("minecraft:red_sandstone.1", "minecraft:chiseled_red_sandstone"); ++ FLATTEN_MAP.put("minecraft:red_sandstone.2", "minecraft:cut_red_sandstone"); ++ FLATTEN_MAP.put("minecraft:stonebrick.0", "minecraft:stone_bricks"); ++ FLATTEN_MAP.put("minecraft:stonebrick.1", "minecraft:mossy_stone_bricks"); ++ FLATTEN_MAP.put("minecraft:stonebrick.2", "minecraft:cracked_stone_bricks"); ++ FLATTEN_MAP.put("minecraft:stonebrick.3", "minecraft:chiseled_stone_bricks"); ++ FLATTEN_MAP.put("minecraft:monster_egg.0", "minecraft:infested_stone"); ++ FLATTEN_MAP.put("minecraft:monster_egg.1", "minecraft:infested_cobblestone"); ++ FLATTEN_MAP.put("minecraft:monster_egg.2", "minecraft:infested_stone_bricks"); ++ FLATTEN_MAP.put("minecraft:monster_egg.3", "minecraft:infested_mossy_stone_bricks"); ++ FLATTEN_MAP.put("minecraft:monster_egg.4", "minecraft:infested_cracked_stone_bricks"); ++ FLATTEN_MAP.put("minecraft:monster_egg.5", "minecraft:infested_chiseled_stone_bricks"); ++ FLATTEN_MAP.put("minecraft:yellow_flower.0", "minecraft:dandelion"); ++ FLATTEN_MAP.put("minecraft:red_flower.0", "minecraft:poppy"); ++ FLATTEN_MAP.put("minecraft:red_flower.1", "minecraft:blue_orchid"); ++ FLATTEN_MAP.put("minecraft:red_flower.2", "minecraft:allium"); ++ FLATTEN_MAP.put("minecraft:red_flower.3", "minecraft:azure_bluet"); ++ FLATTEN_MAP.put("minecraft:red_flower.4", "minecraft:red_tulip"); ++ FLATTEN_MAP.put("minecraft:red_flower.5", "minecraft:orange_tulip"); ++ FLATTEN_MAP.put("minecraft:red_flower.6", "minecraft:white_tulip"); ++ FLATTEN_MAP.put("minecraft:red_flower.7", "minecraft:pink_tulip"); ++ FLATTEN_MAP.put("minecraft:red_flower.8", "minecraft:oxeye_daisy"); ++ FLATTEN_MAP.put("minecraft:double_plant.0", "minecraft:sunflower"); ++ FLATTEN_MAP.put("minecraft:double_plant.1", "minecraft:lilac"); ++ FLATTEN_MAP.put("minecraft:double_plant.2", "minecraft:tall_grass"); ++ FLATTEN_MAP.put("minecraft:double_plant.3", "minecraft:large_fern"); ++ FLATTEN_MAP.put("minecraft:double_plant.4", "minecraft:rose_bush"); ++ FLATTEN_MAP.put("minecraft:double_plant.5", "minecraft:peony"); ++ FLATTEN_MAP.put("minecraft:deadbush.0", "minecraft:dead_bush"); ++ FLATTEN_MAP.put("minecraft:tallgrass.0", "minecraft:dead_bush"); ++ FLATTEN_MAP.put("minecraft:tallgrass.1", "minecraft:grass"); ++ FLATTEN_MAP.put("minecraft:tallgrass.2", "minecraft:fern"); ++ FLATTEN_MAP.put("minecraft:sponge.0", "minecraft:sponge"); ++ FLATTEN_MAP.put("minecraft:sponge.1", "minecraft:wet_sponge"); ++ FLATTEN_MAP.put("minecraft:purpur_slab.0", "minecraft:purpur_slab"); ++ FLATTEN_MAP.put("minecraft:stone_slab.0", "minecraft:stone_slab"); ++ FLATTEN_MAP.put("minecraft:stone_slab.1", "minecraft:sandstone_slab"); ++ FLATTEN_MAP.put("minecraft:stone_slab.2", "minecraft:petrified_oak_slab"); ++ FLATTEN_MAP.put("minecraft:stone_slab.3", "minecraft:cobblestone_slab"); ++ FLATTEN_MAP.put("minecraft:stone_slab.4", "minecraft:brick_slab"); ++ FLATTEN_MAP.put("minecraft:stone_slab.5", "minecraft:stone_brick_slab"); ++ FLATTEN_MAP.put("minecraft:stone_slab.6", "minecraft:nether_brick_slab"); ++ FLATTEN_MAP.put("minecraft:stone_slab.7", "minecraft:quartz_slab"); ++ FLATTEN_MAP.put("minecraft:stone_slab2.0", "minecraft:red_sandstone_slab"); ++ FLATTEN_MAP.put("minecraft:wooden_slab.0", "minecraft:oak_slab"); ++ FLATTEN_MAP.put("minecraft:wooden_slab.1", "minecraft:spruce_slab"); ++ FLATTEN_MAP.put("minecraft:wooden_slab.2", "minecraft:birch_slab"); ++ FLATTEN_MAP.put("minecraft:wooden_slab.3", "minecraft:jungle_slab"); ++ FLATTEN_MAP.put("minecraft:wooden_slab.4", "minecraft:acacia_slab"); ++ FLATTEN_MAP.put("minecraft:wooden_slab.5", "minecraft:dark_oak_slab"); ++ FLATTEN_MAP.put("minecraft:coal.0", "minecraft:coal"); ++ FLATTEN_MAP.put("minecraft:coal.1", "minecraft:charcoal"); ++ FLATTEN_MAP.put("minecraft:fish.0", "minecraft:cod"); ++ FLATTEN_MAP.put("minecraft:fish.1", "minecraft:salmon"); ++ FLATTEN_MAP.put("minecraft:fish.2", "minecraft:clownfish"); ++ FLATTEN_MAP.put("minecraft:fish.3", "minecraft:pufferfish"); ++ FLATTEN_MAP.put("minecraft:cooked_fish.0", "minecraft:cooked_cod"); ++ FLATTEN_MAP.put("minecraft:cooked_fish.1", "minecraft:cooked_salmon"); ++ FLATTEN_MAP.put("minecraft:skull.0", "minecraft:skeleton_skull"); ++ FLATTEN_MAP.put("minecraft:skull.1", "minecraft:wither_skeleton_skull"); ++ FLATTEN_MAP.put("minecraft:skull.2", "minecraft:zombie_head"); ++ FLATTEN_MAP.put("minecraft:skull.3", "minecraft:player_head"); ++ FLATTEN_MAP.put("minecraft:skull.4", "minecraft:creeper_head"); ++ FLATTEN_MAP.put("minecraft:skull.5", "minecraft:dragon_head"); ++ FLATTEN_MAP.put("minecraft:golden_apple.0", "minecraft:golden_apple"); ++ FLATTEN_MAP.put("minecraft:golden_apple.1", "minecraft:enchanted_golden_apple"); ++ FLATTEN_MAP.put("minecraft:fireworks.0", "minecraft:firework_rocket"); ++ FLATTEN_MAP.put("minecraft:firework_charge.0", "minecraft:firework_star"); ++ FLATTEN_MAP.put("minecraft:dye.0", "minecraft:ink_sac"); ++ FLATTEN_MAP.put("minecraft:dye.1", "minecraft:rose_red"); ++ FLATTEN_MAP.put("minecraft:dye.2", "minecraft:cactus_green"); ++ FLATTEN_MAP.put("minecraft:dye.3", "minecraft:cocoa_beans"); ++ FLATTEN_MAP.put("minecraft:dye.4", "minecraft:lapis_lazuli"); ++ FLATTEN_MAP.put("minecraft:dye.5", "minecraft:purple_dye"); ++ FLATTEN_MAP.put("minecraft:dye.6", "minecraft:cyan_dye"); ++ FLATTEN_MAP.put("minecraft:dye.7", "minecraft:light_gray_dye"); ++ FLATTEN_MAP.put("minecraft:dye.8", "minecraft:gray_dye"); ++ FLATTEN_MAP.put("minecraft:dye.9", "minecraft:pink_dye"); ++ FLATTEN_MAP.put("minecraft:dye.10", "minecraft:lime_dye"); ++ FLATTEN_MAP.put("minecraft:dye.11", "minecraft:dandelion_yellow"); ++ FLATTEN_MAP.put("minecraft:dye.12", "minecraft:light_blue_dye"); ++ FLATTEN_MAP.put("minecraft:dye.13", "minecraft:magenta_dye"); ++ FLATTEN_MAP.put("minecraft:dye.14", "minecraft:orange_dye"); ++ FLATTEN_MAP.put("minecraft:dye.15", "minecraft:bone_meal"); ++ FLATTEN_MAP.put("minecraft:silver_shulker_box.0", "minecraft:light_gray_shulker_box"); ++ FLATTEN_MAP.put("minecraft:fence.0", "minecraft:oak_fence"); ++ FLATTEN_MAP.put("minecraft:fence_gate.0", "minecraft:oak_fence_gate"); ++ FLATTEN_MAP.put("minecraft:wooden_door.0", "minecraft:oak_door"); ++ FLATTEN_MAP.put("minecraft:boat.0", "minecraft:oak_boat"); ++ FLATTEN_MAP.put("minecraft:lit_pumpkin.0", "minecraft:jack_o_lantern"); ++ FLATTEN_MAP.put("minecraft:pumpkin.0", "minecraft:carved_pumpkin"); ++ FLATTEN_MAP.put("minecraft:trapdoor.0", "minecraft:oak_trapdoor"); ++ FLATTEN_MAP.put("minecraft:nether_brick.0", "minecraft:nether_bricks"); ++ FLATTEN_MAP.put("minecraft:red_nether_brick.0", "minecraft:red_nether_bricks"); ++ FLATTEN_MAP.put("minecraft:netherbrick.0", "minecraft:nether_brick"); ++ FLATTEN_MAP.put("minecraft:wooden_button.0", "minecraft:oak_button"); ++ FLATTEN_MAP.put("minecraft:wooden_pressure_plate.0", "minecraft:oak_pressure_plate"); ++ FLATTEN_MAP.put("minecraft:noteblock.0", "minecraft:note_block"); ++ FLATTEN_MAP.put("minecraft:bed.0", "minecraft:white_bed"); ++ FLATTEN_MAP.put("minecraft:bed.1", "minecraft:orange_bed"); ++ FLATTEN_MAP.put("minecraft:bed.2", "minecraft:magenta_bed"); ++ FLATTEN_MAP.put("minecraft:bed.3", "minecraft:light_blue_bed"); ++ FLATTEN_MAP.put("minecraft:bed.4", "minecraft:yellow_bed"); ++ FLATTEN_MAP.put("minecraft:bed.5", "minecraft:lime_bed"); ++ FLATTEN_MAP.put("minecraft:bed.6", "minecraft:pink_bed"); ++ FLATTEN_MAP.put("minecraft:bed.7", "minecraft:gray_bed"); ++ FLATTEN_MAP.put("minecraft:bed.8", "minecraft:light_gray_bed"); ++ FLATTEN_MAP.put("minecraft:bed.9", "minecraft:cyan_bed"); ++ FLATTEN_MAP.put("minecraft:bed.10", "minecraft:purple_bed"); ++ FLATTEN_MAP.put("minecraft:bed.11", "minecraft:blue_bed"); ++ FLATTEN_MAP.put("minecraft:bed.12", "minecraft:brown_bed"); ++ FLATTEN_MAP.put("minecraft:bed.13", "minecraft:green_bed"); ++ FLATTEN_MAP.put("minecraft:bed.14", "minecraft:red_bed"); ++ FLATTEN_MAP.put("minecraft:bed.15", "minecraft:black_bed"); ++ FLATTEN_MAP.put("minecraft:banner.15", "minecraft:white_banner"); ++ FLATTEN_MAP.put("minecraft:banner.14", "minecraft:orange_banner"); ++ FLATTEN_MAP.put("minecraft:banner.13", "minecraft:magenta_banner"); ++ FLATTEN_MAP.put("minecraft:banner.12", "minecraft:light_blue_banner"); ++ FLATTEN_MAP.put("minecraft:banner.11", "minecraft:yellow_banner"); ++ FLATTEN_MAP.put("minecraft:banner.10", "minecraft:lime_banner"); ++ FLATTEN_MAP.put("minecraft:banner.9", "minecraft:pink_banner"); ++ FLATTEN_MAP.put("minecraft:banner.8", "minecraft:gray_banner"); ++ FLATTEN_MAP.put("minecraft:banner.7", "minecraft:light_gray_banner"); ++ FLATTEN_MAP.put("minecraft:banner.6", "minecraft:cyan_banner"); ++ FLATTEN_MAP.put("minecraft:banner.5", "minecraft:purple_banner"); ++ FLATTEN_MAP.put("minecraft:banner.4", "minecraft:blue_banner"); ++ FLATTEN_MAP.put("minecraft:banner.3", "minecraft:brown_banner"); ++ FLATTEN_MAP.put("minecraft:banner.2", "minecraft:green_banner"); ++ FLATTEN_MAP.put("minecraft:banner.1", "minecraft:red_banner"); ++ FLATTEN_MAP.put("minecraft:banner.0", "minecraft:black_banner"); ++ FLATTEN_MAP.put("minecraft:grass.0", "minecraft:grass_block"); ++ FLATTEN_MAP.put("minecraft:brick_block.0", "minecraft:bricks"); ++ FLATTEN_MAP.put("minecraft:end_bricks.0", "minecraft:end_stone_bricks"); ++ FLATTEN_MAP.put("minecraft:golden_rail.0", "minecraft:powered_rail"); ++ FLATTEN_MAP.put("minecraft:magma.0", "minecraft:magma_block"); ++ FLATTEN_MAP.put("minecraft:quartz_ore.0", "minecraft:nether_quartz_ore"); ++ FLATTEN_MAP.put("minecraft:reeds.0", "minecraft:sugar_cane"); ++ FLATTEN_MAP.put("minecraft:slime.0", "minecraft:slime_block"); ++ FLATTEN_MAP.put("minecraft:stone_stairs.0", "minecraft:cobblestone_stairs"); ++ FLATTEN_MAP.put("minecraft:waterlily.0", "minecraft:lily_pad"); ++ FLATTEN_MAP.put("minecraft:web.0", "minecraft:cobweb"); ++ FLATTEN_MAP.put("minecraft:snow.0", "minecraft:snow_block"); ++ FLATTEN_MAP.put("minecraft:snow_layer.0", "minecraft:snow"); ++ FLATTEN_MAP.put("minecraft:record_11.0", "minecraft:music_disc_11"); ++ FLATTEN_MAP.put("minecraft:record_13.0", "minecraft:music_disc_13"); ++ FLATTEN_MAP.put("minecraft:record_blocks.0", "minecraft:music_disc_blocks"); ++ FLATTEN_MAP.put("minecraft:record_cat.0", "minecraft:music_disc_cat"); ++ FLATTEN_MAP.put("minecraft:record_chirp.0", "minecraft:music_disc_chirp"); ++ FLATTEN_MAP.put("minecraft:record_far.0", "minecraft:music_disc_far"); ++ FLATTEN_MAP.put("minecraft:record_mall.0", "minecraft:music_disc_mall"); ++ FLATTEN_MAP.put("minecraft:record_mellohi.0", "minecraft:music_disc_mellohi"); ++ FLATTEN_MAP.put("minecraft:record_stal.0", "minecraft:music_disc_stal"); ++ FLATTEN_MAP.put("minecraft:record_strad.0", "minecraft:music_disc_strad"); ++ FLATTEN_MAP.put("minecraft:record_wait.0", "minecraft:music_disc_wait"); ++ FLATTEN_MAP.put("minecraft:record_ward.0", "minecraft:music_disc_ward"); ++ } ++ ++ // maps out ids requiring flattening ++ private static final Set IDS_REQUIRING_FLATTENING = new HashSet<>(); ++ static { ++ for (final String key : FLATTEN_MAP.keySet()) { ++ IDS_REQUIRING_FLATTENING.add(key.substring(0, key.indexOf('.'))); ++ } ++ } ++ ++ // Damage tag is moved from the ItemStack base tag to the ItemStack tag, and we only want to migrate that ++ // for items that actually require it for damage purposes (Remember, old damage was used to differentiate item types) ++ // It should be noted that this ID set should not be included in the flattening map, because damage for these items ++ // is actual damage and not a subtype specifier ++ private static final Set ITEMS_WITH_DAMAGE = new HashSet<>(Arrays.asList( ++ "minecraft:bow", ++ "minecraft:carrot_on_a_stick", ++ "minecraft:chainmail_boots", ++ "minecraft:chainmail_chestplate", ++ "minecraft:chainmail_helmet", ++ "minecraft:chainmail_leggings", ++ "minecraft:diamond_axe", ++ "minecraft:diamond_boots", ++ "minecraft:diamond_chestplate", ++ "minecraft:diamond_helmet", ++ "minecraft:diamond_hoe", ++ "minecraft:diamond_leggings", ++ "minecraft:diamond_pickaxe", ++ "minecraft:diamond_shovel", ++ "minecraft:diamond_sword", ++ "minecraft:elytra", ++ "minecraft:fishing_rod", ++ "minecraft:flint_and_steel", ++ "minecraft:golden_axe", ++ "minecraft:golden_boots", ++ "minecraft:golden_chestplate", ++ "minecraft:golden_helmet", ++ "minecraft:golden_hoe", ++ "minecraft:golden_leggings", ++ "minecraft:golden_pickaxe", ++ "minecraft:golden_shovel", ++ "minecraft:golden_sword", ++ "minecraft:iron_axe", ++ "minecraft:iron_boots", ++ "minecraft:iron_chestplate", ++ "minecraft:iron_helmet", ++ "minecraft:iron_hoe", ++ "minecraft:iron_leggings", ++ "minecraft:iron_pickaxe", ++ "minecraft:iron_shovel", ++ "minecraft:iron_sword", ++ "minecraft:leather_boots", ++ "minecraft:leather_chestplate", ++ "minecraft:leather_helmet", ++ "minecraft:leather_leggings", ++ "minecraft:shears", ++ "minecraft:shield", ++ "minecraft:stone_axe", ++ "minecraft:stone_hoe", ++ "minecraft:stone_pickaxe", ++ "minecraft:stone_shovel", ++ "minecraft:stone_sword", ++ "minecraft:wooden_axe", ++ "minecraft:wooden_hoe", ++ "minecraft:wooden_pickaxe", ++ "minecraft:wooden_shovel", ++ "minecraft:wooden_sword" ++ )); ++ ++ public ConverterFlattenItemStack() { ++ super(MCVersions.V17W47A, 4); ++ } ++ ++ public static String flattenItem(final String oldName, final int data) { ++ if (IDS_REQUIRING_FLATTENING.contains(oldName)) { ++ final String flattened = FLATTEN_MAP.get(oldName + '.' + data); ++ return flattened == null ? FLATTEN_MAP.get(oldName.concat(".0")) : flattened; ++ } else { ++ return null; ++ } ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final String id = data.getString("id"); ++ ++ if (id == null) { ++ return null; ++ } ++ ++ final int damage = data.getInt("Damage"); ++ data.remove("Damage"); ++ ++ if (IDS_REQUIRING_FLATTENING.contains(id)) { ++ String remap = FLATTEN_MAP.get(id + '.' + damage); ++ if (remap == null) { ++ remap = FLATTEN_MAP.get(id.concat(".0")); ++ // this shouldn't be null ++ } ++ if (remap != null) { ++ data.setString("id", remap); ++ } else { ++ LOGGER.warn("Item '" + id + "' requires flattening but found no mapping for it! (ConverterFlattenItemStack)"); ++ } ++ } ++ ++ if (damage != 0 && ITEMS_WITH_DAMAGE.contains(id)) { ++ // migrate damage ++ MapType tag = data.getMap("tag"); ++ if (tag == null) { ++ tag = Types.NBT.createEmptyMap(); ++ data.setMap("tag", tag); ++ } ++ tag.setInt("Damage", damage); ++ } ++ ++ return null; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenSpawnEgg.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenSpawnEgg.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4fa31e40b0a6f571a853299b4e242de921ccbda0 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenSpawnEgg.java +@@ -0,0 +1,87 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.itemstack; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.types.MapType; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class ConverterFlattenSpawnEgg extends DataConverter, MapType> { ++ ++ private static final Map ENTITY_ID_TO_NEW_EGG_ID = new HashMap<>(); ++ static { ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:bat", "minecraft:bat_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:blaze", "minecraft:blaze_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:cave_spider", "minecraft:cave_spider_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:chicken", "minecraft:chicken_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:cow", "minecraft:cow_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:creeper", "minecraft:creeper_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:donkey", "minecraft:donkey_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:elder_guardian", "minecraft:elder_guardian_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:enderman", "minecraft:enderman_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:endermite", "minecraft:endermite_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:evocation_illager", "minecraft:evocation_illager_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:ghast", "minecraft:ghast_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:guardian", "minecraft:guardian_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:ender_dragon", "minecraft:ender_dragon_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:horse", "minecraft:horse_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:husk", "minecraft:husk_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:iron_golem", "minecraft:iron_golem_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:llama", "minecraft:llama_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:magma_cube", "minecraft:magma_cube_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:mooshroom", "minecraft:mooshroom_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:mule", "minecraft:mule_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:ocelot", "minecraft:ocelot_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:pufferfish", "minecraft:pufferfish_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:parrot", "minecraft:parrot_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:pig", "minecraft:pig_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:polar_bear", "minecraft:polar_bear_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:rabbit", "minecraft:rabbit_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:sheep", "minecraft:sheep_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:shulker", "minecraft:shulker_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:silverfish", "minecraft:silverfish_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:skeleton", "minecraft:skeleton_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:skeleton_horse", "minecraft:skeleton_horse_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:slime", "minecraft:slime_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:snow_golem", "minecraft:snow_golem_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:spider", "minecraft:spider_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:squid", "minecraft:squid_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:stray", "minecraft:stray_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:turtle", "minecraft:turtle_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:vex", "minecraft:vex_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:villager", "minecraft:villager_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:vindication_illager", "minecraft:vindication_illager_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:witch", "minecraft:witch_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:wither", "minecraft:wither_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:wither_skeleton", "minecraft:wither_skeleton_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:wolf", "minecraft:wolf_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:zombie", "minecraft:zombie_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:zombie_horse", "minecraft:zombie_horse_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:zombie_pigman", "minecraft:zombie_pigman_spawn_egg"); ++ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:zombie_villager", "minecraft:zombie_villager_spawn_egg"); ++ } ++ ++ public ConverterFlattenSpawnEgg(final int version, final int versionStep) { ++ super(version, versionStep); ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType tag = data.getMap("tag"); ++ if (tag == null) { ++ return null; ++ } ++ ++ final MapType entityTag = tag.getMap("EntityTag"); ++ if (entityTag == null) { ++ return null; ++ } ++ ++ final String id = entityTag.getString("id"); ++ if (id != null) { ++ data.setString("id", ENTITY_ID_TO_NEW_EGG_ID.getOrDefault(id, "minecraft:pig_spawn_egg")); ++ } ++ ++ return null; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterItemStackToDataComponents.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterItemStackToDataComponents.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1354b139f929010bdb090764d6151ac9569df753 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterItemStackToDataComponents.java +@@ -0,0 +1,1244 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.itemstack; ++ ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.minecraft.util.ComponentUtils; ++import ca.spottedleaf.dataconverter.minecraft.versions.V3818; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.TypeUtil; ++import ca.spottedleaf.dataconverter.util.NamespaceUtil; ++import net.minecraft.util.Mth; ++import java.util.Arrays; ++import java.util.HashSet; ++import java.util.Set; ++ ++public final class ConverterItemStackToDataComponents { ++ ++ private static final int TOOLTIP_FLAG_HIDE_ENCHANTMENTS = 1 << 0; ++ private static final int TOOLTIP_FLAG_HIDE_MODIFIERS = 1 << 1; ++ private static final int TOOLTIP_FLAG_HIDE_UNBREAKABLE = 1 << 2; ++ private static final int TOOLTIP_FLAG_HIDE_CAN_DESTROY = 1 << 3; ++ private static final int TOOLTIP_FLAG_HIDE_CAN_PLACE = 1 << 4; ++ private static final int TOOLTIP_FLAG_HIDE_ADDITIONAL = 1 << 5; ++ private static final int TOOLTIP_FLAG_HIDE_DYE = 1 << 6; ++ private static final int TOOLTIP_FLAG_HIDE_UPGRADES = 1 << 7; ++ ++ private static final int DEFAULT_LEATHER_COLOUR = (160 << 16) | (101 << 8) | (64 << 0); // r, g, b ++ ++ private static final String[] BUCKETED_MOB_TAGS = new String[] { ++ "NoAI", ++ "Silent", ++ "NoGravity", ++ "Glowing", ++ "Invulnerable", ++ "Health", ++ "Age", ++ "Variant", ++ "HuntingCooldown", ++ "BucketVariantTag" ++ }; ++ private static final Set BOOLEAN_BLOCK_STATE_PROPERTIES = new HashSet<>( ++ Arrays.asList( ++ "attached", ++ "bottom", ++ "conditional", ++ "disarmed", ++ "drag", ++ "enabled", ++ "extended", ++ "eye", ++ "falling", ++ "hanging", ++ "has_bottle_0", ++ "has_bottle_1", ++ "has_bottle_2", ++ "has_record", ++ "has_book", ++ "inverted", ++ "in_wall", ++ "lit", ++ "locked", ++ "occupied", ++ "open", ++ "persistent", ++ "powered", ++ "short", ++ "signal_fire", ++ "snowy", ++ "triggered", ++ "unstable", ++ "waterlogged", ++ "berries", ++ "bloom", ++ "shrieking", ++ "can_summon", ++ "up", ++ "down", ++ "north", ++ "east", ++ "south", ++ "west", ++ "slot_0_occupied", ++ "slot_1_occupied", ++ "slot_2_occupied", ++ "slot_3_occupied", ++ "slot_4_occupied", ++ "slot_5_occupied", ++ "cracked", ++ "crafting" ++ ) ++ ); ++ private static final String[] MAP_DECORATION_CONVERSION_TABLE = new String[34]; ++ static { ++ MAP_DECORATION_CONVERSION_TABLE[0] = "player"; ++ MAP_DECORATION_CONVERSION_TABLE[1] = "frame"; ++ MAP_DECORATION_CONVERSION_TABLE[2] = "red_marker"; ++ MAP_DECORATION_CONVERSION_TABLE[3] = "blue_marker"; ++ MAP_DECORATION_CONVERSION_TABLE[4] = "target_x"; ++ MAP_DECORATION_CONVERSION_TABLE[5] = "target_point"; ++ MAP_DECORATION_CONVERSION_TABLE[6] = "player_off_map"; ++ MAP_DECORATION_CONVERSION_TABLE[7] = "player_off_limits"; ++ MAP_DECORATION_CONVERSION_TABLE[8] = "mansion"; ++ MAP_DECORATION_CONVERSION_TABLE[9] = "monument"; ++ MAP_DECORATION_CONVERSION_TABLE[10] = "banner_white"; ++ MAP_DECORATION_CONVERSION_TABLE[11] = "banner_orange"; ++ MAP_DECORATION_CONVERSION_TABLE[12] = "banner_magenta"; ++ MAP_DECORATION_CONVERSION_TABLE[13] = "banner_light_blue"; ++ MAP_DECORATION_CONVERSION_TABLE[14] = "banner_yellow"; ++ MAP_DECORATION_CONVERSION_TABLE[15] = "banner_lime"; ++ MAP_DECORATION_CONVERSION_TABLE[16] = "banner_pink"; ++ MAP_DECORATION_CONVERSION_TABLE[17] = "banner_gray"; ++ MAP_DECORATION_CONVERSION_TABLE[18] = "banner_light_gray"; ++ MAP_DECORATION_CONVERSION_TABLE[19] = "banner_cyan"; ++ MAP_DECORATION_CONVERSION_TABLE[20] = "banner_purple"; ++ MAP_DECORATION_CONVERSION_TABLE[21] = "banner_blue"; ++ MAP_DECORATION_CONVERSION_TABLE[22] = "banner_brown"; ++ MAP_DECORATION_CONVERSION_TABLE[23] = "banner_green"; ++ MAP_DECORATION_CONVERSION_TABLE[24] = "banner_red"; ++ MAP_DECORATION_CONVERSION_TABLE[25] = "banner_black"; ++ MAP_DECORATION_CONVERSION_TABLE[26] = "red_x"; ++ MAP_DECORATION_CONVERSION_TABLE[27] = "village_desert"; ++ MAP_DECORATION_CONVERSION_TABLE[28] = "village_plains"; ++ MAP_DECORATION_CONVERSION_TABLE[29] = "village_savanna"; ++ MAP_DECORATION_CONVERSION_TABLE[30] = "village_snowy"; ++ MAP_DECORATION_CONVERSION_TABLE[31] = "village_taiga"; ++ MAP_DECORATION_CONVERSION_TABLE[32] = "jungle_temple"; ++ MAP_DECORATION_CONVERSION_TABLE[33] = "swamp_hut"; ++ } ++ ++ private static String convertMapDecorationId(final int type) { ++ return type >= 0 && type < MAP_DECORATION_CONVERSION_TABLE.length ? MAP_DECORATION_CONVERSION_TABLE[type] : MAP_DECORATION_CONVERSION_TABLE[0]; ++ } ++ ++ private static void convertBlockStateProperties(final MapType properties) { ++ // convert values stored as boolean/integer to string ++ for (final String key : properties.keys()) { ++ final Object value = properties.getGeneric(key); ++ if (value instanceof Number number) { ++ if (BOOLEAN_BLOCK_STATE_PROPERTIES.contains(key)) { ++ properties.setString(key, Boolean.toString(number.byteValue() != (byte)0)); ++ } else { ++ properties.setString(key, number.toString()); ++ } ++ } ++ } ++ } ++ ++ private static void convertTileEntity(final MapType tileEntity, final TransientItemStack transientItem) { ++ final Object lock = tileEntity.getGeneric("Lock"); ++ if (lock != null) { ++ tileEntity.remove("Lock"); ++ transientItem.componentSetGeneric("minecraft:lock", lock); ++ } ++ ++ final Object lootTable = tileEntity.getGeneric("LootTable"); ++ if (lootTable != null) { ++ final MapType containerLoot = tileEntity.getTypeUtil().createEmptyMap(); ++ transientItem.componentSetMap("minecraft:container_loot", containerLoot); ++ ++ containerLoot.setGeneric("loot_table", lootTable); ++ ++ final long seed = tileEntity.getLong("LootTableSeed", 0L); ++ if (seed != 0L) { ++ containerLoot.setLong("seed", seed); ++ } ++ ++ tileEntity.remove("LootTable"); ++ tileEntity.remove("LootTableSeed"); ++ } ++ ++ final String id = NamespaceUtil.correctNamespace(tileEntity.getString("id", "")); ++ ++ switch (id) { ++ case "minecraft:skull": { ++ final Object noteBlockSound = tileEntity.getGeneric("note_block_sound"); ++ if (noteBlockSound != null) { ++ tileEntity.remove("note_block_sound"); ++ transientItem.componentSetGeneric("minecraft:note_block_sound", noteBlockSound); ++ } ++ ++ break; ++ } ++ case "minecraft:decorated_pot": { ++ final Object sherds = tileEntity.getGeneric("sherds"); ++ if (sherds != null) { ++ tileEntity.remove("sherds"); ++ transientItem.componentSetGeneric("minecraft:pot_decorations", sherds); ++ } ++ ++ final Object item = tileEntity.getGeneric("item"); ++ if (item != null) { ++ tileEntity.remove("item"); ++ ++ final ListType container = tileEntity.getTypeUtil().createEmptyList(); ++ transientItem.componentSetList("minecraft:container", container); ++ ++ final MapType wrappedItem = tileEntity.getTypeUtil().createEmptyMap(); ++ container.addMap(wrappedItem); ++ ++ wrappedItem.setInt("slot", 0); ++ wrappedItem.setGeneric("item", item); ++ } ++ ++ break; ++ } ++ case "minecraft:banner": { ++ final Object patterns = tileEntity.getGeneric("patterns"); ++ if (patterns != null) { ++ tileEntity.remove("patterns"); ++ ++ transientItem.componentSetGeneric("minecraft:banner_patterns", patterns); ++ } ++ ++ final Number base = tileEntity.getNumber("Base"); ++ if (base != null) { ++ tileEntity.remove("Base"); ++ ++ transientItem.componentSetString("minecraft:base_color", V3818.getBannerColour(base.intValue())); ++ } ++ ++ break; ++ } ++ ++ case "minecraft:shulker_box": ++ case "minecraft:chest": ++ case "minecraft:trapped_chest": ++ case "minecraft:furnace": ++ case "minecraft:ender_chest": ++ case "minecraft:dispenser": ++ case "minecraft:dropper": ++ case "minecraft:brewing_stand": ++ case "minecraft:hopper": ++ case "minecraft:barrel": ++ case "minecraft:smoker": ++ case "minecraft:blast_furnace": ++ case "minecraft:campfire": ++ case "minecraft:chiseled_bookshelf": ++ case "minecraft:crafter": { ++ final ListType items = tileEntity.getList("Items", ObjectType.MAP); ++ tileEntity.remove("Items"); ++ if (items != null && items.size() > 1) { ++ transientItem.componentSetList("minecraft:container", items); ++ ++ for (int i = 0, len = items.size(); i < len; ++i) { ++ final MapType item = items.getMap(i); ++ final int slot = (int)item.getByte("Slot", (byte)0) & 0xFF; ++ item.remove("Slot"); ++ ++ final MapType wrappedItem = item.getTypeUtil().createEmptyMap(); ++ items.setMap(i, wrappedItem); ++ ++ wrappedItem.setInt("slot", slot); ++ wrappedItem.setMap("item", item); ++ } ++ } ++ ++ break; ++ } ++ ++ case "minecraft:beehive": { ++ final Object bees = tileEntity.getGeneric("bees"); ++ if (bees != null) { ++ tileEntity.remove("bees"); ++ ++ transientItem.componentSetGeneric("minecraft:bees", bees); ++ } ++ break; ++ } ++ } ++ } ++ ++ private static void convertEnchantments(final TransientItemStack transientItem, final TypeUtil type, ++ final String tagKey, final String componentKey, ++ final boolean hideToolTip) { ++ final ListType enchantments = transientItem.tagRemoveList(tagKey, ObjectType.MAP); ++ if (enchantments == null || enchantments.size() == 0) { ++ if (hideToolTip) { ++ final MapType newEnchants = type.createEmptyMap(); ++ transientItem.componentSetMap(componentKey, newEnchants); ++ ++ newEnchants.setMap("levels", type.createEmptyMap()); ++ newEnchants.setBoolean("show_in_tooltip", false); ++ } ++ } else { ++ final MapType newEnchantments = type.createEmptyMap(); ++ ++ for (int i = 0, len = enchantments.size(); i < len; ++i) { ++ final MapType enchantment = enchantments.getMap(i); ++ ++ final String id = enchantment.getString("id"); ++ final Number level = enchantment.getNumber("lvl"); ++ ++ if (id == null || level == null) { ++ continue; ++ } ++ ++ newEnchantments.setInt(id, Mth.clamp(level.intValue(), 0, 0xFF)); ++ } ++ ++ if (!newEnchantments.isEmpty() || hideToolTip) { ++ final MapType newEnchants = type.createEmptyMap(); ++ transientItem.componentSetMap(componentKey, newEnchants); ++ ++ newEnchants.setMap("levels", newEnchantments); ++ if (hideToolTip) { ++ newEnchants.setBoolean("show_in_tooltip", false); ++ } ++ } ++ } ++ ++ if (enchantments != null && enchantments.size() == 0) { ++ transientItem.componentSetBoolean("minecraft:enchantment_glint_override", true); ++ } ++ } ++ ++ private static void convertDisplay(final TransientItemStack transientItem, final TypeUtil type, final int flags) { ++ final MapType display = transientItem.tag.getMap("display"); ++ ++ if (display != null) { ++ final Object name = display.getGeneric("Name"); ++ if (name != null) { ++ display.remove("Name"); ++ ++ transientItem.componentSetGeneric("minecraft:custom_name", name); ++ } ++ ++ final Object lore = display.getGeneric("Lore"); ++ if (lore != null) { ++ display.remove("Lore"); ++ ++ transientItem.componentSetGeneric("minecraft:lore", lore); ++ } ++ } ++ ++ final Number color = display == null ? null : display.getNumber("color"); ++ final boolean hideDye = (flags & TOOLTIP_FLAG_HIDE_DYE) != 0; ++ ++ if (hideDye || color != null) { ++ if (color != null) { ++ display.remove("color"); ++ } ++ ++ final MapType dyedColor = type.createEmptyMap(); ++ transientItem.componentSetMap("minecraft:dyed_color", dyedColor); ++ ++ dyedColor.setInt("rgb", color == null ? DEFAULT_LEATHER_COLOUR : color.intValue()); ++ if (hideDye) { ++ dyedColor.setBoolean("show_in_tooltip", false); ++ } ++ } ++ ++ final Object locName = display == null ? null : display.getGeneric("LocName"); ++ if (locName != null) { ++ display.remove("LocName"); ++ ++ if (locName instanceof String locNameString) { ++ transientItem.componentSetString("minecraft:item_name", ComponentUtils.createTranslatableComponent(locNameString)); ++ } ++ } ++ ++ if (display != null && "minecraft:filled_map".equals(transientItem.id)) { ++ final Object mapColor = display.getGeneric("MapColor"); ++ if (mapColor != null) { ++ display.remove("MapColor"); ++ ++ transientItem.componentSetGeneric("minecraft:map_color", mapColor); ++ } ++ } ++ ++ // mirror behavior of fixSubTag ++ if (display != null && display.isEmpty()) { ++ transientItem.tagRemoveMap("display"); ++ } ++ } ++ ++ public static MapType convertBlockStatePredicate(final String value, final TypeUtil type) { ++ final int propertyStart = value.indexOf('['); ++ final int nbtStart = value.indexOf('{'); ++ int blockNameEnd = value.length(); ++ ++ if (propertyStart != -1) { ++ blockNameEnd = propertyStart; ++ } ++ if (nbtStart != -1) { ++ blockNameEnd = Math.min(blockNameEnd, nbtStart); ++ } ++ ++ final MapType ret = type.createEmptyMap(); ++ ++ final String blockName = value.substring(0, blockNameEnd); ++ ++ // string is fine here, the underlying type accepts string AND list under the same name... ++ ret.setString("blocks", blockName.trim()); ++ ++ if (propertyStart != -1) { ++ // unlike DFU, set the fromIndex so that on malformed data we do not IOOBE ++ final int propertyEnd = value.indexOf(']', propertyStart + 1); ++ if (propertyEnd != -1) { ++ final MapType state = type.createEmptyMap(); ++ ret.setMap("state", state); ++ ++ for (final String property : value.substring(propertyStart + 1, propertyEnd).split(",")) { ++ final int separatorIdx = property.indexOf('='); ++ if (separatorIdx == -1) { ++ continue; ++ } ++ ++ final String propertyKey = property.substring(0, separatorIdx).trim(); ++ final String propertyValue = property.substring(separatorIdx + 1); ++ ++ state.setString(propertyKey, propertyValue); ++ } ++ } ++ } ++ ++ if (nbtStart != -1) { ++ // unlike DFU, set the fromIndex so that on malformed data we do not IOOBE ++ final int nbtEnd = value.indexOf('}', nbtStart + 1); ++ if (nbtEnd != -1) { ++ // note: want to include { and } ++ ret.setString("nbt", value.substring(nbtStart, nbtEnd + 1)); ++ } ++ } ++ ++ return ret; ++ } ++ ++ private static void convertBlockStatePredicates(final TransientItemStack item, final TypeUtil type, ++ final String tagKey, final String componentKey, ++ final boolean hideInTooltip) { ++ final ListType blocks = item.tagRemoveListUnchecked(tagKey); ++ if (blocks == null) { ++ return; ++ } ++ ++ final MapType blockPredicates = type.createEmptyMap(); ++ item.componentSetMap(componentKey, blockPredicates); ++ ++ if (hideInTooltip) { ++ blockPredicates.setBoolean("show_in_tooltip", false); ++ } ++ ++ final ListType predicates = type.createEmptyList(); ++ blockPredicates.setList("predicates", predicates); ++ ++ for (int i = 0, len = blocks.size(); i < len; ++i) { ++ final Object block = blocks.getGeneric(i); ++ if (!(block instanceof String blockString)) { ++ // cannot type error here, if block is not a string then nothing in `blocks` is as they have the same type ++ predicates.addGeneric(block); ++ continue; ++ } ++ ++ final MapType predicate = convertBlockStatePredicate(blockString, type); ++ ++ predicates.addMap(predicate); ++ } ++ } ++ ++ private static void convertAdventureMode(final TransientItemStack item, final TypeUtil type, final int flags) { ++ convertBlockStatePredicates( ++ item, type, "CanDestroy", "minecraft:can_break", ++ (flags & TOOLTIP_FLAG_HIDE_CAN_DESTROY) != 0 ++ ); ++ convertBlockStatePredicates( ++ item, type, "CanPlaceOn", "minecraft:can_place_on", ++ (flags & TOOLTIP_FLAG_HIDE_CAN_PLACE) != 0 ++ ); ++ } ++ ++ private static void copy(final MapType src, final String srcKey, final MapType dst, final String dstKey) { ++ if (src == null || dst == null) { ++ return; ++ } ++ ++ final Object srcValue = src.getGeneric(srcKey); ++ if (srcValue != null) { ++ dst.setGeneric(dstKey, srcValue); ++ } ++ } ++ ++ private static MapType convertAttribute(final Object inputGeneric, final TypeUtil type) { ++ final MapType input = inputGeneric instanceof MapType casted ? (MapType)casted : null; ++ ++ final MapType ret = type.createEmptyMap(); ++ ret.setString("name", ""); ++ ret.setDouble("amount", 0.0); ++ ret.setString("operation", "add_value"); ++ ++ copy(input, "AttributeName", ret, "type"); ++ copy(input, "Slot", ret, "slot"); ++ copy(input, "UUID", ret, "uuid"); ++ copy(input, "Name", ret, "name"); ++ copy(input, "Amount", ret, "amount"); ++ ++ // note: no type check on hasKey ++ if (input != null && input.hasKey("Operation")) { ++ final String operation; ++ switch (input.getInt("Operation", 0)) { ++ case 1: { ++ operation = "add_multiplied_base"; ++ break; ++ } ++ case 2: { ++ operation = "add_multiplied_total"; ++ break; ++ } ++ default: { ++ operation = "add_value"; ++ break; ++ } ++ } ++ ret.setString("operation", operation); ++ } ++ ++ return ret; ++ } ++ ++ private static void convertAttributes(final TransientItemStack item, final TypeUtil type, final int flags) { ++ final ListType attributes = item.tagRemoveListUnchecked("AttributeModifiers"); ++ final ListType newAttributes = type.createEmptyList(); ++ ++ if (attributes != null) { ++ for (int i = 0, len = attributes.size(); i < len; ++i) { ++ newAttributes.addMap(convertAttribute(attributes.getGeneric(i), type)); ++ } ++ } ++ ++ final boolean hideModifiers = (flags & TOOLTIP_FLAG_HIDE_MODIFIERS) != 0; ++ if (newAttributes.size() > 0 || hideModifiers) { ++ final MapType newModifiers = type.createEmptyMap(); ++ item.componentSetMap("minecraft:attribute_modifiers", newModifiers); ++ ++ newModifiers.setList("modifiers", newAttributes); ++ if (hideModifiers) { ++ newModifiers.setBoolean("show_in_tooltip", false); ++ } ++ } ++ } ++ ++ private static void convertMap(final TransientItemStack item, final TypeUtil type) { ++ item.tagMigrateToComponent("map", "minecraft:map_id"); ++ ++ final ListType decorations = item.tagRemoveListUnchecked("Decorations"); ++ if (decorations != null) { ++ final MapType newDecorations = type.createEmptyMap(); ++ ++ for (int i = 0, len = decorations.size(); i < len; ++i) { ++ final Object decorationGeneric = decorations.getGeneric(i); ++ ++ final MapType decoration = decorationGeneric instanceof MapType casted ? (MapType)casted : null; ++ ++ // note: getForcedString mirrors DFU converting to string for key ++ final String id = decoration == null ? "" : decoration.getForcedString("id", ""); ++ if (newDecorations.hasKey(id)) { ++ // note: never replace existing decorations by id ++ continue; ++ } ++ ++ final int typeId = decoration == null ? 0 : decoration.getInt("type", 0); ++ final double x = decoration == null ? 0.0 : decoration.getDouble("x", 0.0); ++ final double z = decoration == null ? 0.0 : decoration.getDouble("z", 0.0); ++ final float rot = decoration == null ? 0.0f : (float)decoration.getDouble("rot", 0.0); ++ ++ final MapType newDecoration = type.createEmptyMap(); ++ newDecorations.setMap(id, newDecoration); ++ ++ newDecoration.setString("type", convertMapDecorationId(typeId)); ++ newDecoration.setDouble("x", x); ++ newDecoration.setDouble("z", z); ++ newDecoration.setFloat("rotation", rot); ++ } ++ ++ if (!newDecorations.isEmpty()) { ++ item.componentSetMap("minecraft:map_decorations", newDecorations); ++ } ++ } ++ } ++ ++ private static void convertPotion(final TransientItemStack item, final TypeUtil type) { ++ final MapType potionContents = type.createEmptyMap(); ++ ++ final String potion = item.tagRemoveString("Potion"); ++ ++ if (potion != null && !"minecraft:empty".equals(potion)) { ++ potionContents.setString("potion", potion); ++ } ++ ++ item.migrateTagTo("CustomPotionColor", potionContents, "custom_color"); ++ item.migrateTagTo("custom_potion_effects", potionContents, "custom_effects"); ++ ++ if (!potionContents.isEmpty()) { ++ item.componentSetMap("minecraft:potion_contents", potionContents); ++ } ++ } ++ ++ private static MapType makeFilteredText(final String raw, final String filtered, final TypeUtil type) { ++ final MapType ret = type.createEmptyMap(); ++ ++ ret.setString("raw", raw); ++ if (filtered != null) { ++ ret.setString("filtered", filtered); ++ } ++ ++ return ret; ++ } ++ ++ private static ListType convertBookPages(final TransientItemStack item, final TypeUtil type) { ++ final ListType oldPages = item.tagRemoveListUnchecked("pages"); ++ ++ final MapType filteredPages = item.tagRemoveMap("filtered_pages"); ++ ++ if (oldPages == null || oldPages.size() == 0) { ++ return null; ++ } ++ ++ final ListType ret = type.createEmptyList(); ++ ++ for (int i = 0, len = oldPages.size(); i < len; ++i) { ++ final String page = oldPages.getGeneric(i) instanceof String str ? str : ""; ++ final String filtered = filteredPages == null ? null : filteredPages.getString(Integer.toString(i)); ++ ++ ret.addMap(makeFilteredText(page, filtered, type)); ++ } ++ ++ return ret; ++ } ++ ++ private static void convertWritableBook(final TransientItemStack item, final TypeUtil type) { ++ final ListType pages = convertBookPages(item, type); ++ if (pages != null) { ++ final MapType bookContent = type.createEmptyMap(); ++ item.componentSetMap("minecraft:writable_book_content", bookContent); ++ ++ bookContent.setList("pages", pages); ++ } ++ } ++ ++ private static void convertWrittenBook(final TransientItemStack item, final TypeUtil type) { ++ final ListType pages = convertBookPages(item, type); ++ ++ final MapType bookContent = type.createEmptyMap(); ++ item.componentSetMap("minecraft:written_book_content", bookContent); ++ if (pages != null) { ++ bookContent.setList("pages", pages); ++ } ++ ++ final String title = item.tagRemoveString("title", ""); ++ final String filteredTitle = item.tagRemoveString("filtered_title"); ++ ++ bookContent.setMap("title", makeFilteredText(title, filteredTitle, type)); ++ ++ item.migrateTagTo("author", bookContent, "author"); ++ item.migrateTagTo("resolved", bookContent, "resolved"); ++ item.migrateTagTo("generation", bookContent, "generation"); ++ } ++ ++ private static void convertMobBucket(final TransientItemStack item, final TypeUtil type) { ++ final MapType bucketEntityData = type.createEmptyMap(); ++ ++ for (final String oldKey : BUCKETED_MOB_TAGS) { ++ item.migrateTagTo(oldKey, bucketEntityData, oldKey); ++ } ++ ++ if (!bucketEntityData.isEmpty()) { ++ item.componentSetMap("minecraft:bucket_entity_data", bucketEntityData); ++ } ++ } ++ ++ private static void convertCompass(final TransientItemStack item, final TypeUtil type) { ++ final Object lodestonePos = item.tagRemoveGeneric("LodestonePos"); ++ final Object lodestoneDim = item.tagRemoveGeneric("LodestoneDimension"); ++ ++ if (lodestonePos == null && lodestoneDim == null) { ++ return; ++ } ++ ++ final MapType lodestoneTracker = type.createEmptyMap(); ++ item.componentSetMap("minecraft:lodestone_tracker", lodestoneTracker); ++ ++ if (lodestonePos != null && lodestoneDim != null) { ++ final MapType target = type.createEmptyMap(); ++ lodestoneTracker.setMap("target", target); ++ ++ target.setGeneric("pos", lodestonePos); ++ target.setGeneric("dimension", lodestoneDim); ++ } ++ ++ final boolean tracked = item.tagRemoveBoolean("LodestoneTracked", true); ++ if (!tracked) { ++ lodestoneTracker.setBoolean("tracked", false); ++ } ++ } ++ ++ private static void convertFireworkExplosion(final Object inputGeneric) { ++ if (!(inputGeneric instanceof MapType)) { ++ return; ++ } ++ ++ final MapType input = (MapType)inputGeneric; ++ ++ RenameHelper.renameSingle(input, "Colors", "colors"); ++ RenameHelper.renameSingle(input, "FadeColors", "fade_colors"); ++ RenameHelper.renameSingle(input, "Trail", "has_trail"); ++ RenameHelper.renameSingle(input, "Flicker", "has_twinkle"); ++ ++ final int type = input.getInt("Type", 0); ++ input.remove("Type"); ++ ++ final String newType; ++ switch (type) { ++ case 1: { ++ newType = "large_ball"; ++ break; ++ } ++ case 2: { ++ newType = "star"; ++ break; ++ } ++ case 3: { ++ newType = "creeper"; ++ break; ++ } ++ case 4: { ++ newType = "burst"; ++ break; ++ } ++ default: { ++ newType = "small_ball"; ++ break; ++ } ++ } ++ ++ input.setString("shape", newType); ++ } ++ ++ private static void convertFireworkRocket(final TransientItemStack item, final TypeUtil type) { ++ // adhere to fixSubTag(true) behavior ++ final Object fireworksGeneric = item.tag.getGeneric("Fireworks"); ++ if (fireworksGeneric == null) { ++ return; ++ } ++ ++ if (!(fireworksGeneric instanceof MapType)) { ++ final MapType newFireworks = type.createEmptyMap(); ++ item.componentSetMap("minecraft:fireworks", newFireworks); ++ ++ newFireworks.setList("explosions", type.createEmptyList()); ++ newFireworks.setByte("flight_duration", (byte)0); ++ ++ return; ++ } ++ ++ final MapType fireworks = (MapType)fireworksGeneric; ++ ++ final MapType newFireworks = type.createEmptyMap(); ++ item.componentSetMap("minecraft:fireworks", newFireworks); ++ ++ final int flight = fireworks.getInt("Flight", 0); ++ newFireworks.setByte("flight_duration", (byte)flight); ++ ++ final ListType explosions = fireworks.getListUnchecked("Explosions", type.createEmptyList()); ++ newFireworks.setList("explosions", explosions); ++ ++ for (int i = 0, len = explosions.size(); i < len; ++i) { ++ convertFireworkExplosion(explosions.getGeneric(i)); ++ } ++ ++ fireworks.remove("Explosions"); ++ fireworks.remove("Flight"); ++ if (fireworks.isEmpty()) { ++ item.tag.remove("Fireworks"); ++ } ++ } ++ ++ private static Object copyGeneric(final Object value, final TypeUtil type) { ++ if (value == null || value instanceof Number || value instanceof String) { ++ return value; ++ } ++ if (value instanceof MapType mapType) { ++ return mapType.copy(); ++ } ++ if (value instanceof ListType listType) { ++ return listType.copy(); ++ } ++ // rest of the cases can take the slow path ++ ++ final ListType dummy = type.createEmptyList(); ++ dummy.addGeneric(value); ++ ++ return dummy.copy().getGeneric(0); ++ } ++ ++ private static void convertFireworkStar(final TransientItemStack item, final TypeUtil type) { ++ // note: adhere to fixSubTag(true) behavior ++ final Object explosionGeneric = item.tag.getGeneric("Explosion"); ++ if (explosionGeneric == null) { ++ return; ++ } ++ ++ if (!(explosionGeneric instanceof MapType)) { ++ // important that we copy the generic value when not moving it ++ item.componentSetGeneric("minecraft:firework_explosion", copyGeneric(explosionGeneric, type)); ++ return; ++ } ++ ++ final MapType explosion = (MapType)explosionGeneric; ++ ++ final MapType explosionCopy = explosion.copy(); ++ item.componentSetGeneric("minecraft:firework_explosion", explosionCopy); ++ convertFireworkExplosion(explosionCopy); ++ ++ explosion.remove("Type"); ++ explosion.remove("Colors"); ++ explosion.remove("FadeColors"); ++ explosion.remove("Trail"); ++ explosion.remove("Flicker"); ++ ++ if (explosion.isEmpty()) { ++ item.tag.remove("Explosion"); ++ } ++ } ++ ++ private static boolean isValidPlayerName(final String name) { ++ if (name.length() > 16) { ++ return false; ++ } ++ ++ for (int i = 0, len = name.length(); i < len; ++i) { ++ final char character = name.charAt(i); ++ if (character <= 0x20 || character >= 0x7F) { // printable ascii ++ return false; ++ } ++ } ++ ++ return true; ++ } ++ ++ private static ListType convertProperties(final MapType properties, final TypeUtil type) { ++ final ListType ret = type.createEmptyList(); ++ ++ for (final String propertyKey : properties.keys()) { ++ final ListType propertyValues = properties.getListUnchecked(propertyKey); ++ ++ if (propertyValues == null) { ++ continue; ++ } ++ ++ for (int i = 0, len = propertyValues.size(); i < len; ++i) { ++ final MapType property = propertyValues.getGeneric(i) instanceof MapType casted ? (MapType)casted : null; ++ ++ final String value = property == null ? "" : property.getString("Value", ""); ++ final String signature = property == null ? null : property.getString("Signature"); ++ ++ final MapType newProperty = type.createEmptyMap(); ++ ret.addMap(newProperty); ++ ++ newProperty.setString("name", propertyKey); ++ newProperty.setString("value", value); ++ if (signature != null) { ++ newProperty.setString("signature", signature); ++ } ++ } ++ } ++ ++ return ret; ++ } ++ ++ public static MapType convertProfile(final Object inputGeneric, final TypeUtil type) { ++ final MapType ret = type.createEmptyMap(); ++ ++ if (inputGeneric instanceof String name) { ++ if (!isValidPlayerName(name)) { ++ return ret; ++ } ++ ++ ret.setString("name", name); ++ ++ return ret; ++ } ++ ++ final MapType input = inputGeneric instanceof MapType casted ? (MapType)casted : null; ++ ++ final String name = input == null ? "" : input.getString("Name", ""); ++ ++ if (isValidPlayerName(name)) { ++ ret.setString("name", name); ++ } ++ ++ final Object id = input == null ? null : input.getGeneric("Id"); ++ ++ if (id != null) { ++ ret.setGeneric("id", id); ++ } ++ ++ final MapType properties = input == null ? null : input.getMap("Properties"); ++ if (properties != null && !properties.isEmpty()) { ++ ret.setList("properties", convertProperties(properties, type)); ++ } ++ ++ return ret; ++ } ++ ++ private static void convertSukll(final TransientItemStack item, final TypeUtil type) { ++ final Object skullOwnerGeneric = item.tagRemoveGeneric("SkullOwner"); ++ if (skullOwnerGeneric == null) { ++ return; ++ } ++ ++ item.componentSetMap("minecraft:profile", convertProfile(skullOwnerGeneric, type)); ++ } ++ ++ // input is unmodified ++ public static MapType convertItem(final MapType input) { ++ if (!input.hasKey("id", ObjectType.STRING) || !input.hasKey("Count", ObjectType.NUMBER)) { ++ return input.copy(); ++ } ++ ++ final TypeUtil type = input.getTypeUtil(); ++ ++ final TransientItemStack item = new TransientItemStack(input); ++ ++ item.tagMigrateToComponent("Damage", "minecraft:damage", 0); ++ item.tagMigrateToComponent("RepairCost", "minecraft:repair_cost", 0); ++ item.tagMigrateToComponent("CustomModelData", "minecraft:custom_model_data"); ++ ++ final MapType blockStateProperties = item.tagRemoveMap("BlockStateTag"); ++ if (blockStateProperties != null) { ++ item.componentSetMap("minecraft:block_state", blockStateProperties); ++ convertBlockStateProperties(blockStateProperties); ++ } ++ ++ item.tagMigrateToComponent("EntityTag", "minecraft:entity_data"); ++ ++ final MapType tileEntityTag = item.tagRemoveMap("BlockEntityTag"); ++ if (tileEntityTag != null) { ++ convertTileEntity(tileEntityTag, item); ++ ++ if (tileEntityTag.size() > 1 || (tileEntityTag.size() == 1 && !tileEntityTag.hasKey("id"))) { ++ item.componentSetMap("minecraft:block_entity_data", tileEntityTag); ++ } ++ } ++ ++ final int flags = item.tagRemoveInt("HideFlags", 0); ++ ++ if (item.tagRemoveInt("Unbreakable", 0) != 0) { ++ final MapType unbreakable = type.createEmptyMap(); ++ item.componentSetMap("minecraft:unbreakable", unbreakable); ++ if ((flags & TOOLTIP_FLAG_HIDE_UNBREAKABLE) != 0) { ++ unbreakable.setBoolean("show_in_tooltip", false); ++ } ++ } ++ ++ convertEnchantments( ++ item, type, "Enchantments", "minecraft:enchantments", ++ (flags & TOOLTIP_FLAG_HIDE_ENCHANTMENTS) != 0 ++ ); ++ ++ convertDisplay(item, type, flags); ++ convertAdventureMode(item, type, flags); ++ convertAttributes(item, type, flags); ++ ++ final Object trim = item.tagRemoveGeneric("Trim"); ++ if (trim != null) { ++ // note: DFU set does nothing if not map ++ if ((flags & TOOLTIP_FLAG_HIDE_UPGRADES) != 0 && trim instanceof MapType) { ++ ((MapType)trim).setBoolean("show_in_tooltip", false); ++ } ++ ++ item.componentSetGeneric("minecraft:trim", trim); ++ } ++ ++ if ((flags & TOOLTIP_FLAG_HIDE_ADDITIONAL) != 0) { ++ item.componentSetMap("minecraft:hide_additional_tooltip", type.createEmptyMap()); ++ } ++ ++ switch (item.id) { ++ case "minecraft:enchanted_book": { ++ convertEnchantments( ++ item, type, "StoredEnchantments", "minecraft:stored_enchantments", ++ (flags & TOOLTIP_FLAG_HIDE_ADDITIONAL) != 0 ++ ); ++ break; ++ } ++ case "minecraft:crossbow": { ++ item.tagRemoveGeneric("Charged"); ++ item.tagMigrateNonEmptyListToComponent("ChargedProjectiles", "minecraft:charged_projectiles"); ++ break; ++ } ++ case "minecraft:bundle": { ++ item.tagMigrateNonEmptyListToComponent("Items", "minecraft:bundle_contents"); ++ break; ++ } ++ case "minecraft:filled_map": { ++ convertMap(item, type); ++ break; ++ } ++ case "minecraft:potion": ++ case "minecraft:splash_potion": ++ case "minecraft:lingering_potion": ++ case "minecraft:tipped_arrow": { ++ convertPotion(item, type); ++ break; ++ } ++ case "minecraft:writable_book": { ++ convertWritableBook(item, type); ++ break; ++ } ++ case "minecraft:written_book": { ++ convertWrittenBook(item, type); ++ break; ++ } ++ case "minecraft:suspicious_stew": { ++ item.tagMigrateToComponent("effects", "minecraft:suspicious_stew_effects"); ++ break; ++ } ++ case "minecraft:debug_stick": { ++ item.tagMigrateToComponent("DebugProperty", "minecraft:debug_stick_state"); ++ break; ++ } ++ case "minecraft:pufferfish_bucket": ++ case "minecraft:salmon_bucket": ++ case "minecraft:cod_bucket": ++ case "minecraft:tropical_fish_bucket": ++ case "minecraft:axolotl_bucket": ++ case "minecraft:tadpole_bucket": { ++ convertMobBucket(item, type); ++ break; ++ } ++ case "minecraft:goat_horn": { ++ item.tagMigrateToComponent("instrument", "minecraft:instrument"); ++ break; ++ } ++ case "minecraft:knowledge_book": { ++ item.tagMigrateToComponent("Recipes", "minecraft:recipes"); ++ break; ++ } ++ case "minecraft:compass": { ++ convertCompass(item, type); ++ break; ++ } ++ case "minecraft:firework_rocket": { ++ convertFireworkRocket(item, type); ++ break; ++ } ++ case "minecraft:firework_star": { ++ convertFireworkStar(item, type); ++ break; ++ } ++ case "minecraft:player_head": { ++ convertSukll(item, type); ++ break; ++ } ++ } ++ ++ return item.serialize(); ++ } ++ ++ private ConverterItemStackToDataComponents() {} ++ ++ private static final class TransientItemStack { ++ ++ private final String id; ++ private final int count; ++ ++ private final MapType components; ++ private final MapType tag; ++ private final MapType root; ++ ++ public TransientItemStack(final MapType root) { ++ this.id = root.getString("id"); ++ this.count = root.getInt("Count"); ++ ++ final TypeUtil type = root.getTypeUtil(); ++ ++ this.components = type.createEmptyMap(); ++ ++ final MapType rootCopy = root.copy(); ++ ++ final MapType tag = rootCopy.getMap("tag"); ++ ++ rootCopy.remove("id"); ++ rootCopy.remove("Count"); ++ rootCopy.remove("tag"); ++ ++ this.tag = tag == null ? type.createEmptyMap() : tag; ++ ++ this.root = rootCopy; ++ } ++ ++ public void migrateTagTo(final String tagKey, final MapType dst, final String dstKey) { ++ final Object value = this.tag.getGeneric(tagKey); ++ ++ if (value != null) { ++ this.tag.remove(tagKey); ++ ++ dst.setGeneric(dstKey, value); ++ } ++ } ++ ++ public String tagRemoveString(final String key) { ++ final String ret = this.tag.getString(key); ++ ++ this.tag.remove(key); ++ ++ return ret; ++ } ++ ++ public String tagRemoveString(final String key, final String dfl) { ++ final String ret = this.tag.getString(key, dfl); ++ ++ this.tag.remove(key); ++ ++ return ret; ++ } ++ ++ public ListType tagRemoveListUnchecked(final String key) { ++ final ListType ret = this.tag.getListUnchecked(key); ++ ++ this.tag.remove(key); ++ ++ return ret; ++ } ++ ++ public ListType tagRemoveList(final String key, final ObjectType listType) { ++ final ListType ret = this.tag.getList(key, listType); ++ ++ this.tag.remove(key); ++ ++ return ret; ++ } ++ ++ public MapType tagRemoveMap(final String key) { ++ final MapType ret = this.tag.getMap(key); ++ ++ this.tag.remove(key); ++ ++ return ret; ++ } ++ ++ public boolean tagRemoveBoolean(final String key, final boolean dfl) { ++ final boolean ret = this.tag.getBoolean(key, dfl); ++ ++ this.tag.remove(key); ++ ++ return ret; ++ } ++ ++ public int tagRemoveInt(final String key, final int dfl) { ++ final int ret = this.tag.getInt(key, dfl); ++ ++ this.tag.remove(key); ++ ++ return ret; ++ } ++ ++ public Object tagRemoveGeneric(final String key) { ++ final Object ret = this.tag.getGeneric(key); ++ ++ if (ret != null) { ++ this.tag.remove(key); ++ return ret; ++ } ++ ++ return ret; ++ } ++ ++ public void tagMigrateToComponent(final String tagKey, final String componentKey) { ++ final Object value = this.tag.getGeneric(tagKey); ++ if (value != null) { ++ this.tag.remove(tagKey); ++ ++ this.components.setGeneric(componentKey, value); ++ } ++ } ++ ++ public void tagMigrateNonEmptyListToComponent(final String tagKey, final String componentKey) { ++ final Object value = this.tag.getGeneric(tagKey); ++ if (value != null) { ++ this.tag.remove(tagKey); ++ ++ if (!(value instanceof ListType list) || list.size() > 0) { ++ this.components.setGeneric(componentKey, value); ++ } ++ } ++ } ++ ++ public void tagMigrateToComponent(final String tagKey, final String componentKey, final int defaultComponent) { ++ final int value = this.tag.getInt(tagKey, defaultComponent); ++ this.tag.remove(tagKey); ++ ++ if (value != defaultComponent) { ++ this.components.setGeneric(componentKey, value); ++ } ++ } ++ ++ public void componentSetBoolean(final String key, final boolean value) { ++ this.components.setBoolean(key, value); ++ } ++ ++ public void componentSetString(final String key, final String value) { ++ this.components.setString(key, value); ++ } ++ ++ public void componentSetList(final String key, final ListType value) { ++ this.components.setList(key, value); ++ } ++ ++ public void componentSetMap(final String key, final MapType value) { ++ this.components.setMap(key, value); ++ } ++ ++ public void componentSetGeneric(final String key, final Object value) { ++ this.components.setGeneric(key, value); ++ } ++ ++ public MapType serialize() { ++ final MapType ret = this.components.getTypeUtil().createEmptyMap(); ++ ++ ret.setString("id", this.id); ++ ret.setInt("count", this.count); ++ if (!this.tag.isEmpty()) { ++ this.components.setMap("minecraft:custom_data", this.tag); ++ } ++ ++ if (!this.components.isEmpty()) { ++ ret.setMap("components", this.components); ++ } ++ ++ // merge root to ret, with entries in ret taking priority ++ if (!this.root.isEmpty()) { ++ for (final String key : this.root.keys()) { ++ if (ret.hasKey(key)) { ++ continue; ++ } ++ ++ ret.setGeneric(key, this.root.getGeneric(key)); ++ } ++ } ++ ++ return ret; ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/leveldat/ConverterRemoveFeatureFlag.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/leveldat/ConverterRemoveFeatureFlag.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4c537b661b7a28193add3267ec2d639add49423b +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/leveldat/ConverterRemoveFeatureFlag.java +@@ -0,0 +1,46 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.leveldat; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import java.util.Set; ++ ++public final class ConverterRemoveFeatureFlag extends DataConverter, MapType> { ++ ++ private final Set flags; ++ ++ public ConverterRemoveFeatureFlag(final int toVersion, final Set flags) { ++ this(toVersion, 0, flags); ++ } ++ ++ public ConverterRemoveFeatureFlag(final int toVersion, final int versionStep, final Set flags) { ++ super(toVersion, versionStep); ++ this.flags = flags; ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final ListType enabledFeatures = data.getList("enabled_features", ObjectType.STRING); ++ if (enabledFeatures == null) { ++ return null; ++ } ++ ++ ListType removedFeatures = null; ++ ++ for (int i = 0; i < enabledFeatures.size(); ++i) { ++ final String flag = enabledFeatures.getString(i); ++ if (!this.flags.contains(flag)) { ++ continue; ++ } ++ enabledFeatures.remove(i--); ++ ++ if (removedFeatures == null) { ++ removedFeatures = data.getOrCreateList("removed_features", ObjectType.STRING); ++ } ++ removedFeatures.addString(flag); ++ } ++ ++ return null; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/options/ConverterAbstractOptionsRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/options/ConverterAbstractOptionsRename.java +new file mode 100644 +index 0000000000000000000000000000000000000000..769dd8447976b66dcfc36283ede4ae16f1e4206d +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/options/ConverterAbstractOptionsRename.java +@@ -0,0 +1,28 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.options; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import java.util.ArrayList; ++import java.util.function.Function; ++ ++public final class ConverterAbstractOptionsRename { ++ ++ private ConverterAbstractOptionsRename() {} ++ ++ public static void register(final int version, final Function renamer) { ++ register(version, 0, renamer); ++ } ++ ++ public static void register(final int version, final int subVersion, final Function renamer) { ++ MCTypeRegistry.OPTIONS.addStructureConverter(new DataConverter<>(version, subVersion) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ RenameHelper.renameKeys(data, renamer); ++ return null; ++ } ++ }); ++ } ++ ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/particle/ConverterParticleToNBT.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/particle/ConverterParticleToNBT.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0f961bca2ab2cd9e93cb7c23951ac6b867cdd04e +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/particle/ConverterParticleToNBT.java +@@ -0,0 +1,268 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.particle; ++ ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.TypeUtil; ++import ca.spottedleaf.dataconverter.types.nbt.NBTMapType; ++import ca.spottedleaf.dataconverter.util.NamespaceUtil; ++import com.mojang.brigadier.StringReader; ++import com.mojang.brigadier.exceptions.CommandSyntaxException; ++import com.mojang.logging.LogUtils; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.TagParser; ++import net.minecraft.util.Mth; ++import org.slf4j.Logger; ++ ++public final class ConverterParticleToNBT { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ private static CompoundTag parseNBT(final String flat) { ++ try { ++ return TagParser.parseTag(flat); ++ } catch (final Exception ex) { ++ LOGGER.warn("Failed to parse nbt: " + flat, ex); ++ return null; ++ } ++ } ++ ++ private static void convertItem(final MapType nbt, final String data) { ++ final MapType itemNBT = nbt.getTypeUtil().createEmptyMap(); ++ nbt.setMap("item", itemNBT); ++ itemNBT.setInt("Count", 1); ++ ++ final int nbtStart = data.indexOf('{'); ++ if (nbtStart == -1) { ++ // assume data is item name ++ itemNBT.setString("id", NamespaceUtil.correctNamespace(data)); ++ return; ++ } ++ // itemname{tagNBT} ++ itemNBT.setString("id", NamespaceUtil.correctNamespace(data.substring(0, nbtStart))); ++ ++ final CompoundTag tag = parseNBT(data.substring(nbtStart)); ++ if (tag != null) { ++ // do we need to worry about type conversion? ++ itemNBT.setMap("tag", new NBTMapType(tag)); ++ } ++ } ++ ++ private static MapType parseProperties(final String input, final TypeUtil type) { ++ final MapType ret = type.createEmptyMap(); ++ try { ++ // format: [p1=v1, p2=v2, p3=v3, ...] ++ final StringReader reader = new StringReader(input); ++ ++ reader.expect('['); ++ reader.skipWhitespace(); ++ ++ while (reader.canRead() && reader.peek() != ']') { ++ reader.skipWhitespace(); ++ ++ final String property = reader.readString(); ++ ++ reader.skipWhitespace(); ++ reader.expect('='); ++ reader.skipWhitespace(); ++ ++ final String value = reader.readString(); ++ ret.setString(property, value); ++ ++ reader.skipWhitespace(); ++ if (reader.canRead()) { ++ if (reader.peek() != ',') { ++ // invalid character or ']' ++ break; ++ } ++ ++ // skip ',' and move onto next entry ++ reader.peek(); ++ } ++ } ++ ++ reader.expect(']'); ++ return ret; ++ } catch (final Exception ex) { ++ LOGGER.warn("Failed to parse block properties: " + input, ex); ++ return null; ++ } ++ } ++ ++ private static void convertBlock(final MapType nbt, final String data) { ++ final MapType blockNBT = nbt.getTypeUtil().createEmptyMap(); ++ nbt.setMap("block_state", blockNBT); ++ ++ final int propertiesStart = data.indexOf('['); ++ if (propertiesStart == -1) { ++ // assume data is id ++ blockNBT.setString("Name", NamespaceUtil.correctNamespace(data)); ++ return; ++ } ++ blockNBT.setString("Name", NamespaceUtil.correctNamespace(data.substring(0, propertiesStart))); ++ ++ // blockname{properties} ++ final MapType properties = parseProperties(data.substring(propertiesStart), nbt.getTypeUtil()); ++ if (properties != null && !properties.isEmpty()) { ++ blockNBT.setMap("Properties", properties); ++ } ++ } ++ ++ private static ListType parseFloatVector(final StringReader reader, final TypeUtil type) throws CommandSyntaxException { ++ final float x = reader.readFloat(); ++ ++ reader.expect(' '); ++ final float y = reader.readFloat(); ++ ++ reader.expect(' '); ++ final float z = reader.readFloat(); ++ ++ final ListType ret = type.createEmptyList(); ++ ret.addFloat(x); ++ ret.addFloat(y); ++ ret.addFloat(z); ++ ++ return ret; ++ } ++ ++ private static void convertDust(final MapType nbt, final String data) { ++ try { ++ final StringReader reader = new StringReader(data); ++ ++ final ListType color = parseFloatVector(reader, nbt.getTypeUtil()); ++ ++ reader.expect(' '); ++ final float scale = reader.readFloat(); ++ ++ nbt.setList("color", color); ++ nbt.setFloat("scale", scale); ++ } catch (final Exception ex) { ++ LOGGER.warn("Failed to parse dust particle: " + data, ex); ++ } ++ } ++ ++ private static void convertColorDust(final MapType nbt, final String data) { ++ try { ++ final StringReader reader = new StringReader(data); ++ ++ final ListType fromColor = parseFloatVector(reader, nbt.getTypeUtil()); ++ ++ reader.expect(' '); ++ final float scale = reader.readFloat(); ++ ++ reader.expect(' '); ++ final ListType toColor = parseFloatVector(reader, nbt.getTypeUtil()); ++ ++ nbt.setList("from_color", fromColor); ++ nbt.setFloat("scale", scale); ++ nbt.setList("to_color", toColor); ++ } catch (final Exception ex) { ++ LOGGER.warn("Failed to parse color transition dust particle: " + data, ex); ++ } ++ } ++ ++ private static void convertSculk(final MapType nbt, final String data) { ++ try { ++ final StringReader reader = new StringReader(data); ++ ++ final float roll = reader.readFloat(); ++ ++ nbt.setFloat("roll", roll); ++ } catch (final Exception ex) { ++ LOGGER.warn("Failed to parse sculk particle: " + data, ex); ++ } ++ } ++ ++ private static void convertVibration(final MapType nbt, final String data) { ++ try { ++ final StringReader reader = new StringReader(data); ++ ++ final double posX = reader.readDouble(); ++ ++ reader.expect(' '); ++ final double posY = reader.readDouble(); ++ ++ reader.expect(' '); ++ final double posZ = reader.readDouble(); ++ ++ reader.expect(' '); ++ final int arrival = reader.readInt(); ++ ++ nbt.setInt("arrival_in_ticks", arrival); ++ ++ final MapType destination = nbt.getTypeUtil().createEmptyMap(); ++ nbt.setMap("destination", destination); ++ ++ destination.setString("type", "minecraft:block"); ++ ++ final ListType pos = nbt.getTypeUtil().createEmptyList(); ++ destination.setList("pos", pos); ++ ++ pos.addInt(Mth.floor(posX)); ++ pos.addInt(Mth.floor(posY)); ++ pos.addInt(Mth.floor(posZ)); ++ } catch (final Exception ex) { ++ LOGGER.warn("Failed to parse vibration particle: " + data, ex); ++ } ++ } ++ ++ private static void convertShriek(final MapType nbt, final String data) { ++ try { ++ final StringReader reader = new StringReader(data); ++ ++ final int delay = reader.readInt(); ++ ++ nbt.setInt("delay", delay); ++ } catch (final Exception ex) { ++ LOGGER.warn("Failed to parse shriek particle: " + data, ex); ++ } ++ } ++ ++ public static MapType convert(final String flat, final TypeUtil type) { ++ final String[] split = flat.split(" ", 2); ++ final String name = NamespaceUtil.correctNamespace(split[0]); ++ ++ final MapType ret = type.createEmptyMap(); ++ ret.setString("type", name); ++ ++ if (split.length > 1) { ++ final String data = split[1]; ++ switch (name) { ++ case "minecraft:item": { ++ convertItem(ret, data); ++ break; ++ } ++ case "minecraft:block": ++ case "minecraft:block_marker": ++ case "minecraft:falling_dust": ++ case "minecraft:dust_pillar": { ++ convertBlock(ret, data); ++ break; ++ } ++ case "minecraft:dust": { ++ convertDust(ret, data); ++ break; ++ } ++ case "minecraft:dust_color_transition": { ++ convertColorDust(ret, data); ++ break; ++ } ++ case "minecraft:sculk_charge": { ++ convertSculk(ret, data); ++ break; ++ } ++ case "minecraft:vibration": { ++ convertVibration(ret, data); ++ break; ++ } ++ case "minecraft:shriek": { ++ convertShriek(ret, data); ++ break; ++ } ++ } ++ } ++ ++ return ret; ++ } ++ ++ private ConverterParticleToNBT() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterAbstractPOIRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterAbstractPOIRename.java +new file mode 100644 +index 0000000000000000000000000000000000000000..57e210bf2bb189b15a32899011c4800b19668a5e +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterAbstractPOIRename.java +@@ -0,0 +1,53 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.poi; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import java.util.function.Function; ++ ++public final class ConverterAbstractPOIRename { ++ ++ private ConverterAbstractPOIRename() {} ++ ++ public static void register(final int version, final Function renamer) { ++ register(version, 0, renamer); ++ } ++ ++ public static void register(final int version, final int subVersion, final Function renamer) { ++ MCTypeRegistry.POI_CHUNK.addStructureConverter(new DataConverter<>(version, subVersion) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType sections = data.getMap("Sections"); ++ if (sections == null) { ++ return null; ++ } ++ ++ for (final String key : sections.keys()) { ++ final MapType section = sections.getMap(key); ++ ++ final ListType records = section.getList("Records", ObjectType.MAP); ++ ++ if (records == null) { ++ continue; ++ } ++ ++ for (int i = 0, len = records.size(); i < len; ++i) { ++ final MapType record = records.getMap(i); ++ ++ final String type = record.getString("type"); ++ if (type != null) { ++ final String converted = renamer.apply(type); ++ if (converted != null) { ++ record.setString("type", converted); ++ } ++ } ++ } ++ } ++ ++ return null; ++ } ++ }); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterPoiDelete.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterPoiDelete.java +new file mode 100644 +index 0000000000000000000000000000000000000000..36aa9c3eedb3f2e2f577efed3622fed74268bce1 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterPoiDelete.java +@@ -0,0 +1,53 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.poi; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import java.util.function.Predicate; ++ ++public final class ConverterPoiDelete extends DataConverter, MapType> { ++ ++ private final Predicate delete; ++ ++ public ConverterPoiDelete(final int toVersion, final Predicate delete) { ++ super(toVersion); ++ this.delete = delete; ++ } ++ ++ public ConverterPoiDelete(final int toVersion, final int versionStep, final Predicate delete) { ++ super(toVersion, versionStep); ++ this.delete = delete; ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType sections = data.getMap("Sections"); ++ if (sections == null) { ++ return null; ++ } ++ ++ for (final String key : sections.keys()) { ++ final MapType section = sections.getMap(key); ++ ++ final ListType records = section.getList("Records", ObjectType.MAP); ++ ++ if (records == null) { ++ continue; ++ } ++ ++ for (int i = 0; i < records.size();) { ++ final MapType record = records.getMap(i); ++ ++ final String type = record.getString("type"); ++ if (type != null && this.delete.test(type)) { ++ records.remove(i); ++ continue; ++ } ++ ++i; ++ } ++ } ++ ++ return null; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/recipe/ConverterAbstractRecipeRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/recipe/ConverterAbstractRecipeRename.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8f35cbbd78a629712f9ae3cd5d180269f015a11d +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/recipe/ConverterAbstractRecipeRename.java +@@ -0,0 +1,18 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.recipe; ++ ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import java.util.function.Function; ++ ++public final class ConverterAbstractRecipeRename { ++ ++ private ConverterAbstractRecipeRename() {} ++ ++ public static void register(final int version, final Function renamer) { ++ register(version, 0, renamer); ++ } ++ ++ public static void register(final int version, final int subVersion, final Function renamer) { ++ ConverterAbstractStringValueTypeRename.register(version, subVersion, MCTypeRegistry.RECIPE, renamer); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterAbstractStatsRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterAbstractStatsRename.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a1985c85aa9193699d7d20e6f4f11b6e9744ee70 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterAbstractStatsRename.java +@@ -0,0 +1,66 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.stats; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import java.util.ArrayList; ++import java.util.function.Function; ++ ++public final class ConverterAbstractStatsRename { ++ ++ private ConverterAbstractStatsRename() {} ++ ++ public static void register(final int version, final Function renamer) { ++ register(version, 0, renamer); ++ } ++ ++ public static void register(final int version, final int subVersion, final Function renamer) { ++ MCTypeRegistry.OBJECTIVE.addStructureConverter(new DataConverter<>(version, subVersion) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType criteriaType = data.getMap("CriteriaType"); ++ if (criteriaType == null) { ++ return null; ++ } ++ ++ final String type = criteriaType.getString("type"); ++ if (!"minecraft:custom".equals(type)) { ++ return null; ++ } ++ ++ final String id = criteriaType.getString("id"); ++ if (id == null) { ++ return null; ++ } ++ ++ final String rename = renamer.apply(id); ++ if (rename != null) { ++ criteriaType.setString("id", rename); ++ } ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.STATS.addStructureConverter(new DataConverter<>(version, subVersion) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType stats = data.getMap("stats"); ++ ++ if (stats == null) { ++ return null; ++ } ++ ++ final MapType custom = stats.getMap("minecraft:custom"); ++ if (custom == null) { ++ return null; ++ } ++ ++ RenameHelper.renameKeys(custom, renamer); ++ ++ return null; ++ } ++ }); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterFlattenStats.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterFlattenStats.java +new file mode 100644 +index 0000000000000000000000000000000000000000..891be75bf5c4af56e839c88b26f0a828554ae5c4 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterFlattenStats.java +@@ -0,0 +1,321 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.stats; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.HelperBlockFlatteningV1450; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemstack.ConverterFlattenItemStack; ++import ca.spottedleaf.dataconverter.minecraft.versions.V1451; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.Types; ++import com.google.common.collect.ImmutableMap; ++import com.google.common.collect.ImmutableSet; ++import org.apache.commons.lang3.StringUtils; ++import java.util.HashMap; ++import java.util.HashSet; ++import java.util.Map; ++import java.util.Set; ++ ++public final class ConverterFlattenStats { ++ ++ private static final int VERSION = MCVersions.V17W47A; ++ private static final int VERSION_STEP = 6; ++ ++ private static final Set SPECIAL_OBJECTIVE_CRITERIA = new HashSet<>( ++ Set.of( ++ "dummy", ++ "trigger", ++ "deathCount", ++ "playerKillCount", ++ "totalKillCount", ++ "health", ++ "food", ++ "air", ++ "armor", ++ "xp", ++ "level", ++ "killedByTeam.aqua", ++ "killedByTeam.black", ++ "killedByTeam.blue", ++ "killedByTeam.dark_aqua", ++ "killedByTeam.dark_blue", ++ "killedByTeam.dark_gray", ++ "killedByTeam.dark_green", ++ "killedByTeam.dark_purple", ++ "killedByTeam.dark_red", ++ "killedByTeam.gold", ++ "killedByTeam.gray", ++ "killedByTeam.green", ++ "killedByTeam.light_purple", ++ "killedByTeam.red", ++ "killedByTeam.white", ++ "killedByTeam.yellow", ++ "teamkill.aqua", ++ "teamkill.black", ++ "teamkill.blue", ++ "teamkill.dark_aqua", ++ "teamkill.dark_blue", ++ "teamkill.dark_gray", ++ "teamkill.dark_green", ++ "teamkill.dark_purple", ++ "teamkill.dark_red", ++ "teamkill.gold", ++ "teamkill.gray", ++ "teamkill.green", ++ "teamkill.light_purple", ++ "teamkill.red", ++ "teamkill.white", ++ "teamkill.yellow" ++ ) ++ ); ++ ++ private static final Set SKIP = new HashSet<>( ++ ImmutableSet.builder() ++ .add("stat.craftItem.minecraft.spawn_egg") ++ .add("stat.useItem.minecraft.spawn_egg") ++ .add("stat.breakItem.minecraft.spawn_egg") ++ .add("stat.pickup.minecraft.spawn_egg") ++ .add("stat.drop.minecraft.spawn_egg") ++ .build() ++ ); ++ ++ private static final Map CUSTOM_MAP = new HashMap<>( ++ ImmutableMap.builder() ++ .put("stat.leaveGame", "minecraft:leave_game") ++ .put("stat.playOneMinute", "minecraft:play_one_minute") ++ .put("stat.timeSinceDeath", "minecraft:time_since_death") ++ .put("stat.sneakTime", "minecraft:sneak_time") ++ .put("stat.walkOneCm", "minecraft:walk_one_cm") ++ .put("stat.crouchOneCm", "minecraft:crouch_one_cm") ++ .put("stat.sprintOneCm", "minecraft:sprint_one_cm") ++ .put("stat.swimOneCm", "minecraft:swim_one_cm") ++ .put("stat.fallOneCm", "minecraft:fall_one_cm") ++ .put("stat.climbOneCm", "minecraft:climb_one_cm") ++ .put("stat.flyOneCm", "minecraft:fly_one_cm") ++ .put("stat.diveOneCm", "minecraft:dive_one_cm") ++ .put("stat.minecartOneCm", "minecraft:minecart_one_cm") ++ .put("stat.boatOneCm", "minecraft:boat_one_cm") ++ .put("stat.pigOneCm", "minecraft:pig_one_cm") ++ .put("stat.horseOneCm", "minecraft:horse_one_cm") ++ .put("stat.aviateOneCm", "minecraft:aviate_one_cm") ++ .put("stat.jump", "minecraft:jump") ++ .put("stat.drop", "minecraft:drop") ++ .put("stat.damageDealt", "minecraft:damage_dealt") ++ .put("stat.damageTaken", "minecraft:damage_taken") ++ .put("stat.deaths", "minecraft:deaths") ++ .put("stat.mobKills", "minecraft:mob_kills") ++ .put("stat.animalsBred", "minecraft:animals_bred") ++ .put("stat.playerKills", "minecraft:player_kills") ++ .put("stat.fishCaught", "minecraft:fish_caught") ++ .put("stat.talkedToVillager", "minecraft:talked_to_villager") ++ .put("stat.tradedWithVillager", "minecraft:traded_with_villager") ++ .put("stat.cakeSlicesEaten", "minecraft:eat_cake_slice") ++ .put("stat.cauldronFilled", "minecraft:fill_cauldron") ++ .put("stat.cauldronUsed", "minecraft:use_cauldron") ++ .put("stat.armorCleaned", "minecraft:clean_armor") ++ .put("stat.bannerCleaned", "minecraft:clean_banner") ++ .put("stat.brewingstandInteraction", "minecraft:interact_with_brewingstand") ++ .put("stat.beaconInteraction", "minecraft:interact_with_beacon") ++ .put("stat.dropperInspected", "minecraft:inspect_dropper") ++ .put("stat.hopperInspected", "minecraft:inspect_hopper") ++ .put("stat.dispenserInspected", "minecraft:inspect_dispenser") ++ .put("stat.noteblockPlayed", "minecraft:play_noteblock") ++ .put("stat.noteblockTuned", "minecraft:tune_noteblock") ++ .put("stat.flowerPotted", "minecraft:pot_flower") ++ .put("stat.trappedChestTriggered", "minecraft:trigger_trapped_chest") ++ .put("stat.enderchestOpened", "minecraft:open_enderchest") ++ .put("stat.itemEnchanted", "minecraft:enchant_item") ++ .put("stat.recordPlayed", "minecraft:play_record") ++ .put("stat.furnaceInteraction", "minecraft:interact_with_furnace") ++ .put("stat.craftingTableInteraction", "minecraft:interact_with_crafting_table") ++ .put("stat.chestOpened", "minecraft:open_chest") ++ .put("stat.sleepInBed", "minecraft:sleep_in_bed") ++ .put("stat.shulkerBoxOpened", "minecraft:open_shulker_box") ++ .build() ++ ); ++ ++ private static final String BLOCK_KEY = "stat.mineBlock"; ++ private static final String NEW_BLOCK_KEY = "minecraft:mined"; ++ ++ private static final Map ITEM_KEYS = new HashMap<>( ++ ImmutableMap.builder() ++ .put("stat.craftItem", "minecraft:crafted") ++ .put("stat.useItem", "minecraft:used") ++ .put("stat.breakItem", "minecraft:broken") ++ .put("stat.pickup", "minecraft:picked_up") ++ .put("stat.drop", "minecraft:dropped") ++ .build() ++ ); ++ ++ private static final Map ENTITY_KEYS = new HashMap<>( ++ ImmutableMap.builder() ++ .put("stat.entityKilledBy", "minecraft:killed_by") ++ .put("stat.killEntity", "minecraft:killed") ++ .build() ++ ); ++ ++ private static final Map ENTITIES = new HashMap<>( ++ ImmutableMap.builder() ++ .put("Bat", "minecraft:bat") ++ .put("Blaze", "minecraft:blaze") ++ .put("CaveSpider", "minecraft:cave_spider") ++ .put("Chicken", "minecraft:chicken") ++ .put("Cow", "minecraft:cow") ++ .put("Creeper", "minecraft:creeper") ++ .put("Donkey", "minecraft:donkey") ++ .put("ElderGuardian", "minecraft:elder_guardian") ++ .put("Enderman", "minecraft:enderman") ++ .put("Endermite", "minecraft:endermite") ++ .put("EvocationIllager", "minecraft:evocation_illager") ++ .put("Ghast", "minecraft:ghast") ++ .put("Guardian", "minecraft:guardian") ++ .put("Horse", "minecraft:horse") ++ .put("Husk", "minecraft:husk") ++ .put("Llama", "minecraft:llama") ++ .put("LavaSlime", "minecraft:magma_cube") ++ .put("MushroomCow", "minecraft:mooshroom") ++ .put("Mule", "minecraft:mule") ++ .put("Ozelot", "minecraft:ocelot") ++ .put("Parrot", "minecraft:parrot") ++ .put("Pig", "minecraft:pig") ++ .put("PolarBear", "minecraft:polar_bear") ++ .put("Rabbit", "minecraft:rabbit") ++ .put("Sheep", "minecraft:sheep") ++ .put("Shulker", "minecraft:shulker") ++ .put("Silverfish", "minecraft:silverfish") ++ .put("SkeletonHorse", "minecraft:skeleton_horse") ++ .put("Skeleton", "minecraft:skeleton") ++ .put("Slime", "minecraft:slime") ++ .put("Spider", "minecraft:spider") ++ .put("Squid", "minecraft:squid") ++ .put("Stray", "minecraft:stray") ++ .put("Vex", "minecraft:vex") ++ .put("Villager", "minecraft:villager") ++ .put("VindicationIllager", "minecraft:vindication_illager") ++ .put("Witch", "minecraft:witch") ++ .put("WitherSkeleton", "minecraft:wither_skeleton") ++ .put("Wolf", "minecraft:wolf") ++ .put("ZombieHorse", "minecraft:zombie_horse") ++ .put("PigZombie", "minecraft:zombie_pigman") ++ .put("ZombieVillager", "minecraft:zombie_villager") ++ .put("Zombie", "minecraft:zombie") ++ .build() ++ ); ++ ++ private static final String NEW_CUSTOM_KEY = "minecraft:custom"; ++ ++ private ConverterFlattenStats() {} ++ ++ private static String upgradeItem(final String itemName) { ++ return ConverterFlattenItemStack.flattenItem(itemName, 0); ++ } ++ ++ private static String upgradeBlock(final String block) { ++ return HelperBlockFlatteningV1450.getNewBlockName(block); ++ } ++ ++ private static record StatType(String category, String key) {} ++ ++ private static StatType convertLegacyKey(final String key) { ++ if (SKIP.contains(key)) { ++ return null; ++ } ++ ++ final String custom = CUSTOM_MAP.get(key); ++ if (custom != null) { ++ return new StatType(NEW_CUSTOM_KEY, custom); ++ } ++ ++ final int i = StringUtils.ordinalIndexOf(key, ".", 2); ++ if (i < 0) { ++ return null; ++ } ++ ++ final String stat = key.substring(0, i); ++ ++ if (BLOCK_KEY.equals(stat)) { ++ return new StatType(NEW_BLOCK_KEY, upgradeBlock(key.substring(i + 1).replace('.', ':'))); ++ } ++ ++ final String itemStat = ITEM_KEYS.get(stat); ++ ++ if (itemStat != null) { ++ final String itemId = key.substring(i + 1).replace('.', ':'); ++ final String flattenedItem = upgradeItem(itemId); ++ ++ return new StatType(itemStat, flattenedItem == null ? itemId : flattenedItem); ++ } ++ ++ final String entityStat = ENTITY_KEYS.get(stat); ++ if (entityStat != null) { ++ final String entityId = key.substring(i + 1).replace('.', ':'); ++ ++ return new StatType(entityStat, ENTITIES.getOrDefault(entityId, entityId)); ++ } ++ ++ return null; ++ } ++ ++ public static DataConverter, MapType> makeStatsConverter() { ++ return new DataConverter<>(VERSION, VERSION_STEP) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType stats = Types.NBT.createEmptyMap(); ++ ++ for (final String statKey : data.keys()) { ++ final Number value = data.getNumber(statKey); ++ if (value == null) { ++ continue; ++ } ++ ++ final StatType converted = convertLegacyKey(statKey); ++ ++ if (converted == null) { ++ continue; ++ } ++ ++ stats.getOrCreateMap(converted.category()).setGeneric(converted.key(), value); ++ } ++ ++ data.clear(); ++ data.setMap("stats", stats); ++ ++ return null; ++ } ++ }; ++ } ++ ++ public static DataConverter, MapType> makeObjectiveConverter() { ++ return new DataConverter<>(VERSION, VERSION_STEP) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ convertCriteriaName(data, "CriteriaName"); ++ ++ // We also need to update CriteriaType that is created by the data hook in V1451, ++ // otherwise that data hook will overwrite our CriteriaName ++ final MapType criteriaType = data.getMap("CriteriaType"); ++ if (criteriaType != null) { ++ if ("_special".equals(criteriaType.getString("type"))) { ++ convertCriteriaName(criteriaType, "id"); ++ } ++ } ++ ++ return null; ++ } ++ ++ private void convertCriteriaName(final MapType data, final String key) { ++ final String criteriaName = data.getString(key); ++ ++ if (criteriaName == null) { ++ return; ++ } ++ ++ if (SPECIAL_OBJECTIVE_CRITERIA.contains(criteriaName)) { ++ return; ++ } ++ ++ final StatType converted = convertLegacyKey(criteriaName); ++ data.setString(key, converted == null ? "dummy" : V1451.packWithDot(converted.category()) + ":" + V1451.packWithDot(converted.key())); ++ } ++ }; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/tileentity/ConverterAbstractTileEntityRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/tileentity/ConverterAbstractTileEntityRename.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ab05dda0cc2083418443d0dee23ccc0a6f754ea0 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/tileentity/ConverterAbstractTileEntityRename.java +@@ -0,0 +1,34 @@ ++package ca.spottedleaf.dataconverter.minecraft.converters.tileentity; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import java.util.function.Function; ++ ++public final class ConverterAbstractTileEntityRename { ++ ++ public static void register(final int version, final Function renamer) { ++ register(version, 0, renamer); ++ } ++ ++ public static void register(final int version, final int subVersion, final Function renamer) { ++ MCTypeRegistry.TILE_ENTITY.addStructureConverter(new DataConverter<>(version, subVersion) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final String id = data.getString("id"); ++ if (id == null) { ++ return null; ++ } ++ ++ final String converted = renamer.apply(id); ++ ++ if (converted != null) { ++ data.setString("id", converted); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/DynamicDataType.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/DynamicDataType.java +new file mode 100644 +index 0000000000000000000000000000000000000000..dfa750bdaef7d7b6dadbc5665c1461f7e6df08ca +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/DynamicDataType.java +@@ -0,0 +1,128 @@ ++package ca.spottedleaf.dataconverter.minecraft.datatypes; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.converters.datatypes.DataHook; ++import ca.spottedleaf.dataconverter.converters.datatypes.DataType; ++import ca.spottedleaf.dataconverter.converters.datatypes.DataWalker; ++import ca.spottedleaf.dataconverter.minecraft.MCVersionRegistry; ++import ca.spottedleaf.dataconverter.util.Long2ObjectArraySortedMap; ++import java.util.ArrayList; ++import java.util.List; ++ ++public class DynamicDataType extends DataType { ++ ++ public final String name; ++ ++ protected final ArrayList> structureConverters = new ArrayList<>(); ++ protected final Long2ObjectArraySortedMap>> structureWalkers = new Long2ObjectArraySortedMap<>(); ++ protected final Long2ObjectArraySortedMap>> structureHooks = new Long2ObjectArraySortedMap<>(); ++ ++ public DynamicDataType(final String name) { ++ this.name = name; ++ } ++ ++ public void addStructureConverter(final DataConverter converter) { ++ MCVersionRegistry.checkVersion(converter.getEncodedVersion()); ++ this.structureConverters.add(converter); ++ this.structureConverters.sort(DataConverter.LOWEST_VERSION_COMPARATOR); ++ } ++ ++ public void addStructureWalker(final int minVersion, final DataWalker walker) { ++ this.addStructureWalker(minVersion, 0, walker); ++ } ++ ++ public void addStructureWalker(final int minVersion, final int versionStep, final DataWalker walker) { ++ this.structureWalkers.computeIfAbsent(DataConverter.encodeVersions(minVersion, versionStep), (final long keyInMap) -> { ++ return new ArrayList<>(); ++ }).add(walker); ++ } ++ ++ public void addStructureHook(final int minVersion, final DataHook hook) { ++ this.addStructureHook(minVersion, 0, hook); ++ } ++ ++ public void addStructureHook(final int minVersion, final int versionStep, final DataHook hook) { ++ this.structureHooks.computeIfAbsent(DataConverter.encodeVersions(minVersion, versionStep), (final long keyInMap) -> { ++ return new ArrayList<>(); ++ }).add(hook); ++ } ++ ++ @Override ++ public Object convert(Object data, final long fromVersion, final long toVersion) { ++ Object ret = null; ++ ++ final List> converters = this.structureConverters; ++ for (int i = 0, len = converters.size(); i < len; ++i) { ++ final DataConverter converter = converters.get(i); ++ final long converterVersion = converter.getEncodedVersion(); ++ ++ if (converterVersion <= fromVersion) { ++ continue; ++ } ++ ++ if (converterVersion > toVersion) { ++ break; ++ } ++ ++ List> hooks = this.structureHooks.getFloor(converterVersion); ++ ++ if (hooks != null) { ++ for (int k = 0, klen = hooks.size(); k < klen; ++k) { ++ final Object replace = hooks.get(k).preHook(data, fromVersion, toVersion); ++ if (replace != null) { ++ ret = data = replace; ++ } ++ } ++ } ++ ++ final Object replace = converter.convert(data, fromVersion, toVersion); ++ if (replace != null) { ++ ret = data = replace; ++ } ++ ++ // possibly new data format, update hooks ++ hooks = this.structureHooks.getFloor(toVersion); ++ ++ if (hooks != null) { ++ for (int klen = hooks.size(), k = klen - 1; k >= 0; --k) { ++ final Object postReplace = hooks.get(k).postHook(data, fromVersion, toVersion); ++ if (postReplace != null) { ++ ret = data = postReplace; ++ } ++ } ++ } ++ } ++ ++ final List> hooks = this.structureHooks.getFloor(toVersion); ++ ++ if (hooks != null) { ++ for (int k = 0, klen = hooks.size(); k < klen; ++k) { ++ final Object replace = hooks.get(k).preHook(data, fromVersion, toVersion); ++ if (replace != null) { ++ ret = data = replace; ++ } ++ } ++ } ++ ++ final List> walkers = this.structureWalkers.getFloor(toVersion); ++ if (walkers != null) { ++ for (int i = 0, len = walkers.size(); i < len; ++i) { ++ final Object replace = walkers.get(i).walk(data, fromVersion, toVersion); ++ if (replace != null) { ++ ret = data = replace; ++ } ++ } ++ } ++ ++ if (hooks != null) { ++ for (int klen = hooks.size(), k = klen - 1; k >= 0; --k) { ++ final Object postReplace = hooks.get(k).postHook(data, fromVersion, toVersion); ++ if (postReplace != null) { ++ ret = data = postReplace; ++ } ++ } ++ } ++ ++ return ret; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/IDDataType.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/IDDataType.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b093a9eeeea3f7c1c220485b7144d22c6fd504a0 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/IDDataType.java +@@ -0,0 +1,166 @@ ++package ca.spottedleaf.dataconverter.minecraft.datatypes; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.converters.datatypes.DataHook; ++import ca.spottedleaf.dataconverter.converters.datatypes.DataWalker; ++import ca.spottedleaf.dataconverter.minecraft.MCVersionRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.util.Long2ObjectArraySortedMap; ++ ++import java.util.ArrayList; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Map; ++ ++public class IDDataType extends MCDataType { ++ ++ protected final Map>>>> walkersById = new HashMap<>(); ++ ++ public IDDataType(final String name) { ++ super(name); ++ } ++ ++ public void addConverterForId(final String id, final DataConverter, MapType> converter) { ++ this.addStructureConverter(new DataConverter<>(converter.getToVersion(), converter.getVersionStep()) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (!id.equals(data.getString("id"))) { ++ return null; ++ } ++ return converter.convert(data, sourceVersion, toVersion); ++ } ++ }); ++ } ++ ++ public void addWalker(final int minVersion, final String id, final DataWalker> walker) { ++ this.addWalker(minVersion, 0, id, walker); ++ } ++ ++ public void addWalker(final int minVersion, final int versionStep, final String id, final DataWalker> walker) { ++ this.walkersById.computeIfAbsent(id, (final String keyInMap) -> { ++ return new Long2ObjectArraySortedMap<>(); ++ }).computeIfAbsent(DataConverter.encodeVersions(minVersion, versionStep), (final long keyInMap) -> { ++ return new ArrayList<>(); ++ }).add(walker); ++ } ++ ++ public void copyWalkers(final int minVersion, final String fromId, final String toId) { ++ this.copyWalkers(minVersion, 0, fromId, toId); ++ } ++ ++ public void copyWalkers(final int minVersion, final int versionStep, final String fromId, final String toId) { ++ final long version = DataConverter.encodeVersions(minVersion, versionStep); ++ final Long2ObjectArraySortedMap>>> walkersForId = this.walkersById.get(fromId); ++ if (walkersForId == null) { ++ return; ++ } ++ ++ final List>> nearest = walkersForId.getFloor(version); ++ ++ if (nearest == null) { ++ return; ++ } ++ ++ for (final DataWalker> walker : nearest) { ++ this.addWalker(minVersion, versionStep, toId, walker); ++ } ++ } ++ ++ @Override ++ public MapType convert(MapType data, final long fromVersion, final long toVersion) { ++ MapType ret = null; ++ ++ final List, MapType>> converters = this.structureConverters; ++ for (int i = 0, len = converters.size(); i < len; ++i) { ++ final DataConverter, MapType> converter = converters.get(i); ++ final long converterVersion = converter.getEncodedVersion(); ++ ++ if (converterVersion <= fromVersion) { ++ continue; ++ } ++ ++ if (converterVersion > toVersion) { ++ break; ++ } ++ ++ List, MapType>> hooks = this.structureHooks.getFloor(converterVersion); ++ ++ if (hooks != null) { ++ for (int k = 0, klen = hooks.size(); k < klen; ++k) { ++ final MapType replace = hooks.get(k).preHook(data, fromVersion, toVersion); ++ if (replace != null) { ++ ret = data = replace; ++ } ++ } ++ } ++ ++ final MapType replace = converter.convert(data, fromVersion, toVersion); ++ if (replace != null) { ++ ret = data = replace; ++ } ++ ++ // possibly new data format, update hooks ++ hooks = this.structureHooks.getFloor(toVersion); ++ ++ if (hooks != null) { ++ for (int klen = hooks.size(), k = klen - 1; k >= 0; --k) { ++ final MapType postReplace = hooks.get(k).postHook(data, fromVersion, toVersion); ++ if (postReplace != null) { ++ ret = data = postReplace; ++ } ++ } ++ } ++ } ++ ++ final List, MapType>> hooks = this.structureHooks.getFloor(toVersion); ++ ++ // run pre hooks ++ ++ if (hooks != null) { ++ for (int k = 0, klen = hooks.size(); k < klen; ++k) { ++ final MapType replace = hooks.get(k).preHook(data, fromVersion, toVersion); ++ if (replace != null) { ++ ret = data = replace; ++ } ++ } ++ } ++ ++ // run all walkers ++ ++ final List>> walkers = this.structureWalkers.getFloor(toVersion); ++ if (walkers != null) { ++ for (int i = 0, len = walkers.size(); i < len; ++i) { ++ final MapType replace = walkers.get(i).walk(data, fromVersion, toVersion); ++ if (replace != null) { ++ ret = data = replace; ++ } ++ } ++ } ++ ++ final Long2ObjectArraySortedMap>>> walkersByVersion = this.walkersById.get(data.getString("id")); ++ if (walkersByVersion != null) { ++ final List>> walkersForId = walkersByVersion.getFloor(toVersion); ++ if (walkersForId != null) { ++ for (int i = 0, len = walkersForId.size(); i < len; ++i) { ++ final MapType replace = walkersForId.get(i).walk(data, fromVersion, toVersion); ++ if (replace != null) { ++ ret = data = replace; ++ } ++ } ++ } ++ } ++ ++ // run post hooks ++ ++ if (hooks != null) { ++ for (int klen = hooks.size(), k = klen - 1; k >= 0; --k) { ++ final MapType postReplace = hooks.get(k).postHook(data, fromVersion, toVersion); ++ if (postReplace != null) { ++ ret = data = postReplace; ++ } ++ } ++ } ++ ++ return ret; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCDataType.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCDataType.java +new file mode 100644 +index 0000000000000000000000000000000000000000..075574f33476882ddc6787e3b8bac8643a414eb0 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCDataType.java +@@ -0,0 +1,129 @@ ++package ca.spottedleaf.dataconverter.minecraft.datatypes; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.converters.datatypes.DataHook; ++import ca.spottedleaf.dataconverter.converters.datatypes.DataType; ++import ca.spottedleaf.dataconverter.converters.datatypes.DataWalker; ++import ca.spottedleaf.dataconverter.minecraft.MCVersionRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.util.Long2ObjectArraySortedMap; ++import java.util.ArrayList; ++import java.util.List; ++ ++public class MCDataType extends DataType, MapType> { ++ ++ public final String name; ++ ++ protected final ArrayList, MapType>> structureConverters = new ArrayList<>(); ++ protected final Long2ObjectArraySortedMap>>> structureWalkers = new Long2ObjectArraySortedMap<>(); ++ protected final Long2ObjectArraySortedMap, MapType>>> structureHooks = new Long2ObjectArraySortedMap<>(); ++ ++ public MCDataType(final String name) { ++ this.name = name; ++ } ++ ++ public void addStructureConverter(final DataConverter, MapType> converter) { ++ MCVersionRegistry.checkVersion(converter.getEncodedVersion()); ++ this.structureConverters.add(converter); ++ this.structureConverters.sort(DataConverter.LOWEST_VERSION_COMPARATOR); ++ } ++ ++ public void addStructureWalker(final int minVersion, final DataWalker> walker) { ++ this.addStructureWalker(minVersion, 0, walker); ++ } ++ ++ public void addStructureWalker(final int minVersion, final int versionStep, final DataWalker> walker) { ++ this.structureWalkers.computeIfAbsent(DataConverter.encodeVersions(minVersion, versionStep), (final long keyInMap) -> { ++ return new ArrayList<>(); ++ }).add(walker); ++ } ++ ++ public void addStructureHook(final int minVersion, final DataHook, MapType> hook) { ++ this.addStructureHook(minVersion, 0, hook); ++ } ++ ++ public void addStructureHook(final int minVersion, final int versionStep, final DataHook, MapType> hook) { ++ this.structureHooks.computeIfAbsent(DataConverter.encodeVersions(minVersion, versionStep), (final long keyInMap) -> { ++ return new ArrayList<>(); ++ }).add(hook); ++ } ++ ++ @Override ++ public MapType convert(MapType data, final long fromVersion, final long toVersion) { ++ MapType ret = null; ++ ++ final List, MapType>> converters = this.structureConverters; ++ for (int i = 0, len = converters.size(); i < len; ++i) { ++ final DataConverter, MapType> converter = converters.get(i); ++ final long converterVersion = converter.getEncodedVersion(); ++ ++ if (converterVersion <= fromVersion) { ++ continue; ++ } ++ ++ if (converterVersion > toVersion) { ++ break; ++ } ++ ++ List, MapType>> hooks = this.structureHooks.getFloor(converterVersion); ++ ++ if (hooks != null) { ++ for (int k = 0, klen = hooks.size(); k < klen; ++k) { ++ final MapType replace = hooks.get(k).preHook(data, fromVersion, toVersion); ++ if (replace != null) { ++ ret = data = replace; ++ } ++ } ++ } ++ ++ final MapType replace = converter.convert(data, fromVersion, toVersion); ++ if (replace != null) { ++ ret = data = replace; ++ } ++ ++ // possibly new data format, update hooks ++ hooks = this.structureHooks.getFloor(toVersion); ++ ++ if (hooks != null) { ++ for (int klen = hooks.size(), k = klen - 1; k >= 0; --k) { ++ final MapType postReplace = hooks.get(k).postHook(data, fromVersion, toVersion); ++ if (postReplace != null) { ++ ret = data = postReplace; ++ } ++ } ++ } ++ } ++ ++ final List, MapType>> hooks = this.structureHooks.getFloor(toVersion); ++ ++ if (hooks != null) { ++ for (int k = 0, klen = hooks.size(); k < klen; ++k) { ++ final MapType replace = hooks.get(k).preHook(data, fromVersion, toVersion); ++ if (replace != null) { ++ ret = data = replace; ++ } ++ } ++ } ++ ++ final List>> walkers = this.structureWalkers.getFloor(toVersion); ++ if (walkers != null) { ++ for (int i = 0, len = walkers.size(); i < len; ++i) { ++ final MapType replace = walkers.get(i).walk(data, fromVersion, toVersion); ++ if (replace != null) { ++ ret = data = replace; ++ } ++ } ++ } ++ ++ if (hooks != null) { ++ for (int klen = hooks.size(), k = klen - 1; k >= 0; --k) { ++ final MapType postReplace = hooks.get(k).postHook(data, fromVersion, toVersion); ++ if (postReplace != null) { ++ ret = data = postReplace; ++ } ++ } ++ } ++ ++ return ret; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCTypeRegistry.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCTypeRegistry.java +new file mode 100644 +index 0000000000000000000000000000000000000000..37519f2aab94c299f68819d1b2df2caffb75c9cd +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCTypeRegistry.java +@@ -0,0 +1,298 @@ ++package ca.spottedleaf.dataconverter.minecraft.datatypes; ++ ++import ca.spottedleaf.dataconverter.minecraft.versions.*; ++import com.mojang.logging.LogUtils; ++import org.slf4j.Logger; ++ ++public final class MCTypeRegistry { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ public static final MCDataType LEVEL = new MCDataType("Level"); ++ public static final MCDataType PLAYER = new MCDataType("Player"); ++ public static final MCDataType CHUNK = new MCDataType("Chunk"); ++ public static final MCDataType HOTBAR = new MCDataType("CreativeHotbar"); ++ public static final MCDataType OPTIONS = new MCDataType("Options"); ++ public static final MCDataType STRUCTURE = new MCDataType("Structure"); ++ public static final MCDataType STATS = new MCDataType("Stats"); ++ public static final MCDataType ADVANCEMENTS = new MCDataType("Advancements"); ++ public static final MCDataType POI_CHUNK = new MCDataType("PoiChunk"); ++ public static final MCDataType ENTITY_CHUNK = new MCDataType("EntityChunk"); ++ public static final IDDataType TILE_ENTITY = new IDDataType("TileEntity"); ++ public static final IDDataType ITEM_STACK = new IDDataType("ItemStack"); ++ public static final MCDataType BLOCK_STATE = new MCDataType("BlockState"); ++ public static final MCValueType FLAT_BLOCK_STATE = new MCValueType("FlatBlockState"); ++ public static final MCDataType DATA_COMPONENTS = new MCDataType("DataComponents"); ++ public static final MCDataType VILLAGER_TRADE = new MCDataType("VillagerTrade"); ++ public static final DynamicDataType PARTICLE = new DynamicDataType("Particle"); ++ public static final MCValueType ENTITY_NAME = new MCValueType("EntityName"); ++ public static final IDDataType ENTITY = new IDDataType("Entity"); ++ public static final MCValueType BLOCK_NAME = new MCValueType("BlockName"); ++ public static final MCValueType ITEM_NAME = new MCValueType("ItemName"); ++ public static final MCDataType UNTAGGED_SPAWNER = new MCDataType("Spawner"); ++ public static final MCDataType STRUCTURE_FEATURE = new MCDataType("StructureFeature"); ++ public static final MCDataType OBJECTIVE = new MCDataType("Objective"); ++ public static final MCDataType TEAM = new MCDataType("Team"); ++ public static final MCValueType RECIPE = new MCValueType("RecipeName"); ++ public static final MCValueType BIOME = new MCValueType("Biome"); ++ public static final MCDataType WORLD_GEN_SETTINGS = new MCDataType("WorldGenSettings"); ++ public static final MCValueType GAME_EVENT_NAME = new MCValueType("GameEventName"); ++ ++ public static final MCValueType MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST = new MCValueType("MultiNoiseBiomeSourceParameterList"); ++ ++ public static final MCDataType SAVED_DATA_RANDOM_SEQUENCES = new MCDataType("SavedData/RandomSequences"); ++ public static final MCDataType SAVED_DATA_SCOREBOARD = new MCDataType("SavedData/Scoreboard"); ++ public static final MCDataType SAVED_DATA_STRUCTURE_FEATURE_INDICES = new MCDataType("SavedData/StructureFeatureIndices"); ++ public static final MCDataType SAVED_DATA_MAP_DATA = new MCDataType("SavedData/MapData"); ++ public static final MCDataType SAVED_DATA_RAIDS = new MCDataType("SavedData/Raids"); ++ public static final MCDataType SAVED_DATA_COMMAND_STORAGE = new MCDataType("SavedData/CommandStorage"); ++ public static final MCDataType SAVED_DATA_FORCED_CHUNKS = new MCDataType("SavedData/Chunks"); ++ public static final MCDataType SAVED_DATA_MAP_INDEX = new MCDataType("SavedData/IdCounts"); ++ ++ public static final MCValueType DATACONVERTER_CUSTOM_TYPE_COMMAND = new MCValueType("DC_Custom/Command"); ++ ++ static { ++ try { ++ registerAll(); ++ } catch (final Throwable thr) { ++ LOGGER.error(LogUtils.FATAL_MARKER, "Failed to register data converters", thr); ++ throw new RuntimeException(thr); ++ } ++ } ++ ++ private static void registerAll() { ++ // General notes: ++ // - Structure converters run before everything. ++ // - ID specific converters run after structure converters. ++ // - Structure walkers run after id specific converters. ++ // - ID specific walkers run after structure walkers. ++ ++ V99.register(); // all legacy data before converters existed ++ V100.register(); // first version with version id ++ V101.register(); ++ V102.register(); ++ V105.register(); ++ V106.register(); ++ V107.register(); ++ V108.register(); ++ V109.register(); ++ V110.register(); ++ V111.register(); ++ V113.register(); ++ V135.register(); ++ V143.register(); ++ V147.register(); ++ V165.register(); ++ V501.register(); ++ V502.register(); ++ V505.register(); ++ V700.register(); ++ V701.register(); ++ V702.register(); ++ V703.register(); ++ V704.register(); ++ V705.register(); ++ V804.register(); ++ V806.register(); ++ V808.register(); ++ V813.register(); ++ V816.register(); ++ V820.register(); ++ V1022.register(); ++ V1125.register(); ++ // END OF LEGACY DATA CONVERTERS ++ ++ // V1.13 ++ V1344.register(); ++ V1446.register(); ++ // START THE FLATTENING ++ V1450.register(); ++ V1451.register(); ++ // END THE FLATTENING ++ ++ V1456.register(); ++ V1458.register(); ++ V1460.register(); ++ V1466.register(); ++ V1470.register(); ++ V1474.register(); ++ V1475.register(); ++ V1480.register(); ++ // V1481 is adding simple block entity ++ V1483.register(); ++ V1484.register(); ++ V1486.register(); ++ V1487.register(); ++ V1488.register(); ++ V1490.register(); ++ V1492.register(); ++ V1494.register(); ++ V1496.register(); ++ V1500.register(); ++ V1501.register(); ++ V1502.register(); ++ V1506.register(); ++ V1510.register(); ++ V1514.register(); ++ V1515.register(); ++ V1624.register(); ++ // V1.14 ++ V1800.register(); ++ V1801.register(); ++ V1802.register(); ++ V1803.register(); ++ V1904.register(); ++ V1905.register(); ++ V1906.register(); ++ V1909.register(); ++ V1911.register(); ++ V1914.register(); ++ V1917.register(); ++ V1918.register(); ++ V1920.register(); ++ V1925.register(); ++ V1928.register(); ++ V1929.register(); ++ V1931.register(); ++ V1936.register(); ++ V1946.register(); ++ V1948.register(); ++ V1953.register(); ++ V1955.register(); ++ V1961.register(); ++ V1963.register(); ++ // V1.15 ++ V2100.register(); ++ V2202.register(); ++ V2209.register(); ++ V2211.register(); ++ V2218.register(); ++ // V1.16 ++ V2501.register(); ++ V2502.register(); ++ V2503.register(); ++ V2505.register(); ++ V2508.register(); ++ V2509.register(); ++ V2511.register(); ++ V2514.register(); ++ V2516.register(); ++ V2518.register(); ++ V2519.register(); ++ V2522.register(); ++ V2523.register(); ++ V2527.register(); ++ V2528.register(); ++ V2529.register(); ++ V2531.register(); ++ V2533.register(); ++ V2535.register(); ++ V2538.register(); ++ V2550.register(); ++ V2551.register(); ++ V2552.register(); ++ V2553.register(); ++ V2558.register(); ++ V2568.register(); ++ // V1.17 ++ // WARN: Mojang registers V2671 under 2571, but that version predates 1.16.5? So it looks like a typo... ++ // I changed it to 2671, just so that it's after 1.16.5, but even then this looks misplaced... Thankfully this is ++ // the first datafixer, and all it does is add a walker, so I think even if the version here is just wrong it will ++ // work. ++ V2671.register(); ++ V2679.register(); ++ V2680.register(); ++ V2684.register(); ++ V2686.register(); ++ V2688.register(); ++ V2690.register(); ++ V2691.register(); ++ V2693.register(); ++ V2696.register(); ++ V2700.register(); ++ V2701.register(); ++ V2702.register(); ++ // In reference to V2671, why the fuck is goat being registered again? For this obvious reason, V2704 is absent. ++ V2707.register(); ++ V2710.register(); ++ V2717.register(); ++ // V1.18 ++ V2825.register(); ++ V2831.register(); ++ V2832.register(); ++ V2833.register(); ++ V2838.register(); ++ V2841.register(); ++ V2842.register(); ++ V2843.register(); ++ V2846.register(); ++ V2852.register(); ++ V2967.register(); ++ V2970.register(); ++ // V1.19 ++ // V3076 is registering a simple tile entity (sculk_catalyst) ++ V3077.register(); ++ V3078.register(); ++ V3081.register(); ++ V3082.register(); ++ V3083.register(); ++ V3084.register(); ++ V3086.register(); ++ V3087.register(); ++ V3088.register(); ++ V3090.register(); ++ V3093.register(); ++ V3094.register(); ++ V3097.register(); ++ V3108.register(); ++ V3201.register(); ++ // V3202 registers a simple tile entity ++ V3203.register(); ++ V3204.register(); ++ V3209.register(); ++ V3214.register(); ++ V3319.register(); ++ V3322.register(); ++ V3325.register(); ++ V3326.register(); ++ V3327.register(); ++ V3328.register(); ++ // V1.20 ++ V3438.register(); ++ V3439.register(); ++ V3440.register(); ++ V3441.register(); ++ V3447.register(); ++ V3448.register(); ++ V3450.register(); ++ V3451.register(); ++ V3459.register(); ++ V3564.register(); ++ V3565.register(); ++ V3566.register(); ++ V3568.register(); ++ V3682.register(); ++ V3683.register(); ++ V3685.register(); ++ V3689.register(); ++ V3692.register(); ++ // V1.20.5 ++ V3799.register(); ++ V3800.register(); ++ V3803.register(); ++ V3807.register(); ++ V3808.register(); ++ V3809.register(); ++ V3812.register(); ++ V3813.register(); ++ V3814.register(); ++ V3816.register(); ++ V3818.register(); ++ V3820.register(); ++ V3825.register(); ++ V3828.register(); ++ V3833.register(); ++ } ++ ++ private MCTypeRegistry() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCValueType.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCValueType.java +new file mode 100644 +index 0000000000000000000000000000000000000000..13c1381261909ef672fbeb665907f01f2d5c1ced +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCValueType.java +@@ -0,0 +1,86 @@ ++package ca.spottedleaf.dataconverter.minecraft.datatypes; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.converters.datatypes.DataHook; ++import ca.spottedleaf.dataconverter.converters.datatypes.DataType; ++import ca.spottedleaf.dataconverter.minecraft.MCVersionRegistry; ++import ca.spottedleaf.dataconverter.util.Long2ObjectArraySortedMap; ++import java.util.ArrayList; ++import java.util.List; ++ ++public class MCValueType extends DataType { ++ ++ public final String name; ++ ++ protected final ArrayList> converters = new ArrayList<>(); ++ protected final Long2ObjectArraySortedMap>> structureHooks = new Long2ObjectArraySortedMap<>(); ++ ++ public MCValueType(final String name) { ++ this.name = name; ++ } ++ ++ public void addStructureHook(final int minVersion, final DataHook hook) { ++ this.addStructureHook(minVersion, 0, hook); ++ } ++ ++ public void addStructureHook(final int minVersion, final int versionStep, final DataHook hook) { ++ this.structureHooks.computeIfAbsent(DataConverter.encodeVersions(minVersion, versionStep), (final long keyInMap) -> { ++ return new ArrayList<>(); ++ }).add(hook); ++ } ++ ++ public void addConverter(final DataConverter converter) { ++ MCVersionRegistry.checkVersion(converter.getEncodedVersion()); ++ this.converters.add(converter); ++ this.converters.sort(DataConverter.LOWEST_VERSION_COMPARATOR); ++ } ++ ++ @Override ++ public Object convert(final Object data, final long fromVersion, final long toVersion) { ++ Object ret = null; ++ final List> converters = this.converters; ++ ++ for (int i = 0, len = converters.size(); i < len; ++i) { ++ final DataConverter converter = converters.get(i); ++ final long converterVersion = converter.getEncodedVersion(); ++ ++ if (converterVersion <= fromVersion) { ++ continue; ++ } ++ ++ if (converterVersion > toVersion) { ++ break; ++ } ++ ++ List> hooks = this.structureHooks.getFloor(converterVersion); ++ ++ if (hooks != null) { ++ for (int k = 0, klen = hooks.size(); k < klen; ++k) { ++ final Object replace = hooks.get(k).preHook(ret == null ? data : ret, fromVersion, toVersion); ++ if (replace != null) { ++ ret = replace; ++ } ++ } ++ } ++ ++ final Object converted = converter.convert(ret == null ? data : ret, fromVersion, toVersion); ++ if (converted != null) { ++ ret = converted; ++ } ++ ++ // possibly new data format, update hooks ++ hooks = this.structureHooks.getFloor(toVersion); ++ ++ if (hooks != null) { ++ for (int k = 0, klen = hooks.size(); k < klen; ++k) { ++ final Object replace = hooks.get(k).postHook(ret == null ? data : ret, fromVersion, toVersion); ++ if (replace != null) { ++ ret = replace; ++ } ++ } ++ } ++ } ++ ++ return ret; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookEnforceNamespacedID.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookEnforceNamespacedID.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f7dced8a47ebdd262ae815ff9bc453312343ce49 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookEnforceNamespacedID.java +@@ -0,0 +1,29 @@ ++package ca.spottedleaf.dataconverter.minecraft.hooks; ++ ++import ca.spottedleaf.dataconverter.converters.datatypes.DataHook; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.util.NamespaceUtil; ++ ++public class DataHookEnforceNamespacedID implements DataHook, MapType> { ++ ++ private final String path; ++ ++ public DataHookEnforceNamespacedID() { ++ this("id"); ++ } ++ ++ public DataHookEnforceNamespacedID(final String path) { ++ this.path = path; ++ } ++ ++ @Override ++ public MapType preHook(final MapType data, final long fromVersion, final long toVersion) { ++ NamespaceUtil.enforceForPath(data, this.path); ++ return null; ++ } ++ ++ @Override ++ public MapType postHook(final MapType data, final long fromVersion, final long toVersion) { ++ return null; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookValueTypeEnforceNamespaced.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookValueTypeEnforceNamespaced.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7f88487e7db589070512fafef1eb243ae29a379a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookValueTypeEnforceNamespaced.java +@@ -0,0 +1,20 @@ ++package ca.spottedleaf.dataconverter.minecraft.hooks; ++ ++import ca.spottedleaf.dataconverter.converters.datatypes.DataHook; ++import ca.spottedleaf.dataconverter.util.NamespaceUtil; ++ ++public class DataHookValueTypeEnforceNamespaced implements DataHook { ++ ++ @Override ++ public Object preHook(final Object data, final long fromVersion, final long toVersion) { ++ if (data instanceof String) { ++ return NamespaceUtil.correctNamespaceOrNull((String)data); ++ } ++ return null; ++ } ++ ++ @Override ++ public Object postHook(final Object data, final long fromVersion, final long toVersion) { ++ return null; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/util/ComponentUtils.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/util/ComponentUtils.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7f8f9d002b286f166d6e59bbf77aff74af10731c +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/util/ComponentUtils.java +@@ -0,0 +1,78 @@ ++package ca.spottedleaf.dataconverter.minecraft.util; ++ ++import com.google.gson.JsonElement; ++import com.google.gson.JsonObject; ++import com.google.gson.JsonParseException; ++import com.google.gson.JsonParser; ++import com.google.gson.JsonPrimitive; ++import net.minecraft.util.GsonHelper; ++ ++public final class ComponentUtils { ++ ++ public static final String EMPTY = createPlainTextComponent(""); ++ ++ public static String createPlainTextComponent(final String text) { ++ final JsonObject ret = new JsonObject(); ++ ++ ret.addProperty("text", text); ++ ++ return GsonHelper.toStableString(ret); ++ } ++ ++ public static String createTranslatableComponent(final String key) { ++ final JsonObject ret = new JsonObject(); ++ ++ ret.addProperty("translate", key); ++ ++ return GsonHelper.toStableString(ret); ++ } ++ ++ public static String retrieveTranslationString(final String possibleJson) { ++ try { ++ final JsonElement element = JsonParser.parseString(possibleJson); ++ ++ if (element instanceof JsonObject object) { ++ final JsonElement translation = object.get("translate"); ++ if (translation instanceof JsonPrimitive primitive) { ++ return primitive.getAsString(); ++ } ++ } ++ ++ return null; ++ } catch (final Exception ex) { ++ return null; ++ } ++ } ++ ++ public static String convertFromLenient(final String input) { ++ if (input == null) { ++ return input; ++ } ++ ++ if (input.isEmpty() || input.equals("null")) { ++ return EMPTY; ++ } ++ ++ final char firstCharacter = input.charAt(0); ++ final char lastCharacter = input.charAt(input.length() - 1); ++ if ((firstCharacter == '"' && lastCharacter == '"') ++ || (firstCharacter == '{' && lastCharacter == '}') ++ || (firstCharacter == '[' && lastCharacter == ']')) { ++ try { ++ final JsonElement json = JsonParser.parseString(input); ++ ++ if (json.isJsonPrimitive()) { ++ return createPlainTextComponent(json.getAsString()); ++ } ++ ++ return GsonHelper.toStableString(json); ++ } catch (final JsonParseException ignored) { ++ // fall through to plain text ++ } ++ } ++ ++ return createPlainTextComponent(input); ++ } ++ ++ private ComponentUtils() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V100.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V100.java +new file mode 100644 +index 0000000000000000000000000000000000000000..91b1d0be9d697a4fa8bc5b448b329df1f5deabc4 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V100.java +@@ -0,0 +1,161 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.block_name.DataWalkerBlockNames; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.Types; ++ ++public final class V100 { ++ ++ private static final int VERSION = MCVersions.V15W32A; ++ ++ static void registerEquipment(final int version, final String id) { ++ registerEquipment(version, 0, id); ++ } ++ ++ private static final DataWalkerItemLists EQUIPMENT_ITEM_LISTS = new DataWalkerItemLists("ArmorItems", "HandItems"); ++ private static final DataWalkerItems EQUIPMENT_ITEMS = new DataWalkerItems("body_armor_item"); ++ ++ static void registerEquipment(final int version, final int versionStep, final String id) { ++ MCTypeRegistry.ENTITY.addWalker(version, versionStep, id, EQUIPMENT_ITEM_LISTS); ++ MCTypeRegistry.ENTITY.addWalker(version, versionStep, id, EQUIPMENT_ITEMS); ++ } ++ ++ private static void registerMob(final String id) { ++ registerEquipment(VERSION, 0, id); ++ } ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final ListType equipment = data.getList("Equipment", ObjectType.MAP); ++ data.remove("Equipment"); ++ ++ if (equipment != null) { ++ if (equipment.size() > 0 && data.getListUnchecked("HandItems") == null) { ++ final ListType handItems = Types.NBT.createEmptyList(); ++ data.setList("HandItems", handItems); ++ handItems.addMap(equipment.getMap(0)); ++ handItems.addMap(Types.NBT.createEmptyMap()); ++ } ++ ++ if (equipment.size() > 1 && data.getListUnchecked("ArmorItems") == null) { ++ final ListType armorItems = Types.NBT.createEmptyList(); ++ data.setList("ArmorItems", armorItems); ++ for (int i = 1; i < Math.min(equipment.size(), 5); ++i) { ++ armorItems.addMap(equipment.getMap(i)); ++ } ++ } ++ } ++ ++ final ListType dropChances = data.getList("DropChances", ObjectType.FLOAT); ++ data.remove("DropChances"); ++ ++ if (dropChances != null) { ++ if (data.getListUnchecked("HandDropChances") == null) { ++ final ListType handDropChances = Types.NBT.createEmptyList(); ++ data.setList("HandDropChances", handDropChances); ++ if (0 < dropChances.size()) { ++ handDropChances.addFloat(dropChances.getFloat(0)); ++ } else { ++ handDropChances.addFloat(0.0F); ++ } ++ handDropChances.addFloat(0.0F); ++ } ++ ++ if (data.getListUnchecked("ArmorDropChances") == null) { ++ final ListType armorDropChances = Types.NBT.createEmptyList(); ++ data.setList("ArmorDropChances", armorDropChances); ++ for (int i = 1; i < 5; ++i) { ++ if (i < dropChances.size()) { ++ armorDropChances.addFloat(dropChances.getFloat(i)); ++ } else { ++ armorDropChances.addFloat(0.0F); ++ } ++ } ++ } ++ } ++ ++ return null; ++ } ++ }); ++ ++ registerMob("ArmorStand"); ++ registerMob("Creeper"); ++ registerMob("Skeleton"); ++ registerMob("Spider"); ++ registerMob("Giant"); ++ registerMob("Zombie"); ++ registerMob("Slime"); ++ registerMob("Ghast"); ++ registerMob("PigZombie"); ++ registerMob("Enderman"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "Enderman", new DataWalkerBlockNames("carried")); ++ registerMob("CaveSpider"); ++ registerMob("Silverfish"); ++ registerMob("Blaze"); ++ registerMob("LavaSlime"); ++ registerMob("EnderDragon"); ++ registerMob("WitherBoss"); ++ registerMob("Bat"); ++ registerMob("Witch"); ++ registerMob("Endermite"); ++ registerMob("Guardian"); ++ registerMob("Pig"); ++ registerMob("Sheep"); ++ registerMob("Cow"); ++ registerMob("Chicken"); ++ registerMob("Squid"); ++ registerMob("Wolf"); ++ registerMob("MushroomCow"); ++ registerMob("SnowMan"); ++ registerMob("Ozelot"); ++ registerMob("VillagerGolem"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "EntityHorse", new DataWalkerItemLists("Items", "ArmorItems", "HandItems")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "EntityHorse", new DataWalkerItems("ArmorItem", "SaddleItem")); ++ registerMob("Rabbit"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "Villager", (final MapType data, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, data, "Inventory", fromVersion, toVersion); ++ ++ WalkerUtils.convertList(MCTypeRegistry.VILLAGER_TRADE, data.getMap("Offers"), "Recipes", fromVersion, toVersion); ++ ++ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, data, "ArmorItems", fromVersion, toVersion); ++ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, data, "HandItems", fromVersion, toVersion); ++ ++ return null; ++ }); ++ registerMob("Shulker"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "AreaEffectCloud", new DataWalkerTypePaths<>(MCTypeRegistry.PARTICLE, "Particle")); ++ ++ MCTypeRegistry.STRUCTURE.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ final ListType entities = data.getList("entities", ObjectType.MAP); ++ if (entities != null) { ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ WalkerUtils.convert(MCTypeRegistry.ENTITY, entities.getMap(i), "nbt", fromVersion, toVersion); ++ } ++ } ++ ++ final ListType blocks = data.getList("blocks", ObjectType.MAP); ++ if (blocks != null) { ++ for (int i = 0, len = blocks.size(); i < len; ++i) { ++ WalkerUtils.convert(MCTypeRegistry.TILE_ENTITY, blocks.getMap(i), "nbt", fromVersion, toVersion); ++ } ++ } ++ ++ WalkerUtils.convertList(MCTypeRegistry.BLOCK_STATE, data, "palette", fromVersion, toVersion); ++ ++ return null; ++ }); ++ } ++ ++ private V100() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V101.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V101.java +new file mode 100644 +index 0000000000000000000000000000000000000000..96ea4669d6ad893ba52af045082f9ccb90fe90bf +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V101.java +@@ -0,0 +1,38 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.util.ComponentUtils; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V101 { ++ ++ private static final int VERSION = MCVersions.V15W32A + 1; ++ ++ private static void updateLine(final MapType data, final String path) { ++ final String textString = data.getString(path); ++ ++ if (textString == null) { ++ return; ++ } ++ ++ data.setString(path, ComponentUtils.convertFromLenient(textString)); ++ } ++ ++ public static void register() { ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("Sign", new DataConverter<>(VERSION) { ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ updateLine(data, "Text1"); ++ updateLine(data, "Text2"); ++ updateLine(data, "Text3"); ++ updateLine(data, "Text4"); ++ return null; ++ } ++ }); ++ } ++ ++ private V101() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V102.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V102.java +new file mode 100644 +index 0000000000000000000000000000000000000000..00bb3cff8f3d220d65a18f9b82b4b5361588b109 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V102.java +@@ -0,0 +1,86 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.HelperItemNameV102; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.Types; ++import com.mojang.logging.LogUtils; ++import org.slf4j.Logger; ++ ++public final class V102 { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ private static final int VERSION = MCVersions.V15W32A + 2; ++ ++ public static void register() { ++ // V102 -> V15W32A + 2 ++ // V102 schema only modifies ITEM_STACK to have only a string ID, but our ITEM_NAME is generic (int or String) so we don't ++ // actually need to update the walker ++ ++ MCTypeRegistry.ITEM_NAME.addConverter(new DataConverter<>(VERSION) { ++ @Override ++ public Object convert(final Object data, final long sourceVersion, final long toVersion) { ++ if (!(data instanceof Number)) { ++ return null; ++ } ++ final int id = ((Number)data).intValue(); ++ final String remap = HelperItemNameV102.getNameFromId(id); ++ if (remap == null) { ++ LOGGER.warn("Unknown legacy integer id (V102) " + id); ++ } ++ return remap == null ? HelperItemNameV102.getNameFromId(0) : remap; ++ } ++ }); ++ ++ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (!data.hasKey("id", ObjectType.NUMBER)) { ++ return null; ++ } ++ ++ final int id = data.getInt("id"); ++ ++ String remap = HelperItemNameV102.getNameFromId(id); ++ if (remap == null) { ++ LOGGER.warn("Unknown legacy integer id (V102) " + id); ++ remap = HelperItemNameV102.getNameFromId(0); ++ } ++ ++ data.setString("id", remap); ++ ++ return null; ++ } ++ }); ++ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:potion", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final short damage = data.getShort("Damage"); ++ if (damage != 0) { ++ data.setShort("Damage", (short)0); ++ } ++ MapType tag = data.getMap("tag"); ++ if (tag == null) { ++ tag = Types.NBT.createEmptyMap(); ++ data.setMap("tag", tag); ++ } ++ ++ if (!tag.hasKey("Potion", ObjectType.STRING)) { ++ final String converted = HelperItemNameV102.getPotionNameFromId(damage); ++ tag.setString("Potion", converted == null ? "minecraft:water" : converted); ++ if ((damage & 16384) == 16384) { ++ data.setString("id", "minecraft:splash_potion"); ++ } ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V102() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1022.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1022.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c1510d9167f466d3b7e3756353224f12f3876442 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1022.java +@@ -0,0 +1,41 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V1022 { ++ ++ private static final int VERSION = MCVersions.V17W06A; ++ ++ public static void register() { ++ MCTypeRegistry.PLAYER.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convert(MCTypeRegistry.ENTITY, data.getMap("RootVehicle"), "Entity", fromVersion, toVersion); ++ ++ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, data, "Inventory", fromVersion, toVersion); ++ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, data, "EnderItems", fromVersion, toVersion); ++ ++ WalkerUtils.convert(MCTypeRegistry.ENTITY, data, "ShoulderEntityLeft", fromVersion, toVersion); ++ WalkerUtils.convert(MCTypeRegistry.ENTITY, data, "ShoulderEntityRight", fromVersion, toVersion); ++ ++ final MapType recipeBook = data.getMap("recipeBook"); ++ if (recipeBook != null) { ++ WalkerUtils.convertList(MCTypeRegistry.RECIPE, recipeBook, "recipes", fromVersion, toVersion); ++ WalkerUtils.convertList(MCTypeRegistry.RECIPE, recipeBook, "toBeDisplayed", fromVersion, toVersion); ++ } ++ ++ return null; ++ }); ++ ++ MCTypeRegistry.HOTBAR.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ for (final String key : data.keys()) { ++ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, data, key, fromVersion, toVersion); ++ } ++ ++ return null; ++ }); ++ } ++ ++ private V1022() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V105.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V105.java +new file mode 100644 +index 0000000000000000000000000000000000000000..189b682da7eacea118610e466e8648675fccf776 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V105.java +@@ -0,0 +1,49 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.HelperSpawnEggNameV105; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.Types; ++ ++public final class V105 { ++ ++ private static final int VERSION = MCVersions.V15W32C + 1; ++ ++ public static void register() { ++ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:spawn_egg", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ MapType tag = data.getMap("tag"); ++ if (tag == null) { ++ tag = Types.NBT.createEmptyMap(); ++ } ++ ++ final short damage = data.getShort("Damage"); ++ if (damage != 0) { ++ data.setShort("Damage", (short)0); ++ } ++ ++ MapType entityTag = tag.getMap("EntityTag"); ++ if (entityTag == null) { ++ entityTag = Types.NBT.createEmptyMap(); ++ } ++ ++ if (!entityTag.hasKey("id", ObjectType.STRING)) { ++ final String converted = HelperSpawnEggNameV105.getSpawnNameFromId(damage); ++ if (converted != null) { ++ entityTag.setString("id", converted); ++ tag.setMap("EntityTag", entityTag); ++ data.setMap("tag", tag); ++ } ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V105() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V106.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V106.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fa9b11b46f0fbcaabcaed02a7fc3f5af3337ec27 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V106.java +@@ -0,0 +1,83 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.Types; ++ ++public final class V106 { ++ ++ private static final int VERSION = MCVersions.V15W32C + 2; ++ ++ public static void register() { ++ // V106 -> V15W32C + 2 ++ ++ MCTypeRegistry.UNTAGGED_SPAWNER.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ // While all converters for spawners check the id for this version, we don't because spawners exist in minecarts. ooops! Loading a chunk ++ // with a minecart spawner from 1.7.10 in 1.16.5 vanilla will fail to convert! Clearly there was a mistake in how they ++ // used and applied spawner converters. In anycase, do not check the id - we are not guaranteed to be a tile ++ // entity. We can be a regular old minecart spawner. And we know we are a spawner because this is only called from data walkers. ++ ++ final String entityId = data.getString("EntityId"); ++ if (entityId != null) { ++ data.remove("EntityId"); ++ MapType spawnData = data.getMap("SpawnData"); ++ if (spawnData == null) { ++ spawnData = Types.NBT.createEmptyMap(); ++ data.setMap("SpawnData", spawnData); ++ } ++ spawnData.setString("id", entityId.isEmpty() ? "Pig" : entityId); ++ } ++ ++ final ListType spawnPotentials = data.getList("SpawnPotentials", ObjectType.MAP); ++ if (spawnPotentials != null) { ++ for (int i = 0, len = spawnPotentials.size(); i < len; ++i) { ++ // convert to standard entity format (it's not a coincidence a walker for spawners is only added ++ // in this version) ++ final MapType spawn = spawnPotentials.getMap(i); ++ final String spawnType = spawn.getString("Type"); ++ if (spawnType == null) { ++ continue; ++ } ++ spawn.remove("Type"); ++ ++ MapType properties = spawn.getMap("Properties"); ++ if (properties == null) { ++ properties = Types.NBT.createEmptyMap(); ++ } else { ++ spawn.remove("Properties"); ++ } ++ ++ properties.setString("id", spawnType); ++ ++ spawn.setMap("Entity", properties); ++ } ++ } ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.UNTAGGED_SPAWNER.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ final ListType spawnPotentials = data.getList("SpawnPotentials", ObjectType.MAP); ++ if (spawnPotentials != null) { ++ for (int i = 0, len = spawnPotentials.size(); i < len; ++i) { ++ final MapType spawnPotential = spawnPotentials.getMap(i); ++ WalkerUtils.convert(MCTypeRegistry.ENTITY, spawnPotential, "Entity", fromVersion, toVersion); ++ } ++ } ++ ++ WalkerUtils.convert(MCTypeRegistry.ENTITY, data, "SpawnData", fromVersion, toVersion); ++ ++ return null; ++ }); ++ } ++ ++ private V106() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V107.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V107.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e9d288c41c40d96ac7c6b605babc436d6a5796f3 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V107.java +@@ -0,0 +1,43 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V107 { ++ ++ private static final int VERSION = MCVersions.V15W32C + 3; ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("Minecart", new DataConverter<>(VERSION) { ++ protected final String[] MINECART_IDS = new String[] { ++ "MinecartRideable", // 0 ++ "MinecartChest", // 1 ++ "MinecartFurnace", // 2 ++ "MinecartTNT", // 3 ++ "MinecartSpawner", // 4 ++ "MinecartHopper", // 5 ++ "MinecartCommandBlock" // 6 ++ }; ++ // Vanilla does not use all of the IDs here. The legacy (pre DFU) code does, so I'm going to use them. ++ // No harm in catching more cases here. ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ String newId = "MinecartRideable"; // dfl ++ final int type = data.getInt("Type"); ++ data.remove("Type"); ++ ++ if (type >= 0 && type < MINECART_IDS.length) { ++ newId = MINECART_IDS[type]; ++ } ++ data.setString("id", newId); ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V107() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V108.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V108.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ba9487bc35bedfd7261d4a4fd9476de070f65f33 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V108.java +@@ -0,0 +1,46 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import com.mojang.logging.LogUtils; ++import org.slf4j.Logger; ++import java.util.UUID; ++ ++public final class V108 { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ private static final int VERSION = MCVersions.V15W32C + 4; ++ ++ public static void register() { ++ // Convert String UUID into UUIDMost and UUIDLeast ++ MCTypeRegistry.ENTITY.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final String uuidString = data.getString("UUID"); ++ ++ if (uuidString == null) { ++ return null; ++ } ++ data.remove("UUID"); ++ ++ final UUID uuid; ++ try { ++ uuid = UUID.fromString(uuidString); ++ } catch (final Exception ex) { ++ LOGGER.warn("Failed to parse UUID for legacy entity (V108): " + uuidString, ex); ++ return null; ++ } ++ ++ data.setLong("UUIDMost", uuid.getMostSignificantBits()); ++ data.setLong("UUIDLeast", uuid.getLeastSignificantBits()); ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V108() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V109.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V109.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5df0c8da6415a4651e5678a170bc8ff32dd66337 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V109.java +@@ -0,0 +1,41 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V109 { ++ ++ private static final int VERSION = MCVersions.V15W32C + 5; ++ ++ public static void register() { ++ // Converts health to be in float, and cleans up whatever the hell was going on with HealF and Health... ++ MCTypeRegistry.ENTITY.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final Number healF = data.getNumber("HealF"); ++ final Number heal = data.getNumber("Health"); ++ ++ final float newHealth; ++ ++ if (healF != null) { ++ data.remove("HealF"); ++ newHealth = healF.floatValue(); ++ } else { ++ if (heal == null) { ++ return null; ++ } ++ ++ newHealth = heal.floatValue(); ++ } ++ ++ data.setFloat("Health", newHealth); ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V109() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V110.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V110.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b089fc93b88c5a7b4bb1eb0e105120b5393de1b1 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V110.java +@@ -0,0 +1,38 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.Types; ++ ++public final class V110 { ++ ++ private static final int VERSION = MCVersions.V15W32C + 6; ++ ++ public static void register() { ++ // Moves the Saddle boolean to be an actual saddle item. Note: The data walker for the SaddleItem exists ++ // in V99, it doesn't need to be added here. ++ MCTypeRegistry.ENTITY.addConverterForId("EntityHorse", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (!data.getBoolean("Saddle") || data.hasKey("SaddleItem", ObjectType.MAP)) { ++ return null; ++ } ++ ++ final MapType saddleItem = Types.NBT.createEmptyMap(); ++ data.remove("Saddle"); ++ data.setMap("SaddleItem", saddleItem); ++ ++ saddleItem.setString("id", "minecraft:saddle"); ++ saddleItem.setByte("Count", (byte)1); ++ saddleItem.setShort("Damage", (short)0); ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V110() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V111.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V111.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0c69cf9b419049dc5338abb408fa3f0390e4e353 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V111.java +@@ -0,0 +1,64 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V111 { ++ ++ private static final int VERSION = MCVersions.V15W33B; ++ ++ public static void register() { ++ final EntityRotationFix rotationFix = new EntityRotationFix(VERSION); ++ MCTypeRegistry.ENTITY.addConverterForId("Painting", rotationFix); ++ MCTypeRegistry.ENTITY.addConverterForId("ItemFrame", rotationFix); ++ } ++ ++ private V111() {} ++ ++ protected static final class EntityRotationFix extends DataConverter, MapType> { ++ ++ private static final int[][] DIRECTIONS = new int[][] { ++ {0, 0, 1}, ++ {-1, 0, 0}, ++ {0, 0, -1}, ++ {1, 0, 0} ++ }; ++ ++ public EntityRotationFix(final int version) { ++ super(version); ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (data.getNumber("Facing") != null) { ++ return null; ++ } ++ ++ final Number direction = data.getNumber("Direction"); ++ final int facing; ++ if (direction != null) { ++ data.remove("Direction"); ++ facing = direction.intValue() % DIRECTIONS.length; ++ final int[] offsets = DIRECTIONS[facing]; ++ data.setInt("TileX", data.getInt("TileX") + offsets[0]); ++ data.setInt("TileY", data.getInt("TileY") + offsets[1]); ++ data.setInt("TileZ", data.getInt("TileZ") + offsets[2]); ++ if ("ItemFrame".equals(data.getString("id"))) { ++ final Number rotation = data.getNumber("ItemRotation"); ++ if (rotation != null) { ++ data.setByte("ItemRotation", (byte)(rotation.byteValue() * 2)); ++ } ++ } ++ } else { ++ facing = data.getByte("Dir") % DIRECTIONS.length; ++ data.remove("Dir"); ++ } ++ ++ data.setByte("Facing", (byte)facing); ++ ++ return null; ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1125.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1125.java +new file mode 100644 +index 0000000000000000000000000000000000000000..41ceef54e202420616ad57e9f9c200457c7d2848 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1125.java +@@ -0,0 +1,101 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.hooks.DataHookValueTypeEnforceNamespaced; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.Types; ++ ++public final class V1125 { ++ ++ private static final int VERSION = MCVersions.V17W15A; ++ private static final int BED_BLOCK_ID = 416; ++ ++ public static void register() { ++ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType level = data.getMap("Level"); ++ if (level == null) { ++ return null; ++ } ++ ++ final int chunkX = level.getInt("xPos"); ++ final int chunkZ = level.getInt("zPos"); ++ ++ final ListType sections = level.getList("Sections", ObjectType.MAP); ++ if (sections == null) { ++ return null; ++ } ++ ++ ListType tileEntities = level.getList("TileEntities", ObjectType.MAP); ++ if (tileEntities == null) { ++ tileEntities = Types.NBT.createEmptyList(); ++ level.setList("TileEntities", tileEntities); ++ } ++ ++ for (int i = 0, len = sections.size(); i < len; ++i) { ++ final MapType section = sections.getMap(i); ++ ++ final byte sectionY = section.getByte("Y"); ++ final byte[] blocks = section.getBytes("Blocks"); ++ ++ if (blocks == null) { ++ continue; ++ } ++ ++ for (int blockIndex = 0; blockIndex < blocks.length; ++blockIndex) { ++ if (BED_BLOCK_ID != ((blocks[blockIndex] & 255) << 4)) { ++ continue; ++ } ++ ++ final int localX = blockIndex & 15; ++ final int localZ = (blockIndex >> 4) & 15; ++ final int localY = (blockIndex >> 8) & 15; ++ ++ final MapType newTile = Types.NBT.createEmptyMap(); ++ newTile.setString("id", "minecraft:bed"); ++ newTile.setInt("x", localX + (chunkX << 4)); ++ newTile.setInt("y", localY + (sectionY << 4)); ++ newTile.setInt("z", localZ + (chunkZ << 4)); ++ newTile.setShort("color", (short)14); // Red ++ ++ tileEntities.addMap(newTile); ++ } ++ } ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:bed", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (data.getShort("Damage") == 0) { ++ data.setShort("Damage", (short)14); // Red ++ } ++ ++ return null; ++ } ++ }); ++ ++ ++ MCTypeRegistry.ADVANCEMENTS.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convertKeys(MCTypeRegistry.BIOME, data.getMap("minecraft:adventure/adventuring_time"), "criteria", fromVersion, toVersion); ++ WalkerUtils.convertKeys(MCTypeRegistry.ENTITY_NAME, data.getMap("minecraft:adventure/kill_a_mob"), "criteria", fromVersion, toVersion); ++ WalkerUtils.convertKeys(MCTypeRegistry.ENTITY_NAME, data.getMap("minecraft:adventure/kill_all_mobs"), "criteria", fromVersion, toVersion); ++ WalkerUtils.convertKeys(MCTypeRegistry.ENTITY_NAME, data.getMap("minecraft:adventure/bred_all_animals"), "criteria", fromVersion, toVersion); ++ ++ return null; ++ }); ++ ++ // Enforce namespacing for ids ++ MCTypeRegistry.BIOME.addStructureHook(VERSION, new DataHookValueTypeEnforceNamespaced()); ++ } ++ ++ private V1125() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V113.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V113.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7b7d02eac9e121c45b557b664e156327d182c015 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V113.java +@@ -0,0 +1,40 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V113 { ++ ++ private static final int VERSION = MCVersions.V15W33C + 1; ++ ++ private static void checkList(final MapType data, final String id, final int requiredLength) { ++ final ListType list = data.getList(id, ObjectType.FLOAT); ++ if (list != null && list.size() == requiredLength) { ++ for (int i = 0; i < requiredLength; ++i) { ++ if (list.getFloat(i) != 0.0F) { ++ return; ++ } ++ } ++ } ++ ++ data.remove(id); ++ } ++ ++ public static void register() { ++ // Removes "HandDropChances" and "ArmorDropChances" if they're empty. ++ MCTypeRegistry.ENTITY.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ checkList(data, "HandDropChances", 2); ++ checkList(data, "ArmorDropChances", 4); ++ return null; ++ } ++ }); ++ } ++ ++ private V113() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1344.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1344.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b735165f9b296730b77339875255aa982e18a40a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1344.java +@@ -0,0 +1,176 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; ++ ++public final class V1344 { ++ ++ private static final int VERSION = MCVersions.V1_12_2 + 1; ++ ++ private static final Int2ObjectOpenHashMap BUTTON_ID_TO_NAME = new Int2ObjectOpenHashMap<>(); ++ static { ++ BUTTON_ID_TO_NAME.put(0, "key.unknown"); ++ BUTTON_ID_TO_NAME.put(11, "key.0"); ++ BUTTON_ID_TO_NAME.put(2, "key.1"); ++ BUTTON_ID_TO_NAME.put(3, "key.2"); ++ BUTTON_ID_TO_NAME.put(4, "key.3"); ++ BUTTON_ID_TO_NAME.put(5, "key.4"); ++ BUTTON_ID_TO_NAME.put(6, "key.5"); ++ BUTTON_ID_TO_NAME.put(7, "key.6"); ++ BUTTON_ID_TO_NAME.put(8, "key.7"); ++ BUTTON_ID_TO_NAME.put(9, "key.8"); ++ BUTTON_ID_TO_NAME.put(10, "key.9"); ++ BUTTON_ID_TO_NAME.put(30, "key.a"); ++ BUTTON_ID_TO_NAME.put(40, "key.apostrophe"); ++ BUTTON_ID_TO_NAME.put(48, "key.b"); ++ BUTTON_ID_TO_NAME.put(43, "key.backslash"); ++ BUTTON_ID_TO_NAME.put(14, "key.backspace"); ++ BUTTON_ID_TO_NAME.put(46, "key.c"); ++ BUTTON_ID_TO_NAME.put(58, "key.caps.lock"); ++ BUTTON_ID_TO_NAME.put(51, "key.comma"); ++ BUTTON_ID_TO_NAME.put(32, "key.d"); ++ BUTTON_ID_TO_NAME.put(211, "key.delete"); ++ BUTTON_ID_TO_NAME.put(208, "key.down"); ++ BUTTON_ID_TO_NAME.put(18, "key.e"); ++ BUTTON_ID_TO_NAME.put(207, "key.end"); ++ BUTTON_ID_TO_NAME.put(28, "key.enter"); ++ BUTTON_ID_TO_NAME.put(13, "key.equal"); ++ BUTTON_ID_TO_NAME.put(1, "key.escape"); ++ BUTTON_ID_TO_NAME.put(33, "key.f"); ++ BUTTON_ID_TO_NAME.put(59, "key.f1"); ++ BUTTON_ID_TO_NAME.put(68, "key.f10"); ++ BUTTON_ID_TO_NAME.put(87, "key.f11"); ++ BUTTON_ID_TO_NAME.put(88, "key.f12"); ++ BUTTON_ID_TO_NAME.put(100, "key.f13"); ++ BUTTON_ID_TO_NAME.put(101, "key.f14"); ++ BUTTON_ID_TO_NAME.put(102, "key.f15"); ++ BUTTON_ID_TO_NAME.put(103, "key.f16"); ++ BUTTON_ID_TO_NAME.put(104, "key.f17"); ++ BUTTON_ID_TO_NAME.put(105, "key.f18"); ++ BUTTON_ID_TO_NAME.put(113, "key.f19"); ++ BUTTON_ID_TO_NAME.put(60, "key.f2"); ++ BUTTON_ID_TO_NAME.put(61, "key.f3"); ++ BUTTON_ID_TO_NAME.put(62, "key.f4"); ++ BUTTON_ID_TO_NAME.put(63, "key.f5"); ++ BUTTON_ID_TO_NAME.put(64, "key.f6"); ++ BUTTON_ID_TO_NAME.put(65, "key.f7"); ++ BUTTON_ID_TO_NAME.put(66, "key.f8"); ++ BUTTON_ID_TO_NAME.put(67, "key.f9"); ++ BUTTON_ID_TO_NAME.put(34, "key.g"); ++ BUTTON_ID_TO_NAME.put(41, "key.grave.accent"); ++ BUTTON_ID_TO_NAME.put(35, "key.h"); ++ BUTTON_ID_TO_NAME.put(199, "key.home"); ++ BUTTON_ID_TO_NAME.put(23, "key.i"); ++ BUTTON_ID_TO_NAME.put(210, "key.insert"); ++ BUTTON_ID_TO_NAME.put(36, "key.j"); ++ BUTTON_ID_TO_NAME.put(37, "key.k"); ++ BUTTON_ID_TO_NAME.put(82, "key.keypad.0"); ++ BUTTON_ID_TO_NAME.put(79, "key.keypad.1"); ++ BUTTON_ID_TO_NAME.put(80, "key.keypad.2"); ++ BUTTON_ID_TO_NAME.put(81, "key.keypad.3"); ++ BUTTON_ID_TO_NAME.put(75, "key.keypad.4"); ++ BUTTON_ID_TO_NAME.put(76, "key.keypad.5"); ++ BUTTON_ID_TO_NAME.put(77, "key.keypad.6"); ++ BUTTON_ID_TO_NAME.put(71, "key.keypad.7"); ++ BUTTON_ID_TO_NAME.put(72, "key.keypad.8"); ++ BUTTON_ID_TO_NAME.put(73, "key.keypad.9"); ++ BUTTON_ID_TO_NAME.put(78, "key.keypad.add"); ++ BUTTON_ID_TO_NAME.put(83, "key.keypad.decimal"); ++ BUTTON_ID_TO_NAME.put(181, "key.keypad.divide"); ++ BUTTON_ID_TO_NAME.put(156, "key.keypad.enter"); ++ BUTTON_ID_TO_NAME.put(141, "key.keypad.equal"); ++ BUTTON_ID_TO_NAME.put(55, "key.keypad.multiply"); ++ BUTTON_ID_TO_NAME.put(74, "key.keypad.subtract"); ++ BUTTON_ID_TO_NAME.put(38, "key.l"); ++ BUTTON_ID_TO_NAME.put(203, "key.left"); ++ BUTTON_ID_TO_NAME.put(56, "key.left.alt"); ++ BUTTON_ID_TO_NAME.put(26, "key.left.bracket"); ++ BUTTON_ID_TO_NAME.put(29, "key.left.control"); ++ BUTTON_ID_TO_NAME.put(42, "key.left.shift"); ++ BUTTON_ID_TO_NAME.put(219, "key.left.win"); ++ BUTTON_ID_TO_NAME.put(50, "key.m"); ++ BUTTON_ID_TO_NAME.put(12, "key.minus"); ++ BUTTON_ID_TO_NAME.put(49, "key.n"); ++ BUTTON_ID_TO_NAME.put(69, "key.num.lock"); ++ BUTTON_ID_TO_NAME.put(24, "key.o"); ++ BUTTON_ID_TO_NAME.put(25, "key.p"); ++ BUTTON_ID_TO_NAME.put(209, "key.page.down"); ++ BUTTON_ID_TO_NAME.put(201, "key.page.up"); ++ BUTTON_ID_TO_NAME.put(197, "key.pause"); ++ BUTTON_ID_TO_NAME.put(52, "key.period"); ++ BUTTON_ID_TO_NAME.put(183, "key.print.screen"); ++ BUTTON_ID_TO_NAME.put(16, "key.q"); ++ BUTTON_ID_TO_NAME.put(19, "key.r"); ++ BUTTON_ID_TO_NAME.put(205, "key.right"); ++ BUTTON_ID_TO_NAME.put(184, "key.right.alt"); ++ BUTTON_ID_TO_NAME.put(27, "key.right.bracket"); ++ BUTTON_ID_TO_NAME.put(157, "key.right.control"); ++ BUTTON_ID_TO_NAME.put(54, "key.right.shift"); ++ BUTTON_ID_TO_NAME.put(220, "key.right.win"); ++ BUTTON_ID_TO_NAME.put(31, "key.s"); ++ BUTTON_ID_TO_NAME.put(70, "key.scroll.lock"); ++ BUTTON_ID_TO_NAME.put(39, "key.semicolon"); ++ BUTTON_ID_TO_NAME.put(53, "key.slash"); ++ BUTTON_ID_TO_NAME.put(57, "key.space"); ++ BUTTON_ID_TO_NAME.put(20, "key.t"); ++ BUTTON_ID_TO_NAME.put(15, "key.tab"); ++ BUTTON_ID_TO_NAME.put(22, "key.u"); ++ BUTTON_ID_TO_NAME.put(200, "key.up"); ++ BUTTON_ID_TO_NAME.put(47, "key.v"); ++ BUTTON_ID_TO_NAME.put(17, "key.w"); ++ BUTTON_ID_TO_NAME.put(45, "key.x"); ++ BUTTON_ID_TO_NAME.put(21, "key.y"); ++ BUTTON_ID_TO_NAME.put(44, "key.z"); ++ } ++ ++ public static void register() { ++ MCTypeRegistry.OPTIONS.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ for (final String key : data.keys()) { ++ if (!key.startsWith("key_")) { ++ continue; ++ } ++ final String value = data.getString(key); ++ final int code; ++ try { ++ code = Integer.parseInt(value); ++ } catch (final NumberFormatException ex) { ++ continue; ++ } ++ ++ final String newEntry; ++ ++ if (code < 0) { ++ final int mouseCode = code + 100; ++ switch (mouseCode) { ++ case 0: ++ newEntry = "key.mouse.left"; ++ break; ++ case 1: ++ newEntry = "key.mouse.right"; ++ break; ++ case 2: ++ newEntry = "key.mouse.middle"; ++ break; ++ default: ++ newEntry = "key.mouse." + (mouseCode + 1); ++ break; ++ } ++ } else { ++ newEntry = BUTTON_ID_TO_NAME.getOrDefault(code, "key.unknown"); ++ } ++ ++ // No CMEs occur for existing entries in maps. ++ data.setString(key, newEntry); ++ } ++ return null; ++ } ++ }); ++ } ++ ++ private V1344() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V135.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V135.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1c9f754332d8a2299b7e2e5565882da52948d3a9 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V135.java +@@ -0,0 +1,60 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.Types; ++ ++public final class V135 { ++ ++ private static final int VERSION = MCVersions.V15W40B + 1; ++ ++ public static void register() { ++ // In this update they changed the "Riding" value to be "Passengers", which is now a list. So it added ++ // support for multiple entities riding. Of course, Riding and Passenger are opposites - so it also will ++ // switch the data layout to be from highest rider to lowest rider, in terms of depth. ++ MCTypeRegistry.ENTITY.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(MapType data, final long sourceVersion, final long toVersion) { ++ MapType ret = null; ++ while (data.hasKey("Riding", ObjectType.MAP)) { ++ final MapType riding = data.getMap("Riding"); ++ data.remove("Riding"); ++ ++ final ListType passengers = Types.NBT.createEmptyList(); ++ riding.setList("Passengers", passengers); ++ passengers.addMap(data); ++ ++ ret = data = riding; ++ } ++ ++ return ret; ++ } ++ }); ++ ++ ++ MCTypeRegistry.PLAYER.addStructureWalker(VERSION, new DataWalkerItemLists("Inventory", "EnderItems")); ++ MCTypeRegistry.PLAYER.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ final MapType rootVehicle = data.getMap("RootVehicle"); ++ if (rootVehicle != null) { ++ WalkerUtils.convert(MCTypeRegistry.ENTITY, rootVehicle, "Entity", fromVersion, toVersion); ++ } ++ ++ return null; ++ }); ++ ++ MCTypeRegistry.ENTITY.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convertList(MCTypeRegistry.ENTITY, data, "Passengers", fromVersion, toVersion); ++ ++ return null; ++ }); ++ ++ } ++ ++ private V135() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V143.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V143.java +new file mode 100644 +index 0000000000000000000000000000000000000000..90889dddd8a510fe69c47413f5fe3ed4a756fedb +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V143.java +@@ -0,0 +1,17 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterAbstractEntityRename; ++ ++public final class V143 { ++ ++ private static final int VERSION = MCVersions.V15W44B; ++ ++ public static void register() { ++ ConverterAbstractEntityRename.register(VERSION, (final String input) -> { ++ return "TippedArrow".equals(input) ? "Arrow" : null; ++ }); ++ } ++ ++ private V143() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1446.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1446.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0e198bef171c92d53725d338bb793b1e269f2997 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1446.java +@@ -0,0 +1,35 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V1446 { ++ ++ private static final int VERSION = MCVersions.V17W43B + 1; ++ ++ public static void register() { ++ MCTypeRegistry.OPTIONS.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ for (final String key : data.keys()) { ++ if (!key.startsWith("key_")) { ++ continue; ++ } ++ ++ final String value = data.getString(key); ++ ++ if (value.startsWith("key.mouse") || value.startsWith("scancode.")) { ++ continue; ++ } ++ ++ data.setString(key, "key.keyboard." + value.substring("key.".length())); ++ } ++ return null; ++ } ++ }); ++ } ++ ++ private V1446() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1450.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1450.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bf6f57bc84785622aea35dc70872db6d4d9516a1 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1450.java +@@ -0,0 +1,24 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.HelperBlockFlatteningV1450; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V1450 { ++ ++ private static final int VERSION = MCVersions.V17W46A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.BLOCK_STATE.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType ret = HelperBlockFlatteningV1450.flattenNBT(data); ++ return ret == data ? null : ret.copy(); // copy to avoid problems with later state datafixers ++ } ++ }); ++ } ++ ++ private V1450() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1451.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1451.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fd46b075f3c18c52e31aeee4cb4638d5d7b673c3 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1451.java +@@ -0,0 +1,513 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.converters.datatypes.DataHook; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.chunk.ConverterFlattenChunk; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.HelperBlockFlatteningV1450; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.HelperItemNameV102; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemstack.ConverterFlattenItemStack; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemstack.ConverterFlattenSpawnEgg; ++import ca.spottedleaf.dataconverter.minecraft.converters.stats.ConverterFlattenStats; ++import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterFlattenEntity; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; ++import ca.spottedleaf.dataconverter.minecraft.walkers.tile_entity.DataWalkerTileEntities; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.Types; ++import com.google.common.base.Splitter; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.util.datafix.fixes.BlockStateData; ++import net.minecraft.util.datafix.fixes.EntityBlockStateFix; ++import org.apache.commons.lang3.math.NumberUtils; ++import java.util.Iterator; ++import java.util.List; ++import java.util.stream.Collectors; ++import java.util.stream.StreamSupport; ++ ++public final class V1451 { ++ ++ private static final int VERSION = MCVersions.V17W47A; ++ ++ public static String packWithDot(final String string) { ++ final ResourceLocation resourceLocation = ResourceLocation.tryParse(string); ++ return resourceLocation != null ? resourceLocation.getNamespace() + "." + resourceLocation.getPath() : string; ++ } ++ ++ public static void register() { ++ // V0 ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, 0, "minecraft:trapped_chest", new DataWalkerItemLists("Items")); ++ ++ // V1 ++ MCTypeRegistry.CHUNK.addStructureConverter(new ConverterFlattenChunk()); ++ ++ MCTypeRegistry.CHUNK.addStructureWalker(VERSION, 1, (final MapType data, final long fromVersion, final long toVersion) -> { ++ final MapType level = data.getMap("Level"); ++ if (level == null) { ++ return null; ++ } ++ ++ WalkerUtils.convertList(MCTypeRegistry.ENTITY, level, "Entities", fromVersion, toVersion); ++ WalkerUtils.convertList(MCTypeRegistry.TILE_ENTITY, level, "TileEntities", fromVersion, toVersion); ++ ++ final ListType tileTicks = level.getList("TileTicks", ObjectType.MAP); ++ if (tileTicks != null) { ++ for (int i = 0, len = tileTicks.size(); i < len; ++i) { ++ final MapType tileTick = tileTicks.getMap(i); ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, tileTick, "i", fromVersion, toVersion); ++ } ++ } ++ ++ final ListType sections = level.getList("Sections", ObjectType.MAP); ++ if (sections != null) { ++ for (int i = 0, len = sections.size(); i < len; ++i) { ++ final MapType section = sections.getMap(i); ++ WalkerUtils.convertList(MCTypeRegistry.BLOCK_STATE, section, "Palette", fromVersion, toVersion); ++ } ++ } ++ ++ return null; ++ }); ++ ++ // V2 ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:piston", new DataConverter<>(VERSION, 2) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final int blockId = data.getInt("blockId"); ++ final int blockData = data.getInt("blockData") & 15; ++ ++ data.remove("blockId"); ++ data.remove("blockData"); ++ ++ data.setMap("blockState", HelperBlockFlatteningV1450.getNBTForId((blockId << 4) | blockData).copy()); // copy to avoid problems with later state datafixers ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, 2, "minecraft:piston", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "blockState")); ++ ++ // V3 ++ ConverterFlattenEntity.register(); ++ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:filled_map", new DataConverter<>(VERSION, 3) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ MapType tag = data.getMap("tag"); ++ if (tag == null) { ++ tag = Types.NBT.createEmptyMap(); ++ data.setMap("tag", tag); ++ } ++ ++ if (!tag.hasKey("map", ObjectType.NUMBER)) { // This if is from CB. as usual, no documentation from CB. I'm guessing it just wants to avoid possibly overwriting it. seems fine. ++ tag.setInt("map", data.getInt("Damage")); ++ } ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:potion", new DataWalkerItems("Potion")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:arrow", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "inBlockState")); ++ V100.registerEquipment(VERSION, 3, "minecraft:enderman"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:enderman", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "carriedBlockState")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:falling_block", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "BlockState")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:falling_block", new DataWalkerTileEntities("TileEntityData")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:spectral_arrow", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "inBlockState")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:chest_minecart", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "DisplayState")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:chest_minecart", new DataWalkerItemLists("Items")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:commandblock_minecart", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "DisplayState")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:furnace_minecart", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "DisplayState")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:hopper_minecart", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "DisplayState")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:hopper_minecart", new DataWalkerItemLists("Items")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:minecart", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "DisplayState")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:spawner_minecart", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "DisplayState")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:spawner_minecart", (final MapType data, final long fromVersion, final long toVersion) -> { ++ MCTypeRegistry.UNTAGGED_SPAWNER.convert(data, fromVersion, toVersion); ++ return null; ++ }); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:tnt_minecart", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "DisplayState")); ++ ++ // V4 ++ MCTypeRegistry.BLOCK_NAME.addConverter(new DataConverter<>(VERSION, 4) { ++ @Override ++ public Object convert(final Object data, final long sourceVersion, final long toVersion) { ++ if (data instanceof Number) { ++ return HelperBlockFlatteningV1450.getNameForId(((Number)data).intValue()); ++ } else if (data instanceof String) { ++ return HelperBlockFlatteningV1450.getNewBlockName((String)data); // structure hook ensured data is namespaced ++ } ++ return null; ++ } ++ }); ++ MCTypeRegistry.ITEM_STACK.addStructureConverter(new ConverterFlattenItemStack()); ++ ++ // V5 ++ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:spawn_egg", new ConverterFlattenSpawnEgg(VERSION, 5)); ++ /* This datafixer has been disabled because the collar colour handler did not change from 1.12 -> 1.13 at all. ++ // So clearly somebody fucked up. This fixes wolf colours incorrectly converting between versions ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:wolf", new DataConverter<>(VERSION, 5) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final Number colour = data.getNumber("CollarColor"); ++ ++ if (colour != null) { ++ data.setByte("CollarColor", (byte)(15 - colour.intValue())); ++ } ++ ++ return null; ++ } ++ }); ++ */ ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:banner", new DataConverter<>(VERSION, 5) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final Number base = data.getNumber("Base"); ++ if (base != null) { ++ data.setInt("Base", 15 - base.intValue()); ++ } ++ ++ final ListType patterns = data.getList("Patterns", ObjectType.MAP); ++ if (patterns != null) { ++ for (int i = 0, len = patterns.size(); i < len; ++i) { ++ final MapType pattern = patterns.getMap(i); ++ final Number colour = pattern.getNumber("Color"); ++ if (colour != null) { ++ pattern.setInt("Color", 15 - colour.intValue()); ++ } ++ } ++ } ++ ++ return null; ++ } ++ }); ++ MCTypeRegistry.LEVEL.addStructureConverter(new DataConverter<>(VERSION, 5) { ++ private final Splitter SPLITTER = Splitter.on(';').limit(5); ++ private final Splitter LAYER_SPLITTER = Splitter.on(','); ++ private final Splitter OLD_AMOUNT_SPLITTER = Splitter.on('x').limit(2); ++ private final Splitter AMOUNT_SPLITTER = Splitter.on('*').limit(2); ++ private final Splitter BLOCK_SPLITTER = Splitter.on(':').limit(3); ++ ++ // idk man i just copy and pasted this one ++ private String fixGeneratorSettings(final String generatorSettings) { ++ if (generatorSettings.isEmpty()) { ++ return "minecraft:bedrock,2*minecraft:dirt,minecraft:grass_block;1;village"; ++ } else { ++ Iterator iterator = SPLITTER.split(generatorSettings).iterator(); ++ String string2 = (String)iterator.next(); ++ int j; ++ String string4; ++ if (iterator.hasNext()) { ++ j = NumberUtils.toInt(string2, 0); ++ string4 = (String)iterator.next(); ++ } else { ++ j = 0; ++ string4 = string2; ++ } ++ ++ if (j >= 0 && j <= 3) { ++ StringBuilder stringBuilder = new StringBuilder(); ++ Splitter splitter = j < 3 ? OLD_AMOUNT_SPLITTER : AMOUNT_SPLITTER; ++ stringBuilder.append((String) StreamSupport.stream(LAYER_SPLITTER.split(string4).spliterator(), false).map((stringx) -> { ++ List list = splitter.splitToList(stringx); ++ int k; ++ String string3; ++ if (list.size() == 2) { ++ k = NumberUtils.toInt((String)list.get(0)); ++ string3 = (String)list.get(1); ++ } else { ++ k = 1; ++ string3 = (String)list.get(0); ++ } ++ ++ List list2 = BLOCK_SPLITTER.splitToList(string3); ++ int l = ((String)list2.get(0)).equals("minecraft") ? 1 : 0; ++ String string5 = (String)list2.get(l); ++ int m = j == 3 ? EntityBlockStateFix.getBlockId("minecraft:" + string5) : NumberUtils.toInt(string5, 0); ++ int n = l + 1; ++ int o = list2.size() > n ? NumberUtils.toInt((String)list2.get(n), 0) : 0; ++ return (k == 1 ? "" : k + "*") + BlockStateData.getTag(m << 4 | o).get("Name").asString(""); ++ }).collect(Collectors.joining(","))); ++ ++ while(iterator.hasNext()) { ++ stringBuilder.append(';').append((String)iterator.next()); ++ } ++ ++ return stringBuilder.toString(); ++ } else { ++ return "minecraft:bedrock,2*minecraft:dirt,minecraft:grass_block;1;village"; ++ } ++ } ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (!"flat".equalsIgnoreCase(data.getString("generatorName"))) { ++ return null; ++ } ++ ++ final String generatorOptions = data.getString("generatorOptions"); ++ if (generatorOptions == null) { ++ return null; ++ } ++ ++ data.setString("generatorOptions", this.fixGeneratorSettings(generatorOptions)); ++ ++ return null; ++ } ++ }); ++ ++ // V6 ++ MCTypeRegistry.STATS.addStructureConverter(ConverterFlattenStats.makeStatsConverter()); ++ MCTypeRegistry.OBJECTIVE.addStructureConverter(ConverterFlattenStats.makeObjectiveConverter()); ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:jukebox", new DataConverter<>(VERSION, 6) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final int record = data.getInt("Record"); ++ if (record <= 0) { ++ return null; ++ } ++ ++ data.remove("Record"); ++ ++ final String newItemId = ConverterFlattenItemStack.flattenItem(HelperItemNameV102.getNameFromId(record), 0); ++ if (newItemId == null) { ++ return null; ++ } ++ ++ final MapType recordItem = Types.NBT.createEmptyMap(); ++ data.setMap("RecordItem", recordItem); ++ ++ recordItem.setString("id", newItemId); ++ recordItem.setByte("Count", (byte)1); ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.STATS.addStructureWalker(VERSION, 6, (final MapType data, final long fromVersion, final long toVersion) -> { ++ final MapType stats = data.getMap("stats"); ++ if (stats == null) { ++ return null; ++ } ++ ++ WalkerUtils.convertKeys(MCTypeRegistry.BLOCK_NAME, stats, "minecraft:mined", fromVersion, toVersion); ++ ++ WalkerUtils.convertKeys(MCTypeRegistry.ITEM_NAME, stats, "minecraft:crafted", fromVersion, toVersion); ++ WalkerUtils.convertKeys(MCTypeRegistry.ITEM_NAME, stats, "minecraft:used", fromVersion, toVersion); ++ WalkerUtils.convertKeys(MCTypeRegistry.ITEM_NAME, stats, "minecraft:broken", fromVersion, toVersion); ++ WalkerUtils.convertKeys(MCTypeRegistry.ITEM_NAME, stats, "minecraft:picked_up", fromVersion, toVersion); ++ WalkerUtils.convertKeys(MCTypeRegistry.ITEM_NAME, stats, "minecraft:dropped", fromVersion, toVersion); ++ ++ WalkerUtils.convertKeys(MCTypeRegistry.ENTITY_NAME, stats, "minecraft:killed", fromVersion, toVersion); ++ WalkerUtils.convertKeys(MCTypeRegistry.ENTITY_NAME, stats, "minecraft:killed_by", fromVersion, toVersion); ++ ++ return null; ++ }); ++ ++ MCTypeRegistry.OBJECTIVE.addStructureHook(VERSION, 6, new DataHook<>() { ++ @Override ++ public MapType preHook(final MapType data, final long fromVersion, final long toVersion) { ++ // unpack ++ final String criteriaName = data.getString("CriteriaName"); ++ String type; ++ String id; ++ ++ if (criteriaName != null) { ++ final int index = criteriaName.indexOf(':'); ++ if (index < 0) { ++ type = "_special"; ++ id = criteriaName; ++ } else { ++ try { ++ type = ResourceLocation.of(criteriaName.substring(0, index), '.').toString(); ++ id = ResourceLocation.of(criteriaName.substring(index + 1), '.').toString(); ++ } catch (final Exception ex) { ++ type = "_special"; ++ id = criteriaName; ++ } ++ } ++ } else { ++ type = null; ++ id = null; ++ } ++ ++ if (type != null && id != null) { ++ final MapType criteriaType = Types.NBT.createEmptyMap(); ++ data.setMap("CriteriaType", criteriaType); ++ ++ criteriaType.setString("type", type); ++ criteriaType.setString("id", id); ++ } ++ ++ return null; ++ } ++ ++ @Override ++ public MapType postHook(final MapType data, final long fromVersion, final long toVersion) { ++ // repack ++ final MapType criteriaType = data.getMap("CriteriaType"); ++ ++ final String newName; ++ if (criteriaType == null) { ++ newName = null; ++ } else { ++ final String type = criteriaType.getString("type"); ++ final String id = criteriaType.getString("id"); ++ if (type != null && id != null) { ++ if ("_special".equals(type)) { ++ newName = id; ++ } else { ++ newName = packWithDot(type) + ":" + packWithDot(id); ++ } ++ } else { ++ newName = null; ++ } ++ } ++ ++ if (newName != null) { ++ data.remove("CriteriaType"); ++ data.setString("CriteriaName", newName); ++ } ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.OBJECTIVE.addStructureWalker(VERSION, 6, (final MapType data, final long fromVersion, final long toVersion) -> { ++ final MapType criteriaType = data.getMap("CriteriaType"); ++ if (criteriaType == null) { ++ return null; ++ } ++ ++ final String type = criteriaType.getString("type"); ++ ++ if (type == null) { ++ return null; ++ } ++ ++ switch (type) { ++ case "minecraft:mined": { ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, criteriaType, "id", fromVersion, toVersion); ++ break; ++ } ++ ++ case "minecraft:crafted": ++ case "minecraft:used": ++ case "minecraft:broken": ++ case "minecraft:picked_up": ++ case "minecraft:dropped": { ++ WalkerUtils.convert(MCTypeRegistry.ITEM_NAME, criteriaType, "id", fromVersion, toVersion); ++ break; ++ } ++ ++ case "minecraft:killed": ++ case "minecraft:killed_by": { ++ WalkerUtils.convert(MCTypeRegistry.ENTITY_NAME, criteriaType, "id", fromVersion, toVersion); ++ break; ++ } ++ } ++ ++ return null; ++ }); ++ ++ ++ // V7 ++ MCTypeRegistry.STRUCTURE_FEATURE.addStructureConverter(new DataConverter<>(VERSION, 7) { ++ private static void convertToBlockState(final MapType data, final String path) { ++ final Number number = data.getNumber(path); ++ if (number == null) { ++ return; ++ } ++ ++ data.setMap(path, HelperBlockFlatteningV1450.getNBTForId(number.intValue() << 4).copy()); // copy to avoid problems with later state datafixers ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final ListType children = data.getList("Children", ObjectType.MAP); ++ if (children == null) { ++ return null; ++ } ++ ++ for (int i = 0, len = children.size(); i < len; ++i) { ++ final MapType child = children.getMap(i); ++ ++ final String id = child.getString("id"); ++ ++ switch (id) { ++ case "ViF": ++ convertToBlockState(child, "CA"); ++ convertToBlockState(child, "CB"); ++ break; ++ case "ViDF": ++ convertToBlockState(child, "CA"); ++ convertToBlockState(child, "CB"); ++ convertToBlockState(child, "CC"); ++ convertToBlockState(child, "CD"); ++ break; ++ } ++ } ++ ++ return null; ++ } ++ }); ++ ++ // convert villagers to trade with pumpkins and not the carved pumpkin ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:villager", new DataConverter<>(VERSION, 7) { ++ private static void convertPumpkin(final MapType data, final String path) { ++ final MapType item = data.getMap(path); ++ if (item == null) { ++ return; ++ } ++ ++ final String id = item.getString("id"); ++ ++ if (id.equals("minecraft:carved_pumpkin")) { ++ item.setString("id", "minecraft:pumpkin"); ++ } ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType offers = data.getMap("Offers"); ++ if (offers != null) { ++ final ListType recipes = offers.getList("Recipes", ObjectType.MAP); ++ if (recipes != null) { ++ for (int i = 0, len = recipes.size(); i < len; ++i) { ++ final MapType recipe = recipes.getMap(i); ++ ++ convertPumpkin(recipe, "buy"); ++ convertPumpkin(recipe, "buyB"); ++ convertPumpkin(recipe, "sell"); ++ } ++ } ++ } ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.STRUCTURE_FEATURE.addStructureWalker(VERSION, 7, (final MapType data, final long fromVersion, final long toVersion) -> { ++ final ListType list = data.getList("Children", ObjectType.MAP); ++ if (list == null) { ++ return null; ++ } ++ ++ for (int i = 0, len = list.size(); i < len; ++i) { ++ final MapType child = list.getMap(i); ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_STATE, child, "CA", fromVersion, toVersion); ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_STATE, child, "CB", fromVersion, toVersion); ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_STATE, child, "CC", fromVersion, toVersion); ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_STATE, child, "CD", fromVersion, toVersion); ++ } ++ ++ return null; ++ }); ++ } ++ ++ private V1451() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1456.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1456.java +new file mode 100644 +index 0000000000000000000000000000000000000000..47682ffbc10805a4cba73dca43198e52c0ce63df +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1456.java +@@ -0,0 +1,37 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V1456 { ++ ++ private static final int VERSION = MCVersions.V17W49B + 1; ++ ++ private static byte direction2dTo3d(final byte old) { ++ switch (old) { ++ case 0: ++ return 3; ++ case 1: ++ return 4; ++ case 2: ++ default: ++ return 2; ++ case 3: ++ return 5; ++ } ++ } ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:item_frame", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ data.setByte("Facing", direction2dTo3d(data.getByte("Facing"))); ++ return null; ++ } ++ }); ++ } ++ ++ private V1456() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1458.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1458.java +new file mode 100644 +index 0000000000000000000000000000000000000000..95822caa64d6c8a780bb120bedd2728355d26b84 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1458.java +@@ -0,0 +1,87 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.util.ComponentUtils; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V1458 { ++ ++ private static final int VERSION = MCVersions.V17W50A + 1; ++ ++ public static MapType updateCustomName(final MapType data) { ++ final String customName = data.getString("CustomName", ""); ++ ++ if (customName.isEmpty()) { ++ data.remove("CustomName"); ++ } else { ++ data.setString("CustomName", ComponentUtils.createPlainTextComponent(customName)); ++ } ++ ++ return null; ++ } ++ ++ public static void register() { ++ // From CB ++ MCTypeRegistry.PLAYER.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ return updateCustomName(data); ++ } ++ }); ++ ++ MCTypeRegistry.ENTITY.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if ("minecraft:commandblock_minecart".equals(data.getString("id"))) { ++ return null; ++ } ++ ++ return updateCustomName(data); ++ } ++ }); ++ ++ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType tag = data.getMap("tag"); ++ if (tag == null) { ++ return null; ++ } ++ ++ final MapType display = tag.getMap("display"); ++ if (display == null) { ++ return null; ++ } ++ ++ final String name = display.getString("Name"); ++ if (name != null) { ++ display.setString("Name", ComponentUtils.createPlainTextComponent(name)); ++ } /* In 1.20.5, Mojang removed this branch (ItemCustomNameToComponentFix) */ /*else { ++ final String localisedName = display.getString("LocName"); ++ if (localisedName != null) { ++ display.setString("Name", ComponentUtils.createTranslatableComponent(localisedName)); ++ display.remove("LocName"); ++ } ++ }*/ ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.TILE_ENTITY.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if ("minecraft:command_block".equals(data.getString("id"))) { ++ return null; ++ } ++ ++ return updateCustomName(data); ++ } ++ }); ++ ++ } ++ ++ private V1458() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1460.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1460.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9933c54eba686ba851e0d2112c7a22a09544c2c2 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1460.java +@@ -0,0 +1,44 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.block_name.DataWalkerBlockNames; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++import ca.spottedleaf.dataconverter.types.MapType; ++import net.minecraft.resources.ResourceLocation; ++import java.util.HashMap; ++import java.util.Locale; ++import java.util.Map; ++ ++public final class V1460 { ++ ++ private static final Map MOTIVE_REMAP = new HashMap<>(); ++ ++ static { ++ MOTIVE_REMAP.put("donkeykong", "donkey_kong"); ++ MOTIVE_REMAP.put("burningskull", "burning_skull"); ++ MOTIVE_REMAP.put("skullandroses", "skull_and_roses"); ++ } ++ ++ private static final int VERSION = MCVersions.V18W01A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:painting", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ String motive = data.getString("Motive"); ++ if (motive != null) { ++ motive = motive.toLowerCase(Locale.ROOT); ++ data.setString("Motive", new ResourceLocation(MOTIVE_REMAP.getOrDefault(motive, motive)).toString()); ++ } ++ return null; ++ } ++ }); ++ ++ // No idea why so many type redefines exist here in Vanilla. nothing about the data structure changed, it's literally a copy of ++ // the existing types. ++ } ++ ++ private V1460() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1466.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1466.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d870aaca4ff623c71604f889c2e667bfe50fe696 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1466.java +@@ -0,0 +1,142 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.Types; ++ ++public final class V1466 { ++ ++ private static final int VERSION = MCVersions.V18W06A; ++ ++ private static short packOffsetCoordinates(final int x, final int y, final int z) { ++ return (short)((x & 15) | ((y & 15) << 4) | ((z & 15) << 8)); ++ } ++ ++ public static void register() { ++ // There is a rather critical change I've made to this converter: changing the chunk status determination. ++ // In Vanilla, this is determined by whether the terrain has been populated and whether the chunk is lit. ++ // For reference, here is the full status progression (at the time of 18w06a): ++ // empty -> base -> carved -> decorated -> lighted -> mobs_spawned -> finalized -> fullchunk -> postprocessed ++ // So one of those must be picked. ++ // If the chunk is lit and terrain is populated, the Vanilla converter will set the status to "mobs_spawned." ++ // If it is anything else, it will be "empty" ++ // I've changed it to the following: if terrain is populated, it is set to at least decorated. If it is populated ++ // and lit, it is set to "mobs_spawned" ++ // But what if it is not populated? If it is not populated, ignore the lit field - obviously that's just broken. ++ // It can't be lit and not populated. ++ // Let's take a look at chunk generation logic for a chunk that is not populated, or even near a populated chunk. ++ // It actually will generate a chunk up to the "carved" stage. It generates the base terrain, (i.e using noise ++ // to figure out where stone is, dirt, grass) and it will generate caves. Nothing else though. No populators. ++ // So "carved" is the correct stage to use, not empty. Setting it to empty would clobber chunk data, when we don't ++ // need to. If it is populated, at least set it to decorated. If it is lit and populated, set it to mobs_spawned. Else, ++ // it is carved. ++ // This change also fixes the random light check "bug" (really this is Mojang's fault for fucking up the status conversion here) ++ // caused by spigot, which would not set the lit value for some chunks. Now those chunks will not be regenerated. ++ ++ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType level = data.getMap("Level"); ++ if (level == null) { ++ return null; ++ } ++ ++ final boolean terrainPopulated = level.getBoolean("TerrainPopulated"); ++ final boolean lightPopulated = level.getBoolean("LightPopulated") || level.getNumber("LightPopulated") == null; ++ final String newStatus = !terrainPopulated ? "carved" : (lightPopulated ? "mobs_spawned" : "decorated"); ++ ++ level.setString("Status", newStatus); ++ level.setBoolean("hasLegacyStructureData", true); ++ ++ // convert biome byte[] into int[] ++ final byte[] biomes = level.getBytes("Biomes"); ++ if (biomes != null) { ++ final int[] newBiomes = new int[256]; ++ for (int i = 0, len = Math.min(newBiomes.length, biomes.length); i < len; ++i) { ++ newBiomes[i] = biomes[i] & 255; ++ } ++ level.setInts("Biomes", newBiomes); ++ } ++ ++ // ProtoChunks have their own dedicated tick list, so we must convert the TileTicks to that. ++ final ListType ticks = level.getList("TileTicks", ObjectType.MAP); ++ if (ticks != null) { ++ final ListType sections = Types.NBT.createEmptyList(); ++ final ListType[] sectionAccess = new ListType[16]; ++ for (int i = 0; i < sectionAccess.length; ++i) { ++ sections.addList(sectionAccess[i] = Types.NBT.createEmptyList()); ++ } ++ level.setList("ToBeTicked", sections); ++ ++ for (int i = 0, len = ticks.size(); i < len; ++i) { ++ final MapType tick = ticks.getMap(i); ++ ++ final int x = tick.getInt("x"); ++ final int y = tick.getInt("y"); ++ final int z = tick.getInt("z"); ++ final short coordinate = packOffsetCoordinates(x, y, z); ++ ++ sectionAccess[y >> 4].addShort(coordinate); ++ } ++ } ++ ++ return null; ++ } ++ }); ++ ++ ++ MCTypeRegistry.CHUNK.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ final MapType level = data.getMap("Level"); ++ if (level == null) { ++ return null; ++ } ++ ++ WalkerUtils.convertList(MCTypeRegistry.ENTITY, level, "Entities", fromVersion, toVersion); ++ WalkerUtils.convertList(MCTypeRegistry.TILE_ENTITY, level, "TileEntities", fromVersion, toVersion); ++ ++ final ListType tileTicks = level.getList("TileTicks", ObjectType.MAP); ++ if (tileTicks != null) { ++ for (int i = 0, len = tileTicks.size(); i < len; ++i) { ++ final MapType tileTick = tileTicks.getMap(i); ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, tileTick, "i", fromVersion, toVersion); ++ } ++ } ++ ++ final ListType sections = level.getList("Sections", ObjectType.MAP); ++ if (sections != null) { ++ for (int i = 0, len = sections.size(); i < len; ++i) { ++ final MapType section = sections.getMap(i); ++ WalkerUtils.convertList(MCTypeRegistry.BLOCK_STATE, section, "Palette", fromVersion, toVersion); ++ } ++ } ++ ++ WalkerUtils.convertValues(MCTypeRegistry.STRUCTURE_FEATURE, level.getMap("Structures"), "Starts", fromVersion, toVersion); ++ ++ return null; ++ }); ++ MCTypeRegistry.STRUCTURE_FEATURE.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ final ListType list = data.getList("Children", ObjectType.MAP); ++ if (list != null) { ++ for (int i = 0, len = list.size(); i < len; ++i) { ++ final MapType child = list.getMap(i); ++ ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_STATE, child, "CA", fromVersion, toVersion); ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_STATE, child, "CB", fromVersion, toVersion); ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_STATE, child, "CC", fromVersion, toVersion); ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_STATE, child, "CD", fromVersion, toVersion); ++ } ++ } ++ ++ WalkerUtils.convert(MCTypeRegistry.BIOME, data, "biome", fromVersion, toVersion); ++ ++ return null; ++ }); ++ } ++ ++ private V1466() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V147.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V147.java +new file mode 100644 +index 0000000000000000000000000000000000000000..af9c6ee26580eb10bf8426f5b61c26df63a910a6 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V147.java +@@ -0,0 +1,26 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V147 { ++ ++ private static final int VERSION = MCVersions.V15W46A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("ArmorStand", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (data.getBoolean("Silent") && !data.getBoolean("Marker")) { ++ data.remove("Silent"); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V147() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1470.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1470.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2bf1baee2321b3cb584ab6355f43263d6c8ec0be +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1470.java +@@ -0,0 +1,31 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; ++ ++public final class V1470 { ++ ++ private static final int VERSION = MCVersions.V18W08A; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ registerMob("minecraft:turtle"); ++ registerMob("minecraft:cod_mob"); ++ registerMob("minecraft:tropical_fish"); ++ registerMob("minecraft:salmon_mob"); ++ registerMob("minecraft:puffer_fish"); ++ registerMob("minecraft:phantom"); ++ registerMob("minecraft:dolphin"); ++ registerMob("minecraft:drowned"); ++ ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:trident", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "inBlockState")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:trident", new DataWalkerItems("Trident")); ++ } ++ ++ private V1470() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1474.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1474.java +new file mode 100644 +index 0000000000000000000000000000000000000000..99f0f34cc14639ed8ed73b847f74cdc607607af8 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1474.java +@@ -0,0 +1,34 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V1474 { ++ ++ private static final int VERSION = MCVersions.V18W10B; ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:shulker", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (data.getInt("Color") == 10) { ++ data.setByte("Color", (byte)16); ++ } ++ return null; ++ } ++ }); ++ // data hooks ensure the inputs are namespaced ++ ConverterAbstractBlockRename.register(VERSION, (final String old) -> { ++ return "minecraft:purple_shulker_box".equals(old) ? "minecraft:shulker_box" : null; ++ }); ++ ConverterAbstractItemRename.register(VERSION, (final String old) -> { ++ return "minecraft:purple_shulker_box".equals(old) ? "minecraft:shulker_box" : null; ++ }); ++ } ++ ++ private V1474() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1475.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1475.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2ae50eea847671f3995688901c79caf520440d7a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1475.java +@@ -0,0 +1,22 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++ ++public final class V1475 { ++ ++ private static final int VERSION = MCVersions.V18W10B + 1; ++ ++ public static void register() { ++ ConverterAbstractBlockRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:flowing_water", "minecraft:water", ++ "minecraft:flowing_lava", "minecraft:lava" ++ ) ++ )::get); ++ } ++ ++ private V1475() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1480.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1480.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7180c1168bffb9fe70d18fe7414a5372518413a8 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1480.java +@@ -0,0 +1,45 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V1480 { ++ ++ private static final int VERSION = MCVersions.V18W14A + 1; ++ ++ public static final Map RENAMED_IDS = new HashMap<>( ++ ImmutableMap.builder() ++ .put("minecraft:blue_coral", "minecraft:tube_coral_block") ++ .put("minecraft:pink_coral", "minecraft:brain_coral_block") ++ .put("minecraft:purple_coral", "minecraft:bubble_coral_block") ++ .put("minecraft:red_coral", "minecraft:fire_coral_block") ++ .put("minecraft:yellow_coral", "minecraft:horn_coral_block") ++ .put("minecraft:blue_coral_plant", "minecraft:tube_coral") ++ .put("minecraft:pink_coral_plant", "minecraft:brain_coral") ++ .put("minecraft:purple_coral_plant", "minecraft:bubble_coral") ++ .put("minecraft:red_coral_plant", "minecraft:fire_coral") ++ .put("minecraft:yellow_coral_plant", "minecraft:horn_coral") ++ .put("minecraft:blue_coral_fan", "minecraft:tube_coral_fan") ++ .put("minecraft:pink_coral_fan", "minecraft:brain_coral_fan") ++ .put("minecraft:purple_coral_fan", "minecraft:bubble_coral_fan") ++ .put("minecraft:red_coral_fan", "minecraft:fire_coral_fan") ++ .put("minecraft:yellow_coral_fan", "minecraft:horn_coral_fan") ++ .put("minecraft:blue_dead_coral", "minecraft:dead_tube_coral") ++ .put("minecraft:pink_dead_coral", "minecraft:dead_brain_coral") ++ .put("minecraft:purple_dead_coral", "minecraft:dead_bubble_coral") ++ .put("minecraft:red_dead_coral", "minecraft:dead_fire_coral") ++ .put("minecraft:yellow_dead_coral", "minecraft:dead_horn_coral") ++ .build() ++ ); ++ ++ public static void register() { ++ ConverterAbstractBlockRename.register(VERSION, RENAMED_IDS::get); ++ ConverterAbstractItemRename.register(VERSION, RENAMED_IDS::get); ++ } ++ ++ private V1480() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1483.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1483.java +new file mode 100644 +index 0000000000000000000000000000000000000000..56d9babebba8b8ba6be07ea413e9c04ffea84023 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1483.java +@@ -0,0 +1,30 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterAbstractEntityRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++ ++public final class V1483 { ++ ++ private static final int VERSION = MCVersions.V18W16A; ++ ++ public static void register() { ++ ConverterAbstractEntityRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:puffer_fish", "minecraft:pufferfish" ++ ) ++ )::get); ++ ConverterAbstractItemRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:puffer_fish_spawn_egg", "minecraft:pufferfish_spawn_egg" ++ ) ++ )::get); ++ ++ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:puffer_fish", "minecraft:pufferfish"); ++ } ++ ++ private V1483() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1484.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1484.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cdbb9379f66aa6edc05c5e6cb2bdeae97f1ea38b +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1484.java +@@ -0,0 +1,75 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V1484 { ++ ++ private static final int VERSION = MCVersions.V18W19A; ++ ++ public static void register() { ++ final Map renamed = new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:sea_grass", "minecraft:seagrass", ++ "minecraft:tall_sea_grass", "minecraft:tall_seagrass" ++ ) ++ ); ++ ++ ConverterAbstractItemRename.register(VERSION, renamed::get); ++ ConverterAbstractBlockRename.register(VERSION, renamed::get); ++ ++ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType level = data.getMap("Level"); ++ ++ if (level == null) { ++ return null; ++ } ++ ++ final MapType heightmaps = level.getMap("Heightmaps"); ++ ++ if (heightmaps == null) { ++ return null; ++ } ++ ++ final Object liquid = heightmaps.getGeneric("LIQUID"); ++ if (liquid != null) { ++ heightmaps.remove("LIQUID"); ++ heightmaps.setGeneric("WORLD_SURFACE_WG", liquid); ++ } ++ ++ final Object solid = heightmaps.getGeneric("SOLID"); ++ if (solid != null) { ++ heightmaps.remove("SOLID"); ++ heightmaps.setGeneric("OCEAN_FLOOR_WG", solid); ++ heightmaps.setGeneric("OCEAN_FLOOR", solid); ++ } ++ ++ final Object light = heightmaps.getGeneric("LIGHT"); ++ if (light != null) { ++ heightmaps.remove("LIGHT"); ++ heightmaps.setGeneric("LIGHT_BLOCKING", light); ++ } ++ ++ final Object rain = heightmaps.getGeneric("RAIN"); ++ if (rain != null) { ++ heightmaps.remove("RAIN"); ++ heightmaps.setGeneric("MOTION_BLOCKING", rain); ++ heightmaps.setGeneric("MOTION_BLOCKING_NO_LEAVES", rain); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V1484() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1486.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1486.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a9e42da41064ea293a71dbf2d681a857b2e1812e +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1486.java +@@ -0,0 +1,39 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterAbstractEntityRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V1486 { ++ ++ private static final int VERSION = MCVersions.V18W19B + 1; ++ ++ public static final Map RENAMED_ENTITY_IDS = new HashMap<>( ++ ImmutableMap.builder() ++ .put("minecraft:salmon_mob", "minecraft:salmon") ++ .put("minecraft:cod_mob", "minecraft:cod") ++ .build() ++ ); ++ public static final Map RENAMED_ITEM_IDS = new HashMap<>( ++ ImmutableMap.builder() ++ .put("minecraft:salmon_mob_spawn_egg", "minecraft:salmon_spawn_egg") ++ .put("minecraft:cod_mob_spawn_egg", "minecraft:cod_spawn_egg") ++ .build() ++ ); ++ ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:cod_mob", "minecraft:cod"); ++ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:salmon_mob", "minecraft:salmon"); ++ ++ ConverterAbstractEntityRename.register(VERSION, RENAMED_ENTITY_IDS::get); ++ ConverterAbstractItemRename.register(VERSION, RENAMED_ITEM_IDS::get); ++ } ++ ++ private V1486() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1487.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1487.java +new file mode 100644 +index 0000000000000000000000000000000000000000..884049818efdf273443fb3d1c2d7250564fbdbf7 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1487.java +@@ -0,0 +1,27 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V1487 { ++ ++ private static final int VERSION = MCVersions.V18W19B + 2; ++ ++ public static void register() { ++ final Map remap = new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:prismarine_bricks_slab", "minecraft:prismarine_brick_slab", ++ "minecraft:prismarine_bricks_stairs", "minecraft:prismarine_brick_stairs" ++ ) ++ ); ++ ++ ConverterAbstractItemRename.register(VERSION, remap::get); ++ ConverterAbstractBlockRename.register(VERSION, remap::get); ++ } ++ ++ private V1487() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1488.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1488.java +new file mode 100644 +index 0000000000000000000000000000000000000000..907a5e2a26ee046e292508e1f06d5f26d10af8c1 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1488.java +@@ -0,0 +1,93 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import com.google.common.collect.ImmutableMap; ++ ++import java.util.HashMap; ++ ++public final class V1488 { ++ ++ private static final int VERSION = MCVersions.V18W19B + 3; ++ ++ private static boolean isIglooPiece(final MapType piece) { ++ return "Iglu".equals(piece.getString("id")); ++ } ++ ++ public static void register() { ++ ConverterAbstractBlockRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:kelp_top", "minecraft:kelp", ++ "minecraft:kelp", "minecraft:kelp_plant" ++ ) ++ )::get); ++ ConverterAbstractItemRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:kelp_top", "minecraft:kelp" ++ ) ++ )::get); ++ ++ // Don't ask me why in V1458 they wrote the converter to NOT do command blocks and THEN in THIS version ++ // to ONLY do command blocks. I don't know. ++ ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:command_block", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ return V1458.updateCustomName(data); ++ } ++ }); ++ ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:commandblock_minecart", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ return V1458.updateCustomName(data); ++ } ++ }); ++ ++ MCTypeRegistry.STRUCTURE_FEATURE.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final ListType children = data.getList("Children", ObjectType.MAP); ++ boolean isIgloo; ++ if (children != null) { ++ isIgloo = true; ++ for (int i = 0, len = children.size(); i < len; ++i) { ++ if (!isIglooPiece(children.getMap(i))) { ++ isIgloo = false; ++ break; ++ } ++ } ++ } else { ++ isIgloo = false; ++ } ++ ++ if (isIgloo) { ++ data.remove("Children"); ++ data.setString("id", "Igloo"); ++ return null; ++ } ++ ++ if (children != null) { ++ for (int i = 0; i < children.size();) { ++ final MapType child = children.getMap(i); ++ if (isIglooPiece(child)) { ++ children.remove(i); ++ continue; ++ } ++ ++i; ++ } ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V1488() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1490.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1490.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1e99de15732bdd283835a9531f76e29ddab91f46 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1490.java +@@ -0,0 +1,30 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import com.google.common.collect.ImmutableMap; ++ ++import java.util.HashMap; ++ ++public final class V1490 { ++ ++ private static final int VERSION = MCVersions.V18W20A + 1; ++ ++ public static void register() { ++ ConverterAbstractBlockRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:melon_block", "minecraft:melon" ++ ) ++ )::get); ++ ConverterAbstractItemRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:melon_block", "minecraft:melon", ++ "minecraft:melon", "minecraft:melon_slice", ++ "minecraft:speckled_melon", "minecraft:glistering_melon_slice" ++ ) ++ )::get); ++ } ++ ++ private V1490() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1492.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1492.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1259216b43434d0f7c7be10a081fd05057c253cf +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1492.java +@@ -0,0 +1,151 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import com.google.common.collect.ImmutableMap; ++import com.mojang.datafixers.util.Pair; ++ ++public final class V1492 { ++ ++ private static final ImmutableMap>> RENAMES = ImmutableMap.>>builder() ++ .put("EndCity", Pair.of( ++ "ECP", ++ ImmutableMap.builder() ++ .put("second_floor", "second_floor_1") ++ .put("third_floor", "third_floor_1") ++ .put("third_floor_c", "third_floor_2") ++ .build() ++ ) ++ ) ++ ++ .put("Mansion", Pair.of( ++ "WMP", ++ ImmutableMap.builder() ++ .put("carpet_south", "carpet_south_1") ++ .put("carpet_west", "carpet_west_1") ++ .put("indoors_door", "indoors_door_1") ++ .put("indoors_wall", "indoors_wall_1") ++ .build() ++ ) ++ ) ++ ++ .put("Igloo", Pair.of( ++ "Iglu", ++ ImmutableMap.builder() ++ .put("minecraft:igloo/igloo_bottom", "minecraft:igloo/bottom") ++ .put("minecraft:igloo/igloo_middle", "minecraft:igloo/middle") ++ .put("minecraft:igloo/igloo_top", "minecraft:igloo/top") ++ .build() ++ ) ++ ) ++ .put("Ocean_Ruin", Pair.of( ++ "ORP", ++ ImmutableMap.builder() ++ .put("minecraft:ruin/big_ruin1_brick", "minecraft:underwater_ruin/big_brick_1") ++ .put("minecraft:ruin/big_ruin2_brick", "minecraft:underwater_ruin/big_brick_2") ++ .put("minecraft:ruin/big_ruin3_brick", "minecraft:underwater_ruin/big_brick_3") ++ .put("minecraft:ruin/big_ruin8_brick", "minecraft:underwater_ruin/big_brick_8") ++ .put("minecraft:ruin/big_ruin1_cracked", "minecraft:underwater_ruin/big_cracked_1") ++ .put("minecraft:ruin/big_ruin2_cracked", "minecraft:underwater_ruin/big_cracked_2") ++ .put("minecraft:ruin/big_ruin3_cracked", "minecraft:underwater_ruin/big_cracked_3") ++ .put("minecraft:ruin/big_ruin8_cracked", "minecraft:underwater_ruin/big_cracked_8") ++ .put("minecraft:ruin/big_ruin1_mossy", "minecraft:underwater_ruin/big_mossy_1") ++ .put("minecraft:ruin/big_ruin2_mossy", "minecraft:underwater_ruin/big_mossy_2") ++ .put("minecraft:ruin/big_ruin3_mossy", "minecraft:underwater_ruin/big_mossy_3") ++ .put("minecraft:ruin/big_ruin8_mossy", "minecraft:underwater_ruin/big_mossy_8") ++ .put("minecraft:ruin/big_ruin_warm4", "minecraft:underwater_ruin/big_warm_4") ++ .put("minecraft:ruin/big_ruin_warm5", "minecraft:underwater_ruin/big_warm_5") ++ .put("minecraft:ruin/big_ruin_warm6", "minecraft:underwater_ruin/big_warm_6") ++ .put("minecraft:ruin/big_ruin_warm7", "minecraft:underwater_ruin/big_warm_7") ++ .put("minecraft:ruin/ruin1_brick", "minecraft:underwater_ruin/brick_1") ++ .put("minecraft:ruin/ruin2_brick", "minecraft:underwater_ruin/brick_2") ++ .put("minecraft:ruin/ruin3_brick", "minecraft:underwater_ruin/brick_3") ++ .put("minecraft:ruin/ruin4_brick", "minecraft:underwater_ruin/brick_4") ++ .put("minecraft:ruin/ruin5_brick", "minecraft:underwater_ruin/brick_5") ++ .put("minecraft:ruin/ruin6_brick", "minecraft:underwater_ruin/brick_6") ++ .put("minecraft:ruin/ruin7_brick", "minecraft:underwater_ruin/brick_7") ++ .put("minecraft:ruin/ruin8_brick", "minecraft:underwater_ruin/brick_8") ++ .put("minecraft:ruin/ruin1_cracked", "minecraft:underwater_ruin/cracked_1") ++ .put("minecraft:ruin/ruin2_cracked", "minecraft:underwater_ruin/cracked_2") ++ .put("minecraft:ruin/ruin3_cracked", "minecraft:underwater_ruin/cracked_3") ++ .put("minecraft:ruin/ruin4_cracked", "minecraft:underwater_ruin/cracked_4") ++ .put("minecraft:ruin/ruin5_cracked", "minecraft:underwater_ruin/cracked_5") ++ .put("minecraft:ruin/ruin6_cracked", "minecraft:underwater_ruin/cracked_6") ++ .put("minecraft:ruin/ruin7_cracked", "minecraft:underwater_ruin/cracked_7") ++ .put("minecraft:ruin/ruin8_cracked", "minecraft:underwater_ruin/cracked_8") ++ .put("minecraft:ruin/ruin1_mossy", "minecraft:underwater_ruin/mossy_1") ++ .put("minecraft:ruin/ruin2_mossy", "minecraft:underwater_ruin/mossy_2") ++ .put("minecraft:ruin/ruin3_mossy", "minecraft:underwater_ruin/mossy_3") ++ .put("minecraft:ruin/ruin4_mossy", "minecraft:underwater_ruin/mossy_4") ++ .put("minecraft:ruin/ruin5_mossy", "minecraft:underwater_ruin/mossy_5") ++ .put("minecraft:ruin/ruin6_mossy", "minecraft:underwater_ruin/mossy_6") ++ .put("minecraft:ruin/ruin7_mossy", "minecraft:underwater_ruin/mossy_7") ++ .put("minecraft:ruin/ruin8_mossy", "minecraft:underwater_ruin/mossy_8") ++ .put("minecraft:ruin/ruin_warm1", "minecraft:underwater_ruin/warm_1") ++ .put("minecraft:ruin/ruin_warm2", "minecraft:underwater_ruin/warm_2") ++ .put("minecraft:ruin/ruin_warm3", "minecraft:underwater_ruin/warm_3") ++ .put("minecraft:ruin/ruin_warm4", "minecraft:underwater_ruin/warm_4") ++ .put("minecraft:ruin/ruin_warm5", "minecraft:underwater_ruin/warm_5") ++ .put("minecraft:ruin/ruin_warm6", "minecraft:underwater_ruin/warm_6") ++ .put("minecraft:ruin/ruin_warm7", "minecraft:underwater_ruin/warm_7") ++ .put("minecraft:ruin/ruin_warm8", "minecraft:underwater_ruin/warm_8") ++ .put("minecraft:ruin/big_brick_1", "minecraft:underwater_ruin/big_brick_1") ++ .put("minecraft:ruin/big_brick_2", "minecraft:underwater_ruin/big_brick_2") ++ .put("minecraft:ruin/big_brick_3", "minecraft:underwater_ruin/big_brick_3") ++ .put("minecraft:ruin/big_brick_8", "minecraft:underwater_ruin/big_brick_8") ++ .put("minecraft:ruin/big_mossy_1", "minecraft:underwater_ruin/big_mossy_1") ++ .put("minecraft:ruin/big_mossy_2", "minecraft:underwater_ruin/big_mossy_2") ++ .put("minecraft:ruin/big_mossy_3", "minecraft:underwater_ruin/big_mossy_3") ++ .put("minecraft:ruin/big_mossy_8", "minecraft:underwater_ruin/big_mossy_8") ++ .put("minecraft:ruin/big_cracked_1", "minecraft:underwater_ruin/big_cracked_1") ++ .put("minecraft:ruin/big_cracked_2", "minecraft:underwater_ruin/big_cracked_2") ++ .put("minecraft:ruin/big_cracked_3", "minecraft:underwater_ruin/big_cracked_3") ++ .put("minecraft:ruin/big_cracked_8", "minecraft:underwater_ruin/big_cracked_8") ++ .put("minecraft:ruin/big_warm_4", "minecraft:underwater_ruin/big_warm_4") ++ .put("minecraft:ruin/big_warm_5", "minecraft:underwater_ruin/big_warm_5") ++ .put("minecraft:ruin/big_warm_6", "minecraft:underwater_ruin/big_warm_6") ++ .put("minecraft:ruin/big_warm_7", "minecraft:underwater_ruin/big_warm_7") ++ .build() ++ ) ++ ) ++ ++ .build(); ++ ++ private static final int VERSION = MCVersions.V18W20B + 1; ++ ++ public static void register() { ++ MCTypeRegistry.STRUCTURE_FEATURE.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final ListType children = data.getList("Children", ObjectType.MAP); ++ if (children == null) { ++ return null; ++ } ++ ++ final String id = data.getString("id"); ++ ++ final Pair> renames = RENAMES.get(id); ++ if (renames == null) { ++ return null; ++ } ++ ++ for (int i = 0, len = children.size(); i < len; ++i) { ++ final MapType child = children.getMap(i); ++ ++ if (renames.getFirst().equals(child.getString("id"))) { ++ final String template = child.getString("Template", ""); ++ child.setString("Template", renames.getSecond().getOrDefault(template, template)); ++ } ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V1492() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1494.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1494.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b72fe109aa8c60425c00aad234d60a1c70dda60b +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1494.java +@@ -0,0 +1,88 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; ++ ++public final class V1494 { ++ ++ private static final int VERSION = MCVersions.V18W20C + 1; ++ ++ private static final Int2ObjectOpenHashMap ENCH_ID_TO_NAME = new Int2ObjectOpenHashMap<>(); ++ static { ++ ENCH_ID_TO_NAME.put(0, "minecraft:protection"); ++ ENCH_ID_TO_NAME.put(1, "minecraft:fire_protection"); ++ ENCH_ID_TO_NAME.put(2, "minecraft:feather_falling"); ++ ENCH_ID_TO_NAME.put(3, "minecraft:blast_protection"); ++ ENCH_ID_TO_NAME.put(4, "minecraft:projectile_protection"); ++ ENCH_ID_TO_NAME.put(5, "minecraft:respiration"); ++ ENCH_ID_TO_NAME.put(6, "minecraft:aqua_affinity"); ++ ENCH_ID_TO_NAME.put(7, "minecraft:thorns"); ++ ENCH_ID_TO_NAME.put(8, "minecraft:depth_strider"); ++ ENCH_ID_TO_NAME.put(9, "minecraft:frost_walker"); ++ ENCH_ID_TO_NAME.put(10, "minecraft:binding_curse"); ++ ENCH_ID_TO_NAME.put(16, "minecraft:sharpness"); ++ ENCH_ID_TO_NAME.put(17, "minecraft:smite"); ++ ENCH_ID_TO_NAME.put(18, "minecraft:bane_of_arthropods"); ++ ENCH_ID_TO_NAME.put(19, "minecraft:knockback"); ++ ENCH_ID_TO_NAME.put(20, "minecraft:fire_aspect"); ++ ENCH_ID_TO_NAME.put(21, "minecraft:looting"); ++ ENCH_ID_TO_NAME.put(22, "minecraft:sweeping"); ++ ENCH_ID_TO_NAME.put(32, "minecraft:efficiency"); ++ ENCH_ID_TO_NAME.put(33, "minecraft:silk_touch"); ++ ENCH_ID_TO_NAME.put(34, "minecraft:unbreaking"); ++ ENCH_ID_TO_NAME.put(35, "minecraft:fortune"); ++ ENCH_ID_TO_NAME.put(48, "minecraft:power"); ++ ENCH_ID_TO_NAME.put(49, "minecraft:punch"); ++ ENCH_ID_TO_NAME.put(50, "minecraft:flame"); ++ ENCH_ID_TO_NAME.put(51, "minecraft:infinity"); ++ ENCH_ID_TO_NAME.put(61, "minecraft:luck_of_the_sea"); ++ ENCH_ID_TO_NAME.put(62, "minecraft:lure"); ++ ENCH_ID_TO_NAME.put(65, "minecraft:loyalty"); ++ ENCH_ID_TO_NAME.put(66, "minecraft:impaling"); ++ ENCH_ID_TO_NAME.put(67, "minecraft:riptide"); ++ ENCH_ID_TO_NAME.put(68, "minecraft:channeling"); ++ ENCH_ID_TO_NAME.put(70, "minecraft:mending"); ++ ENCH_ID_TO_NAME.put(71, "minecraft:vanishing_curse"); ++ } ++ ++ public static void register() { ++ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType tag = data.getMap("tag"); ++ if (tag == null) { ++ return null; ++ } ++ ++ final ListType enchants = tag.getList("ench", ObjectType.MAP); ++ if (enchants != null) { ++ tag.remove("ench"); ++ tag.setList("Enchantments", enchants); ++ ++ for (int i = 0, len = enchants.size(); i < len; ++i) { ++ final MapType enchant = enchants.getMap(i); ++ enchant.setString("id", ENCH_ID_TO_NAME.getOrDefault(enchant.getInt("id"), "null")); ++ } ++ } ++ ++ final ListType storedEnchants = tag.getList("StoredEnchantments", ObjectType.MAP); ++ if (storedEnchants != null) { ++ for (int i = 0, len = storedEnchants.size(); i < len; ++i) { ++ final MapType enchant = storedEnchants.getMap(i); ++ enchant.setString("id", ENCH_ID_TO_NAME.getOrDefault(enchant.getInt("id"), "null")); ++ } ++ } ++ ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V1494() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1496.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1496.java +new file mode 100644 +index 0000000000000000000000000000000000000000..10349a70b865b19cca471a16548fd49910a2b0e7 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1496.java +@@ -0,0 +1,370 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.Types; ++import com.mojang.datafixers.DataFixUtils; ++import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; ++import it.unimi.dsi.fastutil.ints.IntIterator; ++import it.unimi.dsi.fastutil.ints.IntOpenHashSet; ++import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; ++import net.minecraft.util.datafix.PackedBitStorage; ++import java.util.Arrays; ++import java.util.HashSet; ++import java.util.Set; ++ ++public final class V1496 { ++ ++ private static final int VERSION = MCVersions.V18W21B; ++ ++ private static final int[][] DIRECTIONS = new int[][] { ++ new int[] {-1, 0, 0}, ++ new int[] {1, 0, 0}, ++ new int[] {0, -1, 0}, ++ new int[] {0, 1, 0}, ++ new int[] {0, 0, -1}, ++ new int[] {0, 0, 1} ++ }; ++ ++ private static final Object2IntOpenHashMap LEAVES_TO_ID = new Object2IntOpenHashMap<>(); ++ static { ++ LEAVES_TO_ID.put("minecraft:acacia_leaves", 0); ++ LEAVES_TO_ID.put("minecraft:birch_leaves", 1); ++ LEAVES_TO_ID.put("minecraft:dark_oak_leaves", 2); ++ LEAVES_TO_ID.put("minecraft:jungle_leaves", 3); ++ LEAVES_TO_ID.put("minecraft:oak_leaves", 4); ++ LEAVES_TO_ID.put("minecraft:spruce_leaves", 5); ++ } ++ ++ private static final Set LOGS = new HashSet<>( ++ Arrays.asList( ++ "minecraft:acacia_bark", ++ "minecraft:birch_bark", ++ "minecraft:dark_oak_bark", ++ "minecraft:jungle_bark", ++ "minecraft:oak_bark", ++ "minecraft:spruce_bark", ++ "minecraft:acacia_log", ++ "minecraft:birch_log", ++ "minecraft:dark_oak_log", ++ "minecraft:jungle_log", ++ "minecraft:oak_log", ++ "minecraft:spruce_log", ++ "minecraft:stripped_acacia_log", ++ "minecraft:stripped_birch_log", ++ "minecraft:stripped_dark_oak_log", ++ "minecraft:stripped_jungle_log", ++ "minecraft:stripped_oak_log", ++ "minecraft:stripped_spruce_log" ++ ) ++ ); ++ ++ public static void register() { ++ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType level = data.getMap("Level"); ++ if (level == null) { ++ return null; ++ } ++ ++ final ListType sectionsNBT = level.getList("Sections", ObjectType.MAP); ++ if (sectionsNBT == null) { ++ return null; ++ } ++ ++ int newSides = 0; ++ ++ final LeavesSection[] sections = new LeavesSection[16]; ++ boolean skippable = true; ++ for (int i = 0, len = sectionsNBT.size(); i < len; ++i) { ++ final LeavesSection section = new LeavesSection(sectionsNBT.getMap(i)); ++ sections[section.sectionY] = section; ++ ++ skippable &= section.isSkippable(); ++ } ++ ++ if (skippable) { ++ return null; ++ } ++ ++ final IntOpenHashSet[] positionsByDistance = new IntOpenHashSet[7]; ++ for (int i = 0; i < positionsByDistance.length; ++i) { ++ positionsByDistance[i] = new IntOpenHashSet(); ++ } ++ ++ for (final LeavesSection section : sections) { ++ if (section == null || section.isSkippable()) { ++ continue; ++ } ++ ++ for (int index = 0; index < 4096; ++index) { ++ final int block = section.getBlock(index); ++ if (section.isLog(block)) { ++ positionsByDistance[0].add(section.getSectionY() << 12 | index); ++ } else if (section.isLeaf(block)) { ++ int x = getX(index); ++ int z = getZ(index); ++ newSides |= getSideMask(x == 0, x == 15, z == 0, z == 15); ++ } ++ } ++ } ++ ++ // this is basically supposed to recalculate the distances, because a higher cap was added ++ for (int distance = 1; distance < 7; ++distance) { ++ final IntOpenHashSet positionsLess = positionsByDistance[distance - 1]; ++ final IntOpenHashSet positionsEqual = positionsByDistance[distance]; ++ ++ for (final IntIterator iterator = positionsLess.iterator(); iterator.hasNext();) { ++ final int position = iterator.nextInt(); ++ final int fromX = getX(position); ++ final int fromY = getY(position); ++ final int fromZ = getZ(position); ++ ++ for (final int[] direction : DIRECTIONS) { ++ final int toX = fromX + direction[0]; ++ final int toY = fromY + direction[1]; ++ final int toZ = fromZ + direction[2]; ++ ++ if (!(toX >= 0 && toX <= 15 && toZ >= 0 && toZ <= 15 && toY >= 0 && toY <= 255)) { ++ continue; ++ } ++ ++ final LeavesSection toSection = sections[toY >> 4]; ++ if (toSection == null || toSection.isSkippable()) { ++ continue; ++ } ++ ++ final int sectionLocalIndex = getIndex(toX, toY & 15, toZ); ++ final int toBlock = toSection.getBlock(sectionLocalIndex); ++ ++ if (toSection.isLeaf(toBlock)) { ++ final int newDistance = toSection.getDistance(toBlock); ++ if (newDistance > distance) { ++ toSection.setDistance(sectionLocalIndex, toBlock, distance); ++ positionsEqual.add(getIndex(toX, toY, toZ)); ++ } ++ } ++ } ++ } ++ } ++ ++ // done updating blocks, now just update the blockstates and palette ++ for (int i = 0, len = sectionsNBT.size(); i < len; ++i) { ++ final MapType sectionNBT = sectionsNBT.getMap(i); ++ final int y = sectionNBT.getInt("Y"); ++ final LeavesSection section = sections[y]; ++ ++ section.writeInto(sectionNBT); ++ } ++ ++ // if sides changed during process, update it now ++ if (newSides != 0) { ++ MapType upgradeData = level.getMap("UpgradeData"); ++ if (upgradeData == null) { ++ level.setMap("UpgradeData", upgradeData = Types.NBT.createEmptyMap()); ++ } ++ ++ upgradeData.setByte("Sides", (byte)(upgradeData.getByte("Sides") | newSides)); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ public static int getIndex(final int x, final int y, final int z) { ++ return y << 8 | z << 4 | x; ++ } ++ ++ public static int getX(final int index) { ++ return index & 15; ++ } ++ ++ public static int getY(final int index) { ++ return index >> 8 & 255; ++ } ++ ++ public static int getZ(final int index) { ++ return index >> 4 & 15; ++ } ++ ++ public static int getSideMask(final boolean noLeft, final boolean noRight, final boolean noBack, final boolean noForward) { ++ final int ret; ++ ++ if (noBack) { ++ if (noRight) { ++ ret = 2; ++ } else if (noLeft) { ++ ret = 128; ++ } else { ++ ret = 1; ++ } ++ } else if (noForward) { ++ if (noLeft) { ++ ret = 32; ++ } else if (noRight) { ++ ret = 8; ++ } else { ++ ret = 16; ++ } ++ } else if (noRight) { ++ ret = 4; ++ } else if (noLeft) { ++ ret = 64; ++ } else { ++ ret = 0; ++ } ++ ++ return ret; ++ } ++ ++ private V1496() {} ++ ++ public abstract static class Section { ++ protected final ListType palette; ++ protected final int sectionY; ++ protected PackedBitStorage storage; ++ ++ public Section(final MapType section) { ++ this.palette = section.getList("Palette", ObjectType.MAP); ++ this.sectionY = section.getInt("Y"); ++ this.readStorage(section); ++ } ++ ++ protected void readStorage(final MapType section) { ++ if (this.initSkippable()) { ++ this.storage = null; ++ } else { ++ final long[] states = section.getLongs("BlockStates"); ++ final int bits = Math.max(4, DataFixUtils.ceillog2(this.palette.size())); ++ this.storage = new PackedBitStorage(bits, 4096, states); ++ } ++ } ++ ++ public void writeInto(final MapType section) { ++ if (this.isSkippable()) { ++ return; ++ } ++ ++ section.setList("Palette", this.palette); ++ section.setLongs("BlockStates", this.storage.getRaw()); ++ } ++ ++ public boolean isSkippable() { ++ return this.storage == null; ++ } ++ ++ public int getBlock(final int index) { ++ return this.storage.get(index); ++ } ++ ++ protected int getStateId(final String name, final boolean persistent, final int distance) { ++ return LEAVES_TO_ID.getInt(name) << 5 | (persistent ? 16 : 0) | distance; ++ } ++ ++ protected int getSectionY() { ++ return this.sectionY; ++ } ++ ++ protected abstract boolean initSkippable(); ++ } ++ ++ public static final class LeavesSection extends Section { ++ private IntOpenHashSet leaveIds; ++ private IntOpenHashSet logIds; ++ private Int2IntOpenHashMap stateToIdMap; ++ ++ public LeavesSection(final MapType section) { ++ super(section); ++ } ++ ++ @Override ++ protected boolean initSkippable() { ++ this.leaveIds = new IntOpenHashSet(); ++ this.logIds = new IntOpenHashSet(); ++ this.stateToIdMap = new Int2IntOpenHashMap(); ++ this.stateToIdMap.defaultReturnValue(-1); ++ ++ for(int i = 0; i < this.palette.size(); ++i) { ++ final MapType blockState = this.palette.getMap(i); ++ final String name = blockState.getString("Name", ""); ++ if (LEAVES_TO_ID.containsKey(name)) { ++ final MapType properties = blockState.getMap("Properties"); ++ final boolean notDecayable = properties != null && "false".equals(properties.getString("decayable")); ++ ++ this.leaveIds.add(i); ++ this.stateToIdMap.put(this.getStateId(name, notDecayable, 7), i); ++ this.palette.setMap(i, this.makeNewLeafTag(name, notDecayable, 7)); ++ } ++ ++ if (LOGS.contains(name)) { ++ this.logIds.add(i); ++ } ++ } ++ ++ return this.leaveIds.isEmpty() && this.logIds.isEmpty(); ++ } ++ ++ private MapType makeNewLeafTag(final String name, final boolean notDecayable, final int distance) { ++ final MapType properties = Types.NBT.createEmptyMap(); ++ final MapType ret = Types.NBT.createEmptyMap(); ++ ++ ret.setString("Name", name); ++ ret.setMap("Properties", properties); ++ ++ properties.setString("persistent", Boolean.toString(notDecayable)); ++ properties.setString("distance", Integer.toString(distance)); ++ ++ return ret; ++ } ++ ++ public boolean isLog(final int id) { ++ return this.logIds.contains(id); ++ } ++ ++ public boolean isLeaf(final int id) { ++ return this.leaveIds.contains(id); ++ } ++ ++ // only call for logs or leaves, will throw otherwise! ++ private int getDistance(final int id) { ++ if (this.isLog(id)) { ++ return 0; ++ } ++ ++ return Integer.parseInt(this.palette.getMap(id).getMap("Properties").getString("distance")); ++ } ++ ++ private void setDistance(final int index, final int id, final int distance) { ++ final MapType state = this.palette.getMap(id); ++ final String name = state.getString("Name"); ++ final boolean persistent = "true".equals(state.getMap("Properties").getString("persistent")); ++ final int newState = this.getStateId(name, persistent, distance); ++ int newStateId; ++ if ((newStateId = this.stateToIdMap.get(newState)) == -1) { ++ newStateId = this.palette.size(); ++ this.leaveIds.add(newStateId); ++ this.stateToIdMap.put(newState, newStateId); ++ this.palette.addMap(this.makeNewLeafTag(name, persistent, distance)); ++ } ++ ++ if (1 << this.storage.getBits() <= newStateId) { ++ // need to widen storage ++ final PackedBitStorage newStorage = new PackedBitStorage(this.storage.getBits() + 1, 4096); ++ ++ for(int i = 0; i < 4096; ++i) { ++ newStorage.set(i, this.storage.get(i)); ++ } ++ ++ this.storage = newStorage; ++ } ++ ++ this.storage.set(index, newStateId); ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1500.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1500.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fae8cf61c9900544cdecd223f72e1311c8a1cfb1 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1500.java +@@ -0,0 +1,23 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V1500 { ++ ++ private static final int VERSION = MCVersions.V18W22C + 1; ++ ++ public static void register() { ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("DUMMY", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ data.setBoolean("keepPacked", true); ++ return null; ++ } ++ }); ++ } ++ ++ private V1500() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1501.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1501.java +new file mode 100644 +index 0000000000000000000000000000000000000000..dbfb51b74c54a9a479de49ecb295854fc69aef64 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1501.java +@@ -0,0 +1,78 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.advancements.ConverterAbstractAdvancementsRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V1501 { ++ ++ private static final int VERSION = MCVersions.V1_13_PRE1; ++ ++ private static final Map RENAMES = new HashMap<>( ++ ImmutableMap.builder() ++ .put("minecraft:recipes/brewing/speckled_melon", "minecraft:recipes/brewing/glistering_melon_slice") ++ .put("minecraft:recipes/building_blocks/black_stained_hardened_clay", "minecraft:recipes/building_blocks/black_terracotta") ++ .put("minecraft:recipes/building_blocks/blue_stained_hardened_clay", "minecraft:recipes/building_blocks/blue_terracotta") ++ .put("minecraft:recipes/building_blocks/brown_stained_hardened_clay", "minecraft:recipes/building_blocks/brown_terracotta") ++ .put("minecraft:recipes/building_blocks/cyan_stained_hardened_clay", "minecraft:recipes/building_blocks/cyan_terracotta") ++ .put("minecraft:recipes/building_blocks/gray_stained_hardened_clay", "minecraft:recipes/building_blocks/gray_terracotta") ++ .put("minecraft:recipes/building_blocks/green_stained_hardened_clay", "minecraft:recipes/building_blocks/green_terracotta") ++ .put("minecraft:recipes/building_blocks/light_blue_stained_hardened_clay", "minecraft:recipes/building_blocks/light_blue_terracotta") ++ .put("minecraft:recipes/building_blocks/light_gray_stained_hardened_clay", "minecraft:recipes/building_blocks/light_gray_terracotta") ++ .put("minecraft:recipes/building_blocks/lime_stained_hardened_clay", "minecraft:recipes/building_blocks/lime_terracotta") ++ .put("minecraft:recipes/building_blocks/magenta_stained_hardened_clay", "minecraft:recipes/building_blocks/magenta_terracotta") ++ .put("minecraft:recipes/building_blocks/orange_stained_hardened_clay", "minecraft:recipes/building_blocks/orange_terracotta") ++ .put("minecraft:recipes/building_blocks/pink_stained_hardened_clay", "minecraft:recipes/building_blocks/pink_terracotta") ++ .put("minecraft:recipes/building_blocks/purple_stained_hardened_clay", "minecraft:recipes/building_blocks/purple_terracotta") ++ .put("minecraft:recipes/building_blocks/red_stained_hardened_clay", "minecraft:recipes/building_blocks/red_terracotta") ++ .put("minecraft:recipes/building_blocks/white_stained_hardened_clay", "minecraft:recipes/building_blocks/white_terracotta") ++ .put("minecraft:recipes/building_blocks/yellow_stained_hardened_clay", "minecraft:recipes/building_blocks/yellow_terracotta") ++ .put("minecraft:recipes/building_blocks/acacia_wooden_slab", "minecraft:recipes/building_blocks/acacia_slab") ++ .put("minecraft:recipes/building_blocks/birch_wooden_slab", "minecraft:recipes/building_blocks/birch_slab") ++ .put("minecraft:recipes/building_blocks/dark_oak_wooden_slab", "minecraft:recipes/building_blocks/dark_oak_slab") ++ .put("minecraft:recipes/building_blocks/jungle_wooden_slab", "minecraft:recipes/building_blocks/jungle_slab") ++ .put("minecraft:recipes/building_blocks/oak_wooden_slab", "minecraft:recipes/building_blocks/oak_slab") ++ .put("minecraft:recipes/building_blocks/spruce_wooden_slab", "minecraft:recipes/building_blocks/spruce_slab") ++ .put("minecraft:recipes/building_blocks/brick_block", "minecraft:recipes/building_blocks/bricks") ++ .put("minecraft:recipes/building_blocks/chiseled_stonebrick", "minecraft:recipes/building_blocks/chiseled_stone_bricks") ++ .put("minecraft:recipes/building_blocks/end_bricks", "minecraft:recipes/building_blocks/end_stone_bricks") ++ .put("minecraft:recipes/building_blocks/lit_pumpkin", "minecraft:recipes/building_blocks/jack_o_lantern") ++ .put("minecraft:recipes/building_blocks/magma", "minecraft:recipes/building_blocks/magma_block") ++ .put("minecraft:recipes/building_blocks/melon_block", "minecraft:recipes/building_blocks/melon") ++ .put("minecraft:recipes/building_blocks/mossy_stonebrick", "minecraft:recipes/building_blocks/mossy_stone_bricks") ++ .put("minecraft:recipes/building_blocks/nether_brick", "minecraft:recipes/building_blocks/nether_bricks") ++ .put("minecraft:recipes/building_blocks/pillar_quartz_block", "minecraft:recipes/building_blocks/quartz_pillar") ++ .put("minecraft:recipes/building_blocks/red_nether_brick", "minecraft:recipes/building_blocks/red_nether_bricks") ++ .put("minecraft:recipes/building_blocks/snow", "minecraft:recipes/building_blocks/snow_block") ++ .put("minecraft:recipes/building_blocks/smooth_red_sandstone", "minecraft:recipes/building_blocks/cut_red_sandstone") ++ .put("minecraft:recipes/building_blocks/smooth_sandstone", "minecraft:recipes/building_blocks/cut_sandstone") ++ .put("minecraft:recipes/building_blocks/stonebrick", "minecraft:recipes/building_blocks/stone_bricks") ++ .put("minecraft:recipes/building_blocks/stone_stairs", "minecraft:recipes/building_blocks/cobblestone_stairs") ++ .put("minecraft:recipes/building_blocks/string_to_wool", "minecraft:recipes/building_blocks/white_wool_from_string") ++ .put("minecraft:recipes/decorations/fence", "minecraft:recipes/decorations/oak_fence") ++ .put("minecraft:recipes/decorations/purple_shulker_box", "minecraft:recipes/decorations/shulker_box") ++ .put("minecraft:recipes/decorations/slime", "minecraft:recipes/decorations/slime_block") ++ .put("minecraft:recipes/decorations/snow_layer", "minecraft:recipes/decorations/snow") ++ .put("minecraft:recipes/misc/bone_meal_from_block", "minecraft:recipes/misc/bone_meal_from_bone_block") ++ .put("minecraft:recipes/misc/bone_meal_from_bone", "minecraft:recipes/misc/bone_meal") ++ .put("minecraft:recipes/misc/gold_ingot_from_block", "minecraft:recipes/misc/gold_ingot_from_gold_block") ++ .put("minecraft:recipes/misc/iron_ingot_from_block", "minecraft:recipes/misc/iron_ingot_from_iron_block") ++ .put("minecraft:recipes/redstone/fence_gate", "minecraft:recipes/redstone/oak_fence_gate") ++ .put("minecraft:recipes/redstone/noteblock", "minecraft:recipes/redstone/note_block") ++ .put("minecraft:recipes/redstone/trapdoor", "minecraft:recipes/redstone/oak_trapdoor") ++ .put("minecraft:recipes/redstone/wooden_button", "minecraft:recipes/redstone/oak_button") ++ .put("minecraft:recipes/redstone/wooden_door", "minecraft:recipes/redstone/oak_door") ++ .put("minecraft:recipes/redstone/wooden_pressure_plate", "minecraft:recipes/redstone/oak_pressure_plate") ++ .put("minecraft:recipes/transportation/boat", "minecraft:recipes/transportation/oak_boat") ++ .put("minecraft:recipes/transportation/golden_rail", "minecraft:recipes/transportation/powered_rail") ++ .build() ++ ); ++ ++ public static void register() { ++ ConverterAbstractAdvancementsRename.register(VERSION, RENAMES::get); ++ } ++ ++ private V1501() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1502.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1502.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cd07718649f0e2ca66f1ec3b0aba81611333ba09 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1502.java +@@ -0,0 +1,77 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.recipe.ConverterAbstractRecipeRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V1502 { ++ ++ private static final int VERSION = MCVersions.V1_13_PRE2; ++ ++ private static final Map RECIPES_UPDATES = new HashMap<>( ++ ImmutableMap.builder() ++ .put("minecraft:acacia_wooden_slab", "minecraft:acacia_slab") ++ .put("minecraft:birch_wooden_slab", "minecraft:birch_slab") ++ .put("minecraft:black_stained_hardened_clay", "minecraft:black_terracotta") ++ .put("minecraft:blue_stained_hardened_clay", "minecraft:blue_terracotta") ++ .put("minecraft:boat", "minecraft:oak_boat") ++ .put("minecraft:bone_meal_from_block", "minecraft:bone_meal_from_bone_block") ++ .put("minecraft:bone_meal_from_bone", "minecraft:bone_meal") ++ .put("minecraft:brick_block", "minecraft:bricks") ++ .put("minecraft:brown_stained_hardened_clay", "minecraft:brown_terracotta") ++ .put("minecraft:chiseled_stonebrick", "minecraft:chiseled_stone_bricks") ++ .put("minecraft:cyan_stained_hardened_clay", "minecraft:cyan_terracotta") ++ .put("minecraft:dark_oak_wooden_slab", "minecraft:dark_oak_slab") ++ .put("minecraft:end_bricks", "minecraft:end_stone_bricks") ++ .put("minecraft:fence_gate", "minecraft:oak_fence_gate") ++ .put("minecraft:fence", "minecraft:oak_fence") ++ .put("minecraft:golden_rail", "minecraft:powered_rail") ++ .put("minecraft:gold_ingot_from_block", "minecraft:gold_ingot_from_gold_block") ++ .put("minecraft:gray_stained_hardened_clay", "minecraft:gray_terracotta") ++ .put("minecraft:green_stained_hardened_clay", "minecraft:green_terracotta") ++ .put("minecraft:iron_ingot_from_block", "minecraft:iron_ingot_from_iron_block") ++ .put("minecraft:jungle_wooden_slab", "minecraft:jungle_slab") ++ .put("minecraft:light_blue_stained_hardened_clay", "minecraft:light_blue_terracotta") ++ .put("minecraft:light_gray_stained_hardened_clay", "minecraft:light_gray_terracotta") ++ .put("minecraft:lime_stained_hardened_clay", "minecraft:lime_terracotta") ++ .put("minecraft:lit_pumpkin", "minecraft:jack_o_lantern") ++ .put("minecraft:magenta_stained_hardened_clay", "minecraft:magenta_terracotta") ++ .put("minecraft:magma", "minecraft:magma_block") ++ .put("minecraft:melon_block", "minecraft:melon") ++ .put("minecraft:mossy_stonebrick", "minecraft:mossy_stone_bricks") ++ .put("minecraft:noteblock", "minecraft:note_block") ++ .put("minecraft:oak_wooden_slab", "minecraft:oak_slab") ++ .put("minecraft:orange_stained_hardened_clay", "minecraft:orange_terracotta") ++ .put("minecraft:pillar_quartz_block", "minecraft:quartz_pillar") ++ .put("minecraft:pink_stained_hardened_clay", "minecraft:pink_terracotta") ++ .put("minecraft:purple_shulker_box", "minecraft:shulker_box") ++ .put("minecraft:purple_stained_hardened_clay", "minecraft:purple_terracotta") ++ .put("minecraft:red_nether_brick", "minecraft:red_nether_bricks") ++ .put("minecraft:red_stained_hardened_clay", "minecraft:red_terracotta") ++ .put("minecraft:slime", "minecraft:slime_block") ++ .put("minecraft:smooth_red_sandstone", "minecraft:cut_red_sandstone") ++ .put("minecraft:smooth_sandstone", "minecraft:cut_sandstone") ++ .put("minecraft:snow_layer", "minecraft:snow") ++ .put("minecraft:snow", "minecraft:snow_block") ++ .put("minecraft:speckled_melon", "minecraft:glistering_melon_slice") ++ .put("minecraft:spruce_wooden_slab", "minecraft:spruce_slab") ++ .put("minecraft:stonebrick", "minecraft:stone_bricks") ++ .put("minecraft:stone_stairs", "minecraft:cobblestone_stairs") ++ .put("minecraft:string_to_wool", "minecraft:white_wool_from_string") ++ .put("minecraft:trapdoor", "minecraft:oak_trapdoor") ++ .put("minecraft:white_stained_hardened_clay", "minecraft:white_terracotta") ++ .put("minecraft:wooden_button", "minecraft:oak_button") ++ .put("minecraft:wooden_door", "minecraft:oak_door") ++ .put("minecraft:wooden_pressure_plate", "minecraft:oak_pressure_plate") ++ .put("minecraft:yellow_stained_hardened_clay", "minecraft:yellow_terracotta") ++ .build() ++ ); ++ ++ public static void register() { ++ ConverterAbstractRecipeRename.register(VERSION, RECIPES_UPDATES::get); ++ } ++ ++ private V1502() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1506.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1506.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ce87995961605c80f24371c9c64706ae76e3edea +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1506.java +@@ -0,0 +1,219 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.json.JsonMapType; ++import ca.spottedleaf.dataconverter.types.json.JsonTypeUtil; ++import ca.spottedleaf.dataconverter.types.nbt.NBTMapType; ++import com.google.common.base.Splitter; ++import com.google.common.collect.ImmutableMap; ++import com.google.common.collect.Lists; ++import com.google.common.collect.Maps; ++import com.mojang.datafixers.util.Pair; ++import com.mojang.serialization.Dynamic; ++import com.mojang.serialization.DynamicOps; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.NbtOps; ++import net.minecraft.nbt.Tag; ++import net.minecraft.util.GsonHelper; ++import java.util.ArrayList; ++import java.util.Collections; ++import java.util.HashMap; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Locale; ++import java.util.Map; ++import java.util.stream.Collectors; ++ ++public final class V1506 { ++ ++ private static final int VERSION = MCVersions.V1_13_PRE4 + 2; ++ ++ static final Map MAP = new HashMap<>(); ++ static { ++ MAP.put("0", "minecraft:ocean"); ++ MAP.put("1", "minecraft:plains"); ++ MAP.put("2", "minecraft:desert"); ++ MAP.put("3", "minecraft:mountains"); ++ MAP.put("4", "minecraft:forest"); ++ MAP.put("5", "minecraft:taiga"); ++ MAP.put("6", "minecraft:swamp"); ++ MAP.put("7", "minecraft:river"); ++ MAP.put("8", "minecraft:nether"); ++ MAP.put("9", "minecraft:the_end"); ++ MAP.put("10", "minecraft:frozen_ocean"); ++ MAP.put("11", "minecraft:frozen_river"); ++ MAP.put("12", "minecraft:snowy_tundra"); ++ MAP.put("13", "minecraft:snowy_mountains"); ++ MAP.put("14", "minecraft:mushroom_fields"); ++ MAP.put("15", "minecraft:mushroom_field_shore"); ++ MAP.put("16", "minecraft:beach"); ++ MAP.put("17", "minecraft:desert_hills"); ++ MAP.put("18", "minecraft:wooded_hills"); ++ MAP.put("19", "minecraft:taiga_hills"); ++ MAP.put("20", "minecraft:mountain_edge"); ++ MAP.put("21", "minecraft:jungle"); ++ MAP.put("22", "minecraft:jungle_hills"); ++ MAP.put("23", "minecraft:jungle_edge"); ++ MAP.put("24", "minecraft:deep_ocean"); ++ MAP.put("25", "minecraft:stone_shore"); ++ MAP.put("26", "minecraft:snowy_beach"); ++ MAP.put("27", "minecraft:birch_forest"); ++ MAP.put("28", "minecraft:birch_forest_hills"); ++ MAP.put("29", "minecraft:dark_forest"); ++ MAP.put("30", "minecraft:snowy_taiga"); ++ MAP.put("31", "minecraft:snowy_taiga_hills"); ++ MAP.put("32", "minecraft:giant_tree_taiga"); ++ MAP.put("33", "minecraft:giant_tree_taiga_hills"); ++ MAP.put("34", "minecraft:wooded_mountains"); ++ MAP.put("35", "minecraft:savanna"); ++ MAP.put("36", "minecraft:savanna_plateau"); ++ MAP.put("37", "minecraft:badlands"); ++ MAP.put("38", "minecraft:wooded_badlands_plateau"); ++ MAP.put("39", "minecraft:badlands_plateau"); ++ MAP.put("40", "minecraft:small_end_islands"); ++ MAP.put("41", "minecraft:end_midlands"); ++ MAP.put("42", "minecraft:end_highlands"); ++ MAP.put("43", "minecraft:end_barrens"); ++ MAP.put("44", "minecraft:warm_ocean"); ++ MAP.put("45", "minecraft:lukewarm_ocean"); ++ MAP.put("46", "minecraft:cold_ocean"); ++ MAP.put("47", "minecraft:deep_warm_ocean"); ++ MAP.put("48", "minecraft:deep_lukewarm_ocean"); ++ MAP.put("49", "minecraft:deep_cold_ocean"); ++ MAP.put("50", "minecraft:deep_frozen_ocean"); ++ MAP.put("127", "minecraft:the_void"); ++ MAP.put("129", "minecraft:sunflower_plains"); ++ MAP.put("130", "minecraft:desert_lakes"); ++ MAP.put("131", "minecraft:gravelly_mountains"); ++ MAP.put("132", "minecraft:flower_forest"); ++ MAP.put("133", "minecraft:taiga_mountains"); ++ MAP.put("134", "minecraft:swamp_hills"); ++ MAP.put("140", "minecraft:ice_spikes"); ++ MAP.put("149", "minecraft:modified_jungle"); ++ MAP.put("151", "minecraft:modified_jungle_edge"); ++ MAP.put("155", "minecraft:tall_birch_forest"); ++ MAP.put("156", "minecraft:tall_birch_hills"); ++ MAP.put("157", "minecraft:dark_forest_hills"); ++ MAP.put("158", "minecraft:snowy_taiga_mountains"); ++ MAP.put("160", "minecraft:giant_spruce_taiga"); ++ MAP.put("161", "minecraft:giant_spruce_taiga_hills"); ++ MAP.put("162", "minecraft:modified_gravelly_mountains"); ++ MAP.put("163", "minecraft:shattered_savanna"); ++ MAP.put("164", "minecraft:shattered_savanna_plateau"); ++ MAP.put("165", "minecraft:eroded_badlands"); ++ MAP.put("166", "minecraft:modified_wooded_badlands_plateau"); ++ MAP.put("167", "minecraft:modified_badlands_plateau"); ++ } ++ ++ public static void register() { ++ MCTypeRegistry.LEVEL.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final String generatorOptions = data.getString("generatorOptions"); ++ final String generatorName = data.getString("generatorName"); ++ if ("flat".equalsIgnoreCase(generatorName)) { ++ data.setMap("generatorOptions", V1506.convert(generatorOptions == null ? "" : generatorOptions)); ++ } else if ("buffet".equalsIgnoreCase(generatorName) && generatorOptions != null) { ++ data.setMap("generatorOptions", JsonTypeUtil.convertJsonToNBT(new JsonMapType(GsonHelper.parse(generatorOptions, true), false))); ++ } ++ return null; ++ } ++ }); ++ } ++ ++ private static MapType convert(final String param0) { ++ final Dynamic dynamic = convert(param0, NbtOps.INSTANCE); ++ ++ return new NBTMapType((CompoundTag)dynamic.getValue()); ++ } ++ ++ // Yeah I ain't touching that. This is basically magic value hell. ++ private static Dynamic convert(final String generatorSettings, final DynamicOps ops) { ++ final Iterator splitSettings = Splitter.on(';').split(generatorSettings).iterator(); ++ String biome = "minecraft:plains"; ++ final Map> structures = Maps.newHashMap(); ++ final List> layers; ++ if (!generatorSettings.isEmpty() && splitSettings.hasNext()) { ++ layers = getLayersInfoFromString(splitSettings.next()); ++ if (!layers.isEmpty()) { ++ // biome is next ++ if (splitSettings.hasNext()) { ++ biome = MAP.getOrDefault(splitSettings.next(), "minecraft:plains"); ++ } ++ ++ // structures is next ++ if (splitSettings.hasNext()) { ++ final String[] structuresSplit = splitSettings.next().toLowerCase(Locale.ROOT).split(","); ++ ++ for (final String structureString : structuresSplit) { ++ final String[] structureInfo = structureString.split("\\(", 2); ++ if (!structureInfo[0].isEmpty()) { ++ structures.put(structureInfo[0], Maps.newHashMap()); ++ if (structureInfo.length > 1 && structureInfo[1].endsWith(")") && structureInfo[1].length() > 1) { ++ // I can't even guess the mappings for these. Not worth my time, it will work regardless of the mappings ++ final String[] var7 = structureInfo[1].substring(0, structureInfo[1].length() - 1).split(" "); ++ ++ for (final String var8 : var7) { ++ String[] var9 = var8.split("=", 2); ++ if (var9.length == 2) { ++ structures.get(structureInfo[0]).put(var9[0], var9[1]); ++ } ++ } ++ } ++ } ++ } ++ } else { ++ structures.put("village", Maps.newHashMap()); ++ } ++ } ++ } else { ++ layers = Lists.newArrayList(); ++ layers.add(Pair.of(1, "minecraft:bedrock")); ++ layers.add(Pair.of(2, "minecraft:dirt")); ++ layers.add(Pair.of(1, "minecraft:grass_block")); ++ structures.put("village", Maps.newHashMap()); ++ } ++ ++ final T layerTag = ops.createList(layers.stream().map((param1x) -> ops.createMap(ImmutableMap.of(ops.createString("height"), ops.createInt(param1x.getFirst()), ops.createString("block"), ops.createString(param1x.getSecond()))))); ++ final T structuresTag = ops.createMap(structures.entrySet().stream().map((param1x) -> Pair.of(ops.createString(param1x.getKey().toLowerCase(Locale.ROOT)), ops.createMap(param1x.getValue().entrySet().stream().map((param1xx) -> Pair.of(ops.createString(param1xx.getKey()), ops.createString(param1xx.getValue()))).collect(Collectors.toMap(Pair::getFirst, Pair::getSecond))))).collect(Collectors.toMap(Pair::getFirst, Pair::getSecond))); ++ return new Dynamic<>(ops, ops.createMap(ImmutableMap.of(ops.createString("layers"), layerTag, ops.createString("biome"), ops.createString(biome), ops.createString("structures"), structuresTag))); ++ } ++ ++ private static Pair getLayerInfoFromString(final String layerString) { ++ final String[] split = layerString.split("\\*", 2); ++ int layerCount; ++ if (split.length == 2) { ++ try { ++ layerCount = Integer.parseInt(split[0]); ++ } catch (final NumberFormatException ex) { ++ return null; ++ } ++ } else { ++ layerCount = 1; ++ } ++ ++ final String blockName = split[split.length - 1]; ++ return Pair.of(layerCount, blockName); ++ } ++ ++ private static List> getLayersInfoFromString(final String layersString) { ++ final List> ret = new ArrayList<>(); ++ final String[] layers = layersString.split(","); ++ ++ for (final String layerString : layers) { ++ final Pair layer = getLayerInfoFromString(layerString); ++ if (layer == null) { ++ return Collections.emptyList(); ++ } ++ ++ ret.add(layer); ++ } ++ ++ return ret; ++ } ++ ++ private V1506() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1510.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1510.java +new file mode 100644 +index 0000000000000000000000000000000000000000..dfc9d1e89983c73e06ce3c8a22c29f49af4a935c +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1510.java +@@ -0,0 +1,111 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterAbstractEntityRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.recipe.ConverterAbstractRecipeRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.stats.ConverterAbstractStatsRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V1510 { ++ ++ public static final Map RENAMED_ENTITY_IDS = new HashMap<>( ++ ImmutableMap.builder() ++ .put("minecraft:commandblock_minecart", "minecraft:command_block_minecart") ++ .put("minecraft:ender_crystal", "minecraft:end_crystal") ++ .put("minecraft:snowman", "minecraft:snow_golem") ++ .put("minecraft:evocation_illager", "minecraft:evoker") ++ .put("minecraft:evocation_fangs", "minecraft:evoker_fangs") ++ .put("minecraft:illusion_illager", "minecraft:illusioner") ++ .put("minecraft:vindication_illager", "minecraft:vindicator") ++ .put("minecraft:villager_golem", "minecraft:iron_golem") ++ .put("minecraft:xp_orb", "minecraft:experience_orb") ++ .put("minecraft:xp_bottle", "minecraft:experience_bottle") ++ .put("minecraft:eye_of_ender_signal", "minecraft:eye_of_ender") ++ .put("minecraft:fireworks_rocket", "minecraft:firework_rocket") ++ .build() ++ ); ++ ++ public static final Map RENAMED_BLOCKS = new HashMap<>( ++ ImmutableMap.builder() ++ .put("minecraft:portal", "minecraft:nether_portal") ++ .put("minecraft:oak_bark", "minecraft:oak_wood") ++ .put("minecraft:spruce_bark", "minecraft:spruce_wood") ++ .put("minecraft:birch_bark", "minecraft:birch_wood") ++ .put("minecraft:jungle_bark", "minecraft:jungle_wood") ++ .put("minecraft:acacia_bark", "minecraft:acacia_wood") ++ .put("minecraft:dark_oak_bark", "minecraft:dark_oak_wood") ++ .put("minecraft:stripped_oak_bark", "minecraft:stripped_oak_wood") ++ .put("minecraft:stripped_spruce_bark", "minecraft:stripped_spruce_wood") ++ .put("minecraft:stripped_birch_bark", "minecraft:stripped_birch_wood") ++ .put("minecraft:stripped_jungle_bark", "minecraft:stripped_jungle_wood") ++ .put("minecraft:stripped_acacia_bark", "minecraft:stripped_acacia_wood") ++ .put("minecraft:stripped_dark_oak_bark", "minecraft:stripped_dark_oak_wood") ++ .put("minecraft:mob_spawner", "minecraft:spawner") ++ .build() ++ ); ++ ++ public static final Map RENAMED_ITEMS = new HashMap<>( ++ ImmutableMap.builder() ++ .putAll(RENAMED_BLOCKS) ++ .put("minecraft:clownfish", "minecraft:tropical_fish") ++ .put("minecraft:chorus_fruit_popped", "minecraft:popped_chorus_fruit") ++ .put("minecraft:evocation_illager_spawn_egg", "minecraft:evoker_spawn_egg") ++ .put("minecraft:vindication_illager_spawn_egg", "minecraft:vindicator_spawn_egg") ++ .build() ++ ); ++ ++ private static final Map RECIPES_UPDATES = new HashMap<>( ++ ImmutableMap.builder() ++ .put("minecraft:acacia_bark", "minecraft:acacia_wood") ++ .put("minecraft:birch_bark", "minecraft:birch_wood") ++ .put("minecraft:dark_oak_bark", "minecraft:dark_oak_wood") ++ .put("minecraft:jungle_bark", "minecraft:jungle_wood") ++ .put("minecraft:oak_bark", "minecraft:oak_wood") ++ .put("minecraft:spruce_bark", "minecraft:spruce_wood") ++ .build() ++ ); ++ ++ private static final int VERSION = MCVersions.V1_13_PRE4 + 6; ++ ++ public static void register() { ++ ConverterAbstractBlockRename.register(VERSION, RENAMED_BLOCKS::get); ++ ConverterAbstractItemRename.register(VERSION, RENAMED_ITEMS::get); ++ ConverterAbstractRecipeRename.register(VERSION, RECIPES_UPDATES::get); ++ ++ ConverterAbstractEntityRename.register(VERSION, (String input) -> { ++ if (input.startsWith("minecraft:bred_")) { ++ input = "minecraft:".concat(input.substring("minecraft:bred_".length())); ++ } ++ ++ return RENAMED_ENTITY_IDS.get(input); ++ }); ++ ++ ConverterAbstractStatsRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:swim_one_cm", "minecraft:walk_on_water_one_cm", ++ "minecraft:dive_one_cm", "minecraft:walk_under_water_one_cm" ++ ) ++ )::get); ++ ++ ++ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:commandblock_minecart", "minecraft:command_block_minecart"); ++ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:ender_crystal", "minecraft:end_crystal"); ++ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:snowman", "minecraft:snow_golem"); ++ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:evocation_illager", "minecraft:evoker"); ++ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:evocation_fangs", "minecraft:evoker_fangs"); ++ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:illusion_illager", "minecraft:illusioner"); ++ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:vindication_illager", "minecraft:vindicator"); ++ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:villager_golem", "minecraft:iron_golem"); ++ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:xp_orb", "minecraft:experience_orb"); ++ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:xp_bottle", "minecraft:experience_bottle"); ++ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:eye_of_ender_signal", "minecraft:eye_of_ender"); ++ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:fireworks_rocket", "minecraft:firework_rocket"); ++ } ++ ++ private V1510() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1514.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1514.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6bcc0de5987db4d9ac28fabefbb58c28f2065d96 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1514.java +@@ -0,0 +1,68 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.util.ComponentUtils; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V1514 { ++ ++ private static final int VERSION = MCVersions.V1_13_PRE7 + 1; ++ ++ public static void register() { ++ MCTypeRegistry.OBJECTIVE.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final String displayName = data.getString("DisplayName"); ++ if (displayName == null) { ++ return null; ++ } ++ ++ final String update = ComponentUtils.createPlainTextComponent(displayName); ++ ++ data.setString("DisplayName", update); ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.TEAM.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final String displayName = data.getString("DisplayName"); ++ if (displayName == null) { ++ return null; ++ } ++ ++ final String update = ComponentUtils.createPlainTextComponent(displayName); ++ ++ data.setString("DisplayName", update); ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.OBJECTIVE.addStructureConverter(new DataConverter<>(VERSION) { ++ private static String getRenderType(String string) { ++ return string.equals("health") ? "hearts" : "integer"; ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final String renderType = data.getString("RenderType"); ++ if (renderType != null) { ++ return null; ++ } ++ ++ final String criteriaName = data.getString("CriteriaName", ""); ++ ++ data.setString("RenderType", getRenderType(criteriaName)); ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V1514() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1515.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1515.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d2093732e06ddccdd8a34bbfcaee6ede3aae96d0 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1515.java +@@ -0,0 +1,28 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V1515 { ++ ++ private static final int VERSION = MCVersions.V1_13_PRE7 + 2; ++ ++ public static final Map RENAMED_BLOCK_IDS = new HashMap<>( ++ ImmutableMap.builder() ++ .put("minecraft:tube_coral_fan", "minecraft:tube_coral_wall_fan") ++ .put("minecraft:brain_coral_fan", "minecraft:brain_coral_wall_fan") ++ .put("minecraft:bubble_coral_fan", "minecraft:bubble_coral_wall_fan") ++ .put("minecraft:fire_coral_fan", "minecraft:fire_coral_wall_fan") ++ .put("minecraft:horn_coral_fan", "minecraft:horn_coral_wall_fan") ++ .build() ++ ); ++ ++ public static void register() { ++ ConverterAbstractBlockRename.register(VERSION, RENAMED_BLOCK_IDS::get); ++ } ++ ++ private V1515() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1624.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1624.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f198495e1bad7a1cb84f41c1ea96b1d0e7943c9e +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1624.java +@@ -0,0 +1,110 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import com.mojang.logging.LogUtils; ++import it.unimi.dsi.fastutil.ints.IntOpenHashSet; ++import org.slf4j.Logger; ++ ++public final class V1624 { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ private static final int VERSION = MCVersions.V18W32A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType level = data.getMap("Level"); ++ ++ if (level == null) { ++ return null; ++ } ++ ++ final ListType sections = level.getList("Sections", ObjectType.MAP); ++ if (sections == null) { ++ return null; ++ } ++ ++ final IntOpenHashSet positionsToLook = new IntOpenHashSet(); ++ ++ for (int i = 0, len = sections.size(); i < len; ++i) { ++ final TrappedChestSection section = new TrappedChestSection(sections.getMap(i)); ++ if (section.isSkippable()) { ++ continue; ++ } ++ ++ for (int index = 0; index < 4096; ++index) { ++ if (section.isTrappedChest(section.getBlock(index))) { ++ positionsToLook.add(section.getSectionY() << 12 | index); ++ } ++ } ++ } ++ ++ final int chunkX = level.getInt("xPos"); ++ final int chunkZ = level.getInt("zPos"); ++ ++ final ListType tileEntities = level.getList("TileEntities", ObjectType.MAP); ++ ++ if (tileEntities != null) { ++ for (int i = 0, len = tileEntities.size(); i < len; ++i) { ++ final MapType tile = tileEntities.getMap(i); ++ ++ final int x = tile.getInt("x"); ++ final int y = tile.getInt("y"); ++ final int z = tile.getInt("z"); ++ ++ final int index = V1496.getIndex(x - (chunkX << 4), y, z - (chunkZ << 4)); ++ if (!positionsToLook.contains(index)) { ++ continue; ++ } ++ ++ final String id = tile.getString("id"); ++ if (!"minecraft:chest".equals(id)) { ++ LOGGER.warn("Block Entity ({},{},{}) was expected to be a chest (V1624)", x, y, z); ++ } ++ ++ tile.setString("id", "minecraft:trapped_chest"); ++ } ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V1624() {} ++ ++ public static final class TrappedChestSection extends V1496.Section { ++ ++ private IntOpenHashSet chestIds; ++ ++ public TrappedChestSection(final MapType section) { ++ super(section); ++ } ++ ++ @Override ++ protected boolean initSkippable() { ++ this.chestIds = new IntOpenHashSet(); ++ ++ for (int i = 0; i < this.palette.size(); ++i) { ++ final MapType blockState = this.palette.getMap(i); ++ final String name = blockState.getString("Name"); ++ if ("minecraft:trapped_chest".equals(name)) { ++ this.chestIds.add(i); ++ } ++ } ++ ++ return this.chestIds.isEmpty(); ++ } ++ ++ public boolean isTrappedChest(final int id) { ++ return this.chestIds.contains(id); ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V165.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V165.java +new file mode 100644 +index 0000000000000000000000000000000000000000..810a838edeea95bb5d0b4b351e65417b762fc45c +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V165.java +@@ -0,0 +1,41 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.util.ComponentUtils; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V165 { ++ ++ private static final int VERSION = MCVersions.V1_9_PRE2; ++ ++ public static void register() { ++ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType tag = data.getMap("tag"); ++ if (tag == null) { ++ return null; ++ } ++ ++ final ListType pages = tag.getList("pages", ObjectType.STRING); ++ if (pages == null) { ++ return null; ++ } ++ ++ for (int i = 0, len = pages.size(); i < len; ++i) { ++ final String page = pages.getString(i); ++ ++ pages.setString(i, ComponentUtils.convertFromLenient(page)); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V165() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1800.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1800.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7f65def5a0f48af268183d9c3b74937924b47b75 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1800.java +@@ -0,0 +1,36 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V1800 { ++ ++ private static final int VERSION = MCVersions.V1_13_2 + 169; ++ ++ public static final Map RENAMED_ITEM_IDS = new HashMap<>( ++ ImmutableMap.builder() ++ .put("minecraft:cactus_green", "minecraft:green_dye") ++ .put("minecraft:rose_red", "minecraft:red_dye") ++ .put("minecraft:dandelion_yellow", "minecraft:yellow_dye") ++ .build() ++ ); ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ ConverterAbstractItemRename.register(VERSION, RENAMED_ITEM_IDS::get); ++ ++ registerMob("minecraft:panda"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:pillager", new DataWalkerItemLists("Inventory")); ++ V100.registerEquipment(VERSION, "minecraft:pillager"); ++ } ++ ++ private V1800() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1801.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1801.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9e1a3af9fb261e585542495f189f898eaa6d9263 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1801.java +@@ -0,0 +1,18 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++ ++public final class V1801 { ++ ++ private static final int VERSION = MCVersions.V1_13_2 + 170; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ registerMob("minecraft:illager_beast"); ++ } ++ ++ private V1801() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1802.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1802.java +new file mode 100644 +index 0000000000000000000000000000000000000000..aeae0c62efa1e189fe4b0da585c8a2a101bb5ede +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1802.java +@@ -0,0 +1,28 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++ ++public final class V1802 { ++ ++ private static final int VERSION = MCVersions.V1_13_2 + 171; ++ ++ public static void register() { ++ ConverterAbstractBlockRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:stone_slab", "minecraft:smooth_stone_slab", ++ "minecraft:sign", "minecraft:oak_sign", "minecraft:wall_sign", "minecraft:oak_wall_sign" ++ ) ++ )::get); ++ ConverterAbstractItemRename.register(VERSION, new HashMap<>(ImmutableMap.of( ++ "minecraft:stone_slab", "minecraft:smooth_stone_slab", ++ "minecraft:sign", "minecraft:oak_sign" ++ ) ++ )::get); ++ } ++ ++ private V1802() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1803.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1803.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ad12a97fe28b6f05973f0927245c944dcf184c46 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1803.java +@@ -0,0 +1,46 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.util.ComponentUtils; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++ ++public final class V1803 { ++ ++ private static final int VERSION = MCVersions.V1_13_2 + 172; ++ ++ public static void register() { ++ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType tag = data.getMap("tag"); ++ ++ if (tag == null) { ++ return null; ++ } ++ ++ final MapType display = tag.getMap("display"); ++ ++ if (display == null) { ++ return null; ++ } ++ ++ final ListType lore = display.getList("Lore", ObjectType.STRING); ++ if (lore == null) { ++ return null; ++ } ++ ++ for (int i = 0, len = lore.size(); i < len; ++i) { ++ lore.setString(i, ComponentUtils.createPlainTextComponent(lore.getString(i))); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V1803() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1904.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1904.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2066f320d774319bec84007ca7ed137eb78d91d1 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1904.java +@@ -0,0 +1,42 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V1904 { ++ ++ private static final int VERSION = MCVersions.V18W43C + 1; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:ocelot", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final int catType = data.getInt("CatType"); ++ ++ if (catType == 0) { ++ final String owner = data.getString("Owner"); ++ final String ownerUUID = data.getString("OwnerUUID"); ++ if ((owner != null && owner.length() > 0) || (ownerUUID != null && ownerUUID.length() > 0)) { ++ data.setBoolean("Trusting", true); ++ } ++ } else if (catType > 0 && catType < 4) { ++ data.setString("id", "minecraft:cat"); ++ data.setString("OwnerUUID", data.getString("OwnerUUID", "")); ++ } ++ ++ return null; ++ } ++ }); ++ ++ registerMob("minecraft:cat"); ++ } ++ ++ private V1904() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1905.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1905.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a4bd2c65fe5a4b4d3e430e5c7eee79435afac4ee +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1905.java +@@ -0,0 +1,34 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V1905 { ++ ++ private static final int VERSION = MCVersions.V18W43C + 2; ++ ++ public static void register() { ++ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType level = data.getMap("Level"); ++ ++ if (level == null) { ++ return null; ++ } ++ ++ final String status = level.getString("Status"); ++ ++ if ("postprocessed".equals(status)) { ++ level.setString("Status", "fullchunk"); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V1905() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1906.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1906.java +new file mode 100644 +index 0000000000000000000000000000000000000000..dbf3215a781555d048077565851884eeb48402b1 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1906.java +@@ -0,0 +1,20 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; ++ ++public final class V1906 { ++ ++ private static final int VERSION = MCVersions.V18W43C + 3; ++ ++ public static void register() { ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:barrel", new DataWalkerItemLists("Items")); ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:smoker", new DataWalkerItemLists("Items")); ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:blast_furnace", new DataWalkerItemLists("Items")); ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:lectern", new DataWalkerItems("Book")); ++ } ++ ++ private V1906() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1909.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1909.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ede4d0bfc0fe0e4a3a6fb906037a4c964baac6e6 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1909.java +@@ -0,0 +1,16 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; ++ ++public final class V1909 { ++ ++ private static final int VERSION = MCVersions.V18W45A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:jigsaw", new DataWalkerTypePaths<>(MCTypeRegistry.FLAT_BLOCK_STATE, "final_state")); ++ } ++ ++ private V1909() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1911.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1911.java +new file mode 100644 +index 0000000000000000000000000000000000000000..02204cd67dc614e95f2ab95ed413ce62baec296f +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1911.java +@@ -0,0 +1,49 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V1911 { ++ ++ private static final int VERSION = MCVersions.V18W46A + 1; ++ ++ private static final Map CHUNK_STATUS_REMAP = new HashMap<>( ++ ImmutableMap.builder() ++ .put("structure_references", "empty") ++ .put("biomes", "empty") ++ .put("base", "surface") ++ .put("carved", "carvers") ++ .put("liquid_carved", "liquid_carvers") ++ .put("decorated", "features") ++ .put("lighted", "light") ++ .put("mobs_spawned", "spawn") ++ .put("finalized", "heightmaps") ++ .put("fullchunk", "full") ++ .build() ++ ); ++ ++ public static void register() { ++ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType level = data.getMap("Level"); ++ ++ if (level == null) { ++ return null; ++ } ++ ++ final String status = level.getString("Status", "empty"); ++ level.setString("Status", CHUNK_STATUS_REMAP.getOrDefault(status, "empty")); ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V1911() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1914.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1914.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a965a5941e3624db725a4f101405357df11598c8 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1914.java +@@ -0,0 +1,28 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V1914 { ++ ++ private static final int VERSION = MCVersions.V18W48A; ++ ++ public static void register() { ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:chest", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final String lootTable = data.getString("LootTable"); ++ ++ if ("minecraft:chests/village_blacksmith".equals(lootTable)) { ++ data.setString("LootTable", "minecraft:chests/village/village_weaponsmith"); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V1914() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1917.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1917.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f8b5f5818ed4e839b62777a5d5e9baf70b12a6f0 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1917.java +@@ -0,0 +1,25 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V1917 { ++ ++ private static final int VERSION = MCVersions.V18W49A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:cat", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (data.getInt("CatType") == 9) { ++ data.setInt("CatType", 10); ++ } ++ return null; ++ } ++ }); ++ } ++ ++ private V1917() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1918.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1918.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f97f21e12af1e02aacc1591a88b5da3d7e3f4cfa +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1918.java +@@ -0,0 +1,65 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.Types; ++ ++public final class V1918 { ++ ++ private static final int VERSION = MCVersions.V18W49A + 2; ++ ++ private static String getProfessionString(final int professionId, final int careerId) { ++ if (professionId == 0) { ++ if (careerId == 2) { ++ return "minecraft:fisherman"; ++ } else if (careerId == 3) { ++ return "minecraft:shepherd"; ++ } else { ++ return careerId == 4 ? "minecraft:fletcher" : "minecraft:farmer"; ++ } ++ } else if (professionId == 1) { ++ return careerId == 2 ? "minecraft:cartographer" : "minecraft:librarian"; ++ } else if (professionId == 2) { ++ return "minecraft:cleric"; ++ } else if (professionId == 3) { ++ if (careerId == 2) { ++ return "minecraft:weaponsmith"; ++ } else { ++ return careerId == 3 ? "minecraft:toolsmith" : "minecraft:armorer"; ++ } ++ } else if (professionId == 4) { ++ return careerId == 2 ? "minecraft:leatherworker" : "minecraft:butcher"; ++ } else { ++ return professionId == 5 ? "minecraft:nitwit" : "minecraft:none"; ++ } ++ } ++ ++ public static void register() { ++ final DataConverter, MapType> converter = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final int profession = data.getInt("Profession"); ++ final int career = data.getInt("Career"); ++ final int careerLevel = data.getInt("CareerLevel", 1); ++ data.remove("Profession"); ++ data.remove("Career"); ++ data.remove("CareerLevel"); ++ ++ final MapType villagerData = Types.NBT.createEmptyMap(); ++ data.setMap("VillagerData", villagerData); ++ villagerData.setString("type", "minecraft:plains"); ++ villagerData.setString("profession", getProfessionString(profession, career)); ++ villagerData.setInt("level", careerLevel); ++ ++ return null; ++ } ++ }; ++ ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:villager", converter); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:zombie_villager", converter); ++ } ++ ++ private V1918() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1920.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1920.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fe2d58caf2371f1c430dea209210357f36392a96 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1920.java +@@ -0,0 +1,75 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.util.NamespaceUtil; ++ ++public final class V1920 { ++ ++ private static final int VERSION = MCVersions.V18W50A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType level = data.getMap("Level"); ++ if (level == null) { ++ return null; ++ } ++ ++ final MapType structures = level.getMap("Structures"); ++ if (structures == null) { ++ return null; ++ } ++ ++ final MapType starts = structures.getMap("Starts"); ++ if (starts != null) { ++ final MapType village = starts.getMap("New_Village"); ++ if (village != null) { ++ starts.remove("New_Village"); ++ starts.setMap("Village", village); ++ } else { ++ starts.remove("Village"); ++ } ++ } ++ ++ final MapType references = structures.getMap("References"); ++ if (references != null) { ++ final MapType newVillage = references.getMap("New_Village"); ++ // I believe Mojang had a typo here, removing Village from references only made sense ++ // if the new village didn't exist. DFU removes it whether or not it exists, but still relocates ++ // New_Village to Village first. It doesn't make sense to me to relocate it just to remove it, so it ++ // must be a typo. ++ if (newVillage == null) { ++ references.remove("Village"); ++ } else { ++ references.remove("New_Village"); ++ references.setMap("Village", newVillage); ++ } ++ } ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.STRUCTURE_FEATURE.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final String id = data.getString("id"); ++ ++ if ("minecraft:new_village".equals(NamespaceUtil.correctNamespace(id))) { ++ data.setString("id", "minecraft:village"); ++ } ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:campfire", new DataWalkerItemLists("Items")); ++ } ++ ++ private V1920() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1925.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1925.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7f2db47a58baf1851abb9269b13fb08d4740081a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1925.java +@@ -0,0 +1,30 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.Types; ++ ++public final class V1925 { ++ ++ private static final int VERSION = MCVersions.V19W03C + 1; ++ ++ public static void register() { ++ MCTypeRegistry.SAVED_DATA_MAP_DATA.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType root, final long sourceVersion, final long toVersion) { ++ final MapType data = root.getMap("data"); ++ if (data == null) { ++ final MapType ret = Types.NBT.createEmptyMap(); ++ ret.setMap("data", root); ++ ++ return ret; ++ } ++ return null; ++ } ++ }); ++ } ++ ++ private V1925() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1928.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1928.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f1f7cd60d3fb1d7d3de92091681932607b452d25 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1928.java +@@ -0,0 +1,33 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterAbstractEntityRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++ ++public final class V1928 { ++ ++ private static final int VERSION = MCVersions.V19W04B + 1; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ ConverterAbstractEntityRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:illager_beast", "minecraft:ravager" ++ ) ++ )::get); ++ ConverterAbstractItemRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:illager_beast_spawn_egg", "minecraft:ravager_spawn_egg" ++ ) ++ )::get); ++ ++ registerMob("minecraft:ravager"); ++ } ++ ++ private V1928() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1929.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1929.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cc377819db8182b466b92aba9a9c0d2c483f941d +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1929.java +@@ -0,0 +1,34 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V1929 { ++ ++ private static final int VERSION = MCVersions.V19W04B + 2; ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:wandering_trader", (final MapType data, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, data, "Inventory", fromVersion, toVersion); ++ ++ WalkerUtils.convertList(MCTypeRegistry.VILLAGER_TRADE, data.getMap("Offers"), "Recipes", fromVersion, toVersion); ++ ++ return null; ++ }); ++ V100.registerEquipment(VERSION, "minecraft:wandering_trader"); ++ ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:trader_llama", (final MapType data, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convert(MCTypeRegistry.ITEM_STACK, data, "SaddleItem", fromVersion, toVersion); ++ WalkerUtils.convert(MCTypeRegistry.ITEM_STACK, data, "DecorItem", fromVersion, toVersion); ++ ++ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, data, "Items", fromVersion, toVersion); ++ ++ return null; ++ }); ++ V100.registerEquipment(VERSION, "minecraft:trader_llama"); ++ } ++ ++ private V1929() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1931.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1931.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0ae698a80e81a1648bb90149d9f0effdec8e777c +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1931.java +@@ -0,0 +1,19 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++ ++public final class V1931 { ++ ++ private static final int VERSION = MCVersions.V19W06A; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ registerMob("minecraft:fox"); ++ } ++ ++ private V1931() {} ++ ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1936.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1936.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ddebd1ea2eec5e469d4857503965084d78afce19 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1936.java +@@ -0,0 +1,37 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V1936 { ++ ++ private static final int VERSION = MCVersions.V19W09A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.OPTIONS.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final String chatOpacity = data.getString("chatOpacity"); ++ if (chatOpacity != null) { ++ // Vanilla uses createDouble here, but options is always string -> string. I presume they made ++ // a mistake with this converter. ++ data.setString("textBackgroundOpacity", Double.toString(calculateBackground(chatOpacity))); ++ } ++ return null; ++ } ++ }); ++ } ++ ++ private static double calculateBackground(final String opacity) { ++ try { ++ final double d = 0.9D * Double.parseDouble(opacity) + 0.1D; ++ return d / 2.0D; ++ } catch (final NumberFormatException ex) { ++ return 0.5D; ++ } ++ } ++ ++ private V1936() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1946.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1946.java +new file mode 100644 +index 0000000000000000000000000000000000000000..70d3ab9fe12fab7282edc18938faa94a34d3decb +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1946.java +@@ -0,0 +1,41 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.Types; ++ ++public final class V1946 { ++ ++ private static final int VERSION = MCVersions.V19W14B + 1; ++ ++ public static void register() { ++ MCTypeRegistry.POI_CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType sections = Types.NBT.createEmptyMap(); ++ data.setMap("Sections", sections); ++ ++ for (int y = 0; y < 16; ++y) { ++ final String key = Integer.toString(y); ++ final Object records = data.getGeneric(key); ++ ++ if (records == null) { ++ continue; ++ } ++ ++ data.remove(key); ++ ++ final MapType section = Types.NBT.createEmptyMap(); ++ section.setGeneric("Records", records); ++ sections.setMap(key, section); // integer keys convert to string in DFU (at least for NBT ops) ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V1946() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1948.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1948.java +new file mode 100644 +index 0000000000000000000000000000000000000000..19b0a1197cdf5988f21ba332883b65df646ff0c1 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1948.java +@@ -0,0 +1,39 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V1948 { ++ ++ private static final int VERSION = MCVersions.V1_14_PRE2; ++ ++ public static void register() { ++ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:white_banner", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType tag = data.getMap("tag"); ++ if (tag == null) { ++ return null; ++ } ++ ++ final MapType display = tag.getMap("display"); ++ if (display == null) { ++ return null; ++ } ++ ++ final String name = display.getString("Name"); ++ if (name == null) { ++ return null; ++ } ++ ++ display.setString("Name", name.replace("\"translate\":\"block.minecraft.illager_banner\"", "\"translate\":\"block.minecraft.ominous_banner\"")); ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V1948() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1953.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1953.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c7887c54c85dd7a198aa5c1597c02b2d6887bf71 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1953.java +@@ -0,0 +1,26 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V1953 { ++ ++ private static final int VERSION = MCVersions.V1_14 + 1; ++ ++ public static void register() { ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:banner", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final String name = data.getString("CustomName"); ++ if (name != null) { ++ data.setString("CustomName", name.replace("\"translate\":\"block.minecraft.illager_banner\"", "\"translate\":\"block.minecraft.ominous_banner\"")); ++ } ++ return null; ++ } ++ }); ++ } ++ ++ private V1953() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1955.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1955.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8654f8c7f759720e1e1dd8ae94656699f151c407 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1955.java +@@ -0,0 +1,93 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.Types; ++import net.minecraft.util.Mth; ++ ++public final class V1955 { ++ ++ private static final int VERSION = MCVersions.V1_14_1_PRE1; ++ ++ private static final int[] LEVEL_XP_THRESHOLDS = new int[] { ++ 0, ++ 10, ++ 50, ++ 100, ++ 150 ++ }; ++ ++ private static int getMinXpPerLevel(final int level) { ++ return LEVEL_XP_THRESHOLDS[Mth.clamp(level - 1, 0, LEVEL_XP_THRESHOLDS.length - 1)]; ++ } ++ ++ private static void addLevel(final MapType data, final int level) { ++ MapType villagerData = data.getMap("VillagerData"); ++ if (villagerData == null) { ++ villagerData = Types.NBT.createEmptyMap(); ++ data.setMap("VillagerData", villagerData); ++ } ++ villagerData.setInt("level", level); ++ } ++ ++ private static void addXpFromLevel(final MapType data, final int level) { ++ data.setInt("Xp", getMinXpPerLevel(level)); ++ } ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:villager", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType villagerData = data.getMap("VillagerData"); ++ int level = villagerData == null ? 0 : villagerData.getInt("level"); ++ if (level == 0 || level == 1) { ++ // count recipes ++ final MapType offers = data.getMap("Offers"); ++ final ListType recipes = offers == null ? null : offers.getList("Recipes", ObjectType.MAP); ++ final int recipeCount; ++ if (recipes != null) { ++ recipeCount = recipes.size(); ++ } else { ++ recipeCount = 0; ++ } ++ ++ level = Mth.clamp(recipeCount / 2, 1, 5); ++ if (level > 1) { ++ addLevel(data, level); ++ } ++ } ++ ++ if (!data.hasKey("Xp", ObjectType.NUMBER)) { ++ addXpFromLevel(data, level); ++ } ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:zombie_villager", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final Number xp = data.getNumber("Xp"); ++ if (xp == null) { ++ final int level; ++ final MapType villagerData = data.getMap("VillagerData"); ++ if (villagerData == null) { ++ level = 1; ++ } else { ++ level = villagerData.getInt("level", 1); ++ } ++ ++ data.setInt("Xp", getMinXpPerLevel(level)); ++ } ++ return null; ++ } ++ }); ++ } ++ ++ private V1955() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1961.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1961.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4b1b9b55e2491bd98efddfb28e2aa1074140a1c2 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1961.java +@@ -0,0 +1,29 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V1961 { ++ ++ private static final int VERSION = MCVersions.V1_14_2_PRE3 + 1; ++ ++ public static void register() { ++ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType level = data.getMap("Level"); ++ if (level == null) { ++ return null; ++ } ++ ++ level.remove("isLightOn"); ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V1961() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1963.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1963.java +new file mode 100644 +index 0000000000000000000000000000000000000000..023d8b9aa7d95c674847d9c5dbe0061adcbdc4d3 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1963.java +@@ -0,0 +1,39 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++ ++public final class V1963 { ++ ++ private static final int VERSION = MCVersions.V1_14_2; ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:villager", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final ListType gossips = data.getList("Gossips", ObjectType.MAP); ++ if (gossips == null) { ++ return null; ++ } ++ ++ for (int i = 0; i < gossips.size();) { ++ final MapType gossip = gossips.getMap(i); ++ if ("golem".equals(gossip.getString("Type"))) { ++ gossips.remove(i); ++ continue; ++ } ++ ++ ++i; ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V1963() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2100.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2100.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cec032b20e834a8c6c8901e6fb2d127d7c80b353 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2100.java +@@ -0,0 +1,51 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.advancements.ConverterAbstractAdvancementsRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.recipe.ConverterAbstractRecipeRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V2100 { ++ ++ private static final int VERSION = MCVersions.V1_14_4 + 124; ++ private static final Map RECIPE_RENAMES = new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:sugar", "minecraft:sugar_from_sugar_cane" ++ ) ++ ); ++ private static final Map ADVANCEMENT_RENAMES = new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:recipes/misc/sugar", "minecraft:recipes/misc/sugar_from_sugar_cane" ++ ) ++ ); ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ ConverterAbstractRecipeRename.register(VERSION, RECIPE_RENAMES::get); ++ ConverterAbstractAdvancementsRename.register(VERSION, ADVANCEMENT_RENAMES::get); ++ ++ registerMob("minecraft:bee"); ++ registerMob("minecraft:bee_stinger"); ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:beehive", (data, fromVersion, toVersion) -> { ++ final ListType bees = data.getList("Bees", ObjectType.MAP); ++ if (bees != null) { ++ for (int i = 0, len = bees.size(); i < len; ++i) { ++ WalkerUtils.convert(MCTypeRegistry.ENTITY, bees.getMap(i), "EntityData", fromVersion, toVersion); ++ } ++ } ++ ++ return null; ++ }); ++ } ++ ++ private V2100() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2202.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2202.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c9a23cf055353ee49f07263ea01161de2c035138 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2202.java +@@ -0,0 +1,49 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V2202 { ++ ++ private static final int VERSION = MCVersions.V19W35A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType level = data.getMap("Level"); ++ if (level == null) { ++ return null; ++ } ++ ++ final int[] oldBiomes = level.getInts("Biomes"); ++ ++ if (oldBiomes == null || oldBiomes.length != 256) { ++ return null; ++ } ++ ++ final int[] newBiomes = new int[1024]; ++ level.setInts("Biomes", newBiomes); ++ ++ for (int i = 0; i < 4; ++i) { ++ for (int j = 0; j < 4; ++j) { ++ int k = (j << 2) + 2; ++ int l = (i << 2) + 2; ++ int m = l << 4 | k; ++ newBiomes[i << 2 | j] = oldBiomes[m]; ++ } ++ } ++ ++ for (int i = 1; i < 64; ++i) { ++ System.arraycopy(newBiomes, 0, newBiomes, i * 16, 16); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V2202() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2209.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2209.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7439d0e948f144d93a1fa7b57c2b478a54835d6d +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2209.java +@@ -0,0 +1,28 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.poi.ConverterAbstractPOIRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V2209 { ++ ++ private static final int VERSION = MCVersions.V19W40A + 1; ++ ++ public static void register() { ++ final Map renamedIds = new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:bee_hive", "minecraft:beehive" ++ ) ++ ); ++ ++ ConverterAbstractBlockRename.register(VERSION, renamedIds::get); ++ ConverterAbstractItemRename.register(VERSION, renamedIds::get); ++ ConverterAbstractPOIRename.register(VERSION, renamedIds::get); ++ } ++ ++ private V2209() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2211.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2211.java +new file mode 100644 +index 0000000000000000000000000000000000000000..20904d3e18b317a2f7e5d6063fcf94dda27b5768 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2211.java +@@ -0,0 +1,31 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++ ++public final class V2211 { ++ ++ private static final int VERSION = MCVersions.V19W41A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.STRUCTURE_FEATURE.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (!data.hasKey("references", ObjectType.NUMBER)) { ++ return null; ++ } ++ ++ final int references = data.getInt("references"); ++ if (references <= 0) { ++ data.setInt("references", 1); ++ } ++ return null; ++ } ++ }); ++ } ++ ++ private V2211() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2218.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2218.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8297fe9ab7007399847f3e7ac84519f0dec08576 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2218.java +@@ -0,0 +1,33 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V2218 { ++ ++ private static final int VERSION = MCVersions.V1_15_PRE1; ++ ++ public static void register() { ++ MCTypeRegistry.POI_CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType sections = data.getMap("Sections"); ++ if (sections == null) { ++ return null; ++ } ++ ++ for (final String key : sections.keys()) { ++ final MapType section = sections.getMap(key); ++ ++ section.remove("Valid"); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V2218() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2501.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2501.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f2be8817fe733ae30729952a2aae13d2396b8111 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2501.java +@@ -0,0 +1,65 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.Types; ++ ++public final class V2501 { ++ ++ private static final int VERSION = MCVersions.V1_15_2 + 271; ++ ++ private static void registerFurnace(final String id) { ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, id, (data, fromVersion, toVersion) -> { ++ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, data, "Items", fromVersion, toVersion); ++ ++ WalkerUtils.convertKeys(MCTypeRegistry.RECIPE, data, "RecipesUsed", fromVersion, toVersion); ++ ++ return null; ++ }); ++ } ++ ++ public static void register() { ++ final DataConverter, MapType> converter = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final int recipesUsedSize = data.getInt("RecipesUsedSize"); ++ data.remove("RecipesUsedSize"); ++ ++ if (recipesUsedSize <= 0) { ++ return null; ++ } ++ ++ final MapType newRecipes = Types.NBT.createEmptyMap(); ++ data.setMap("RecipesUsed", newRecipes); ++ ++ for (int i = 0; i < recipesUsedSize; ++i) { ++ final String recipeKey = data.getString("RecipeLocation" + i); ++ data.remove("RecipeLocation" + i); ++ final int recipeAmount = data.getInt("RecipeAmount" + i); ++ data.remove("RecipeAmount" + i); ++ ++ if (i <= 0 || recipeKey == null) { ++ continue; ++ } ++ ++ newRecipes.setInt(recipeKey, recipeAmount); ++ } ++ ++ return null; ++ } ++ }; ++ ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:furnace", converter); ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:blast_furnace", converter); ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:smoker", converter); ++ ++ registerFurnace("minecraft:furnace"); ++ registerFurnace("minecraft:smoker"); ++ registerFurnace("minecraft:blast_furnace"); ++ } ++ ++ private V2501() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2502.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2502.java +new file mode 100644 +index 0000000000000000000000000000000000000000..540ae9aab0acdfbd3800db0468c52e973cb8d93f +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2502.java +@@ -0,0 +1,18 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++ ++public final class V2502 { ++ ++ private static final int VERSION = MCVersions.V1_15_2 + 272; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ registerMob("minecraft:hoglin"); ++ } ++ ++ private V2502() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2503.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2503.java +new file mode 100644 +index 0000000000000000000000000000000000000000..994960d0e67ed0af48d33e9a3db5d1757d85eac5 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2503.java +@@ -0,0 +1,73 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.advancements.ConverterAbstractAdvancementsRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import com.google.common.collect.ImmutableMap; ++import com.google.common.collect.ImmutableSet; ++import java.util.HashMap; ++import java.util.HashSet; ++import java.util.Set; ++ ++public final class V2503 { ++ ++ private static final int VERSION = MCVersions.V1_15_2 + 273; ++ ++ private static final Set WALL_BLOCKS = new HashSet<>( ++ ImmutableSet.of( ++ "minecraft:andesite_wall", ++ "minecraft:brick_wall", ++ "minecraft:cobblestone_wall", ++ "minecraft:diorite_wall", ++ "minecraft:end_stone_brick_wall", ++ "minecraft:granite_wall", ++ "minecraft:mossy_cobblestone_wall", ++ "minecraft:mossy_stone_brick_wall", ++ "minecraft:nether_brick_wall", ++ "minecraft:prismarine_wall", ++ "minecraft:red_nether_brick_wall", ++ "minecraft:red_sandstone_wall", ++ "minecraft:sandstone_wall", ++ "minecraft:stone_brick_wall" ++ ) ++ ); ++ ++ private static void changeWallProperty(final MapType properties, final String path) { ++ final String property = properties.getString(path); ++ if (property != null) { ++ properties.setString(path, "true".equals(property) ? "low" : "none"); ++ } ++ } ++ ++ public static void register() { ++ MCTypeRegistry.BLOCK_STATE.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (!WALL_BLOCKS.contains(data.getString("Name"))) { ++ return null; ++ } ++ ++ final MapType properties = data.getMap("Properties"); ++ if (properties == null) { ++ return null; ++ } ++ ++ changeWallProperty(properties, "east"); ++ changeWallProperty(properties, "west"); ++ changeWallProperty(properties, "north"); ++ changeWallProperty(properties, "south"); ++ ++ return null; ++ } ++ }); ++ ConverterAbstractAdvancementsRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:recipes/misc/composter", "minecraft:recipes/decorations/composter" ++ ) ++ )::get); ++ } ++ ++ private V2503() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2505.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2505.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9342d9efeb1980c7cb67bf0620d12bd9f71165ee +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2505.java +@@ -0,0 +1,48 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.Types; ++ ++public final class V2505 { ++ ++ private static final int VERSION = MCVersions.V20W06A + 1; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:villager", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType brain = data.getMap("Brain"); ++ if (brain == null) { ++ return null; ++ } ++ ++ final MapType memories = brain.getMap("memories"); ++ if (memories == null) { ++ return null; ++ } ++ ++ for (final String key : memories.keys()) { ++ final Object value = memories.getGeneric(key); ++ ++ final MapType wrapped = Types.NBT.createEmptyMap(); ++ wrapped.setGeneric("value", value); ++ ++ memories.setMap(key, wrapped); ++ } ++ ++ return null; ++ } ++ }); ++ ++ registerMob("minecraft:piglin"); ++ } ++ ++ private V2505() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2508.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2508.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f9e9d88e4cca15d2d4fdcbc0dbcae4c35c02284a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2508.java +@@ -0,0 +1,27 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V2508 { ++ ++ private static final int VERSION = MCVersions.V20W08A + 1; ++ ++ public static void register() { ++ final Map remap = new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:warped_fungi", "minecraft:warped_fungus", ++ "minecraft:crimson_fungi", "minecraft:crimson_fungus" ++ ) ++ ); ++ ++ ConverterAbstractBlockRename.register(VERSION, remap::get); ++ ConverterAbstractItemRename.register(VERSION, remap::get); ++ } ++ ++ private V2508() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2509.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2509.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b948564d01726d9891a0733896b3e5cec937bd6d +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2509.java +@@ -0,0 +1,33 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterAbstractEntityRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++ ++public final class V2509 { ++ ++ private static final int VERSION = MCVersions.V20W08A + 2; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ ConverterAbstractItemRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:zombie_pigman_spawn_egg", "minecraft:zombified_piglin_spawn_egg" ++ ) ++ )::get); ++ ConverterAbstractEntityRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:zombie_pigman", "minecraft:zombified_piglin" ++ ) ++ )::get); ++ ++ registerMob("minecraft:zombified_piglin"); ++ } ++ ++ private V2509() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2511.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2511.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a640878469c7ea155cde1cca728b15f2a4bacd73 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2511.java +@@ -0,0 +1,97 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.Types; ++ ++public final class V2511 { ++ ++ private static final int VERSION = MCVersions.V20W09A + 1; ++ ++ private static int[] createUUIDArray(final long most, final long least) { ++ return new int[] { ++ (int)(most >>> 32), ++ (int)most, ++ (int)(least >>> 32), ++ (int)least ++ }; ++ } ++ ++ private static void setUUID(final MapType data, final long most, final long least) { ++ if (most != 0L && least != 0L) { ++ data.setInts("OwnerUUID", createUUIDArray(most, least)); ++ } ++ } ++ ++ public static void register() { ++ final DataConverter, MapType> throwableConverter = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType owner = data.getMap("owner"); ++ data.remove("owner"); ++ if (owner == null) { ++ return null; ++ } ++ ++ setUUID(data, owner.getLong("M"), owner.getLong("L")); ++ ++ return null; ++ } ++ }; ++ final DataConverter, MapType> potionConverter = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType potion = data.getMap("Potion"); ++ data.remove("Potion"); ++ ++ data.setMap("Item", potion == null ? Types.NBT.createEmptyMap() : potion); ++ ++ return null; ++ } ++ }; ++ final DataConverter, MapType> llamaSpitConverter = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType owner = data.getMap("Owner"); ++ data.remove("Owner"); ++ if (owner == null) { ++ return null; ++ } ++ ++ setUUID(data, owner.getLong("OwnerUUIDMost"), owner.getLong("OwnerUUIDLeast")); ++ ++ return null; ++ } ++ }; ++ final DataConverter, MapType> arrowConverter = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ setUUID(data, data.getLong("OwnerUUIDMost"), data.getLong("OwnerUUIDLeast")); ++ ++ data.remove("OwnerUUIDMost"); ++ data.remove("OwnerUUIDLeast"); ++ ++ return null; ++ } ++ }; ++ ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:egg", throwableConverter); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:ender_pearl", throwableConverter); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:experience_bottle", throwableConverter); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:snowball", throwableConverter); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:potion", throwableConverter); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:potion", potionConverter); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:llama_spit", llamaSpitConverter); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:arrow", arrowConverter); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:spectral_arrow", arrowConverter); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:trident", arrowConverter); ++ ++ // Vanilla migrates the potion item but does not change the schema. ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:potion", new DataWalkerItems("Item")); ++ } ++ ++ private V2511() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2514.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2514.java +new file mode 100644 +index 0000000000000000000000000000000000000000..dcd2b1689bbd845238c86cea9dae0c5153d01499 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2514.java +@@ -0,0 +1,590 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.Types; ++import com.google.common.collect.Sets; ++import java.util.Set; ++import java.util.UUID; ++ ++public final class V2514 { ++ ++ private static final int VERSION = MCVersions.V20W11A + 1; ++ ++ private static final Set ABSTRACT_HORSES = Sets.newHashSet(); ++ private static final Set TAMEABLE_ANIMALS = Sets.newHashSet(); ++ private static final Set ANIMALS = Sets.newHashSet(); ++ private static final Set MOBS = Sets.newHashSet(); ++ private static final Set LIVING_ENTITIES = Sets.newHashSet(); ++ private static final Set PROJECTILES = Sets.newHashSet(); ++ static { ++ ABSTRACT_HORSES.add("minecraft:donkey"); ++ ABSTRACT_HORSES.add("minecraft:horse"); ++ ABSTRACT_HORSES.add("minecraft:llama"); ++ ABSTRACT_HORSES.add("minecraft:mule"); ++ ABSTRACT_HORSES.add("minecraft:skeleton_horse"); ++ ABSTRACT_HORSES.add("minecraft:trader_llama"); ++ ABSTRACT_HORSES.add("minecraft:zombie_horse"); ++ ++ TAMEABLE_ANIMALS.add("minecraft:cat"); ++ TAMEABLE_ANIMALS.add("minecraft:parrot"); ++ TAMEABLE_ANIMALS.add("minecraft:wolf"); ++ ++ ANIMALS.add("minecraft:bee"); ++ ANIMALS.add("minecraft:chicken"); ++ ANIMALS.add("minecraft:cow"); ++ ANIMALS.add("minecraft:fox"); ++ ANIMALS.add("minecraft:mooshroom"); ++ ANIMALS.add("minecraft:ocelot"); ++ ANIMALS.add("minecraft:panda"); ++ ANIMALS.add("minecraft:pig"); ++ ANIMALS.add("minecraft:polar_bear"); ++ ANIMALS.add("minecraft:rabbit"); ++ ANIMALS.add("minecraft:sheep"); ++ ANIMALS.add("minecraft:turtle"); ++ ANIMALS.add("minecraft:hoglin"); ++ ++ MOBS.add("minecraft:bat"); ++ MOBS.add("minecraft:blaze"); ++ MOBS.add("minecraft:cave_spider"); ++ MOBS.add("minecraft:cod"); ++ MOBS.add("minecraft:creeper"); ++ MOBS.add("minecraft:dolphin"); ++ MOBS.add("minecraft:drowned"); ++ MOBS.add("minecraft:elder_guardian"); ++ MOBS.add("minecraft:ender_dragon"); ++ MOBS.add("minecraft:enderman"); ++ MOBS.add("minecraft:endermite"); ++ MOBS.add("minecraft:evoker"); ++ MOBS.add("minecraft:ghast"); ++ MOBS.add("minecraft:giant"); ++ MOBS.add("minecraft:guardian"); ++ MOBS.add("minecraft:husk"); ++ MOBS.add("minecraft:illusioner"); ++ MOBS.add("minecraft:magma_cube"); ++ MOBS.add("minecraft:pufferfish"); ++ MOBS.add("minecraft:zombified_piglin"); ++ MOBS.add("minecraft:salmon"); ++ MOBS.add("minecraft:shulker"); ++ MOBS.add("minecraft:silverfish"); ++ MOBS.add("minecraft:skeleton"); ++ MOBS.add("minecraft:slime"); ++ MOBS.add("minecraft:snow_golem"); ++ MOBS.add("minecraft:spider"); ++ MOBS.add("minecraft:squid"); ++ MOBS.add("minecraft:stray"); ++ MOBS.add("minecraft:tropical_fish"); ++ MOBS.add("minecraft:vex"); ++ MOBS.add("minecraft:villager"); ++ MOBS.add("minecraft:iron_golem"); ++ MOBS.add("minecraft:vindicator"); ++ MOBS.add("minecraft:pillager"); ++ MOBS.add("minecraft:wandering_trader"); ++ MOBS.add("minecraft:witch"); ++ MOBS.add("minecraft:wither"); ++ MOBS.add("minecraft:wither_skeleton"); ++ MOBS.add("minecraft:zombie"); ++ MOBS.add("minecraft:zombie_villager"); ++ MOBS.add("minecraft:phantom"); ++ MOBS.add("minecraft:ravager"); ++ MOBS.add("minecraft:piglin"); ++ ++ LIVING_ENTITIES.add("minecraft:armor_stand"); ++ ++ PROJECTILES.add("minecraft:arrow"); ++ PROJECTILES.add("minecraft:dragon_fireball"); ++ PROJECTILES.add("minecraft:firework_rocket"); ++ PROJECTILES.add("minecraft:fireball"); ++ PROJECTILES.add("minecraft:llama_spit"); ++ PROJECTILES.add("minecraft:small_fireball"); ++ PROJECTILES.add("minecraft:snowball"); ++ PROJECTILES.add("minecraft:spectral_arrow"); ++ PROJECTILES.add("minecraft:egg"); ++ PROJECTILES.add("minecraft:ender_pearl"); ++ PROJECTILES.add("minecraft:experience_bottle"); ++ PROJECTILES.add("minecraft:potion"); ++ PROJECTILES.add("minecraft:trident"); ++ PROJECTILES.add("minecraft:wither_skull"); ++ } ++ ++ static int[] createUUIDArray(final long most, final long least) { ++ return new int[] { ++ (int)(most >>> 32), ++ (int)most, ++ (int)(least >>> 32), ++ (int)least ++ }; ++ } ++ ++ static int[] createUUIDFromString(final MapType data, final String path) { ++ if (data == null) { ++ return null; ++ } ++ ++ final String uuidString = data.getString(path); ++ if (uuidString == null) { ++ return null; ++ } ++ ++ try { ++ final UUID uuid = UUID.fromString(uuidString); ++ return createUUIDArray(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits()); ++ } catch (final IllegalArgumentException ignore) { ++ return null; ++ } ++ } ++ ++ static int[] createUUIDFromLongs(final MapType data, final String most, final String least) { ++ if (data == null) { ++ return null; ++ } ++ ++ final long mostBits = data.getLong(most); ++ final long leastBits = data.getLong(least); ++ ++ return (mostBits != 0 || leastBits != 0) ? createUUIDArray(mostBits, leastBits) : null; ++ } ++ ++ static void replaceUUIDString(final MapType data, final String oldPath, final String newPath) { ++ final int[] newUUID = createUUIDFromString(data, oldPath); ++ if (newUUID != null) { ++ data.remove(oldPath); ++ data.setInts(newPath, newUUID); ++ } ++ } ++ ++ static void replaceUUIDMLTag(final MapType data, final String oldPath, final String newPath) { ++ final int[] uuid = createUUIDFromLongs(data.getMap(oldPath), "M", "L"); ++ if (uuid != null) { ++ data.remove(oldPath); ++ data.setInts(newPath, uuid); ++ } ++ } ++ ++ static void replaceUUIDLeastMost(final MapType data, final String prefix, final String newPath) { ++ final String mostPath = prefix.concat("Most"); ++ final String leastPath = prefix.concat("Least"); ++ ++ final int[] uuid = createUUIDFromLongs(data, mostPath, leastPath); ++ if (uuid != null) { ++ data.remove(mostPath); ++ data.remove(leastPath); ++ data.setInts(newPath, uuid); ++ } ++ } ++ ++ private static void updatePiglin(final MapType data) { ++ final MapType brain = data.getMap("Brain"); ++ if (brain == null) { ++ return; ++ } ++ ++ final MapType memories = brain.getMap("memories"); ++ if (memories == null) { ++ return; ++ } ++ ++ final MapType angryAt = memories.getMap("minecraft:angry_at"); ++ ++ replaceUUIDString(angryAt, "value", "value"); ++ } ++ ++ private static void updateEvokerFangs(final MapType data) { ++ replaceUUIDLeastMost(data, "OwnerUUID", "Owner"); ++ } ++ ++ private static void updateZombieVillager(final MapType data) { ++ replaceUUIDLeastMost(data, "ConversionPlayer", "ConversionPlayer"); ++ } ++ ++ private static void updateAreaEffectCloud(final MapType data) { ++ replaceUUIDLeastMost(data, "OwnerUUID", "Owner"); ++ } ++ ++ private static void updateShulkerBullet(final MapType data) { ++ replaceUUIDMLTag(data, "Owner", "Owner"); ++ replaceUUIDMLTag(data, "Target", "Target"); ++ } ++ ++ private static void updateItem(final MapType data) { ++ replaceUUIDMLTag(data, "Owner", "Owner"); ++ replaceUUIDMLTag(data, "Thrower", "Thrower"); ++ } ++ ++ private static void updateFox(final MapType data) { ++ final ListType trustedUUIDS = data.getList("TrustedUUIDs", ObjectType.MAP); ++ if (trustedUUIDS == null) { ++ return; ++ } ++ ++ final ListType newUUIDs = Types.NBT.createEmptyList(); ++ data.remove("TrustedUUIDs"); ++ data.setList("Trusted", newUUIDs); ++ ++ for (int i = 0, len = trustedUUIDS.size(); i < len; ++i) { ++ final MapType uuid = trustedUUIDS.getMap(i); ++ final int[] newUUID = createUUIDFromLongs(uuid, "M", "L"); ++ if (newUUID != null) { ++ newUUIDs.addIntArray(newUUID); ++ } ++ } ++ } ++ ++ private static void updateHurtBy(final MapType data) { ++ replaceUUIDString(data, "HurtBy", "HurtBy"); ++ } ++ ++ private static void updateAnimalOwner(final MapType data) { ++ updateAnimal(data); ++ ++ replaceUUIDString(data, "OwnerUUID", "Owner"); ++ } ++ ++ private static void updateAnimal(final MapType data) { ++ updateMob(data); ++ ++ replaceUUIDLeastMost(data, "LoveCause", "LoveCause"); ++ } ++ ++ private static void updateMob(final MapType data) { ++ updateLivingEntity(data); ++ ++ final MapType leash = data.getMap("Leash"); ++ if (leash == null) { ++ return; ++ } ++ ++ replaceUUIDLeastMost(leash, "UUID", "UUID"); ++ } ++ ++ private static void updateLivingEntity(final MapType data) { ++ final ListType attributes = data.getList("Attributes", ObjectType.MAP); ++ if (attributes == null) { ++ return; ++ } ++ ++ for (int i = 0, len = attributes.size(); i < len; ++i) { ++ final MapType attribute = attributes.getMap(i); ++ ++ final ListType modifiers = attribute.getList("Modifiers", ObjectType.MAP); ++ if (modifiers == null) { ++ continue; ++ } ++ ++ for (int k = 0; k < modifiers.size(); ++k) { ++ replaceUUIDLeastMost(modifiers.getMap(k), "UUID", "UUID"); ++ } ++ } ++ } ++ ++ private static void updateProjectile(final MapType data) { ++ final Object ownerUUID = data.getGeneric("OwnerUUID"); ++ if (ownerUUID != null) { ++ data.remove("OwnerUUID"); ++ data.setGeneric("Owner", ownerUUID); ++ } ++ } ++ ++ private static void updateEntityUUID(final MapType data) { ++ replaceUUIDLeastMost(data, "UUID", "UUID"); ++ } ++ ++ public static void register() { ++ // Entity UUID fixes ++ ++ MCTypeRegistry.ENTITY.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ updateEntityUUID(data); ++ return null; ++ } ++ }); ++ ++ final DataConverter, MapType> animalOwnerConverter = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ updateAnimalOwner(data); ++ return null; ++ } ++ }; ++ final DataConverter, MapType> animalConverter = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ updateAnimal(data); ++ return null; ++ } ++ }; ++ final DataConverter, MapType> mobConverter = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ updateMob(data); ++ return null; ++ } ++ }; ++ final DataConverter, MapType> livingEntityConverter = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ updateLivingEntity(data); ++ return null; ++ } ++ }; ++ final DataConverter, MapType> projectileConverter = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ updateProjectile(data); ++ return null; ++ } ++ }; ++ for (final String id : ABSTRACT_HORSES) { ++ MCTypeRegistry.ENTITY.addConverterForId(id, animalOwnerConverter); ++ } ++ for (final String id : TAMEABLE_ANIMALS) { ++ MCTypeRegistry.ENTITY.addConverterForId(id, animalOwnerConverter); ++ } ++ for (final String id : ANIMALS) { ++ MCTypeRegistry.ENTITY.addConverterForId(id, animalConverter); ++ } ++ for (final String id : MOBS) { ++ MCTypeRegistry.ENTITY.addConverterForId(id, mobConverter); ++ } ++ for (final String id : LIVING_ENTITIES) { ++ MCTypeRegistry.ENTITY.addConverterForId(id, livingEntityConverter); ++ } ++ for (final String id : PROJECTILES) { ++ MCTypeRegistry.ENTITY.addConverterForId(id, projectileConverter); ++ } ++ ++ ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:bee", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ updateHurtBy(data); ++ return null; ++ } ++ }); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:zombified_piglin", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ updateHurtBy(data); ++ return null; ++ } ++ }); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:fox", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ updateFox(data); ++ return null; ++ } ++ }); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:item", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ updateItem(data); ++ return null; ++ } ++ }); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:shulker_bullet", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ updateShulkerBullet(data); ++ return null; ++ } ++ }); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:area_effect_cloud", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ updateAreaEffectCloud(data); ++ return null; ++ } ++ }); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:zombie_villager", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ updateZombieVillager(data); ++ return null; ++ } ++ }); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:evoker_fangs", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ updateEvokerFangs(data); ++ return null; ++ } ++ }); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:piglin", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ updatePiglin(data); ++ return null; ++ } ++ }); ++ ++ ++ // Update TE ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:conduit", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ replaceUUIDMLTag(data, "target_uuid", "Target"); ++ return null; ++ } ++ }); ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:skull", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType owner = data.getMap("Owner"); ++ if (owner == null) { ++ return null; ++ } ++ ++ data.remove("Owner"); ++ ++ replaceUUIDString(owner, "Id", "Id"); ++ ++ data.setMap("SkullOwner", owner); ++ ++ return null; ++ } ++ }); ++ ++ // Player UUID ++ MCTypeRegistry.PLAYER.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ updateLivingEntity(data); ++ updateEntityUUID(data); ++ ++ final MapType rootVehicle = data.getMap("RootVehicle"); ++ if (rootVehicle == null) { ++ return null; ++ } ++ ++ replaceUUIDLeastMost(rootVehicle, "Attach", "Attach"); ++ ++ return null; ++ } ++ }); ++ ++ // Level.dat ++ MCTypeRegistry.LEVEL.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ replaceUUIDString(data, "WanderingTraderId", "WanderingTraderId"); ++ ++ final MapType dimensionData = data.getMap("DimensionData"); ++ if (dimensionData != null) { ++ for (final String key : dimensionData.keys()) { ++ final MapType dimension = dimensionData.getMap(key); ++ ++ final MapType dragonFight = dimension.getMap("DragonFight"); ++ if (dragonFight == null) { ++ continue; ++ } ++ ++ replaceUUIDLeastMost(dragonFight, "DragonUUID", "Dragon"); ++ } ++ } ++ ++ final MapType customBossEvents = data.getMap("CustomBossEvents"); ++ if (customBossEvents != null) { ++ for (final String key : customBossEvents.keys()) { ++ final MapType customBossEvent = customBossEvents.getMap(key); ++ ++ final ListType players = customBossEvent.getList("Players", ObjectType.MAP); ++ if (players == null) { ++ continue; ++ } ++ ++ final ListType newPlayers = Types.NBT.createEmptyList(); ++ customBossEvent.setList("Players", newPlayers); ++ ++ for (int i = 0, len = players.size(); i < len; ++i) { ++ final int[] newUUID = createUUIDFromLongs(players.getMap(i), "M", "L"); ++ if (newUUID != null) { ++ newPlayers.addIntArray(newUUID); ++ } ++ } ++ } ++ } ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.SAVED_DATA_RAIDS.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType root, final long sourceVersion, final long toVersion) { ++ final MapType data = root.getMap("data"); ++ if (data == null) { ++ return null; ++ } ++ ++ final ListType raids = data.getList("Raids", ObjectType.MAP); ++ if (raids == null) { ++ return null; ++ } ++ ++ for (int i = 0, len = raids.size(); i < len; ++i) { ++ final MapType raid = raids.getMap(i); ++ ++ final ListType heros = raid.getList("HeroesOfTheVillage", ObjectType.MAP); ++ ++ if (heros == null) { ++ continue; ++ } ++ ++ final ListType newHeros = Types.NBT.createEmptyList(); ++ raid.setList("HeroesOfTheVillage", newHeros); ++ ++ for (int k = 0, klen = heros.size(); k < klen; ++k) { ++ final MapType uuidOld = heros.getMap(i); ++ final int[] uuidNew = createUUIDFromLongs(uuidOld, "UUIDMost", "UUIDLeast"); ++ if (uuidNew != null) { ++ newHeros.addIntArray(uuidNew); ++ } ++ } ++ } ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType tag = data.getMap("tag"); ++ if (tag == null) { ++ return null; ++ } ++ ++ updateAttributeModifiers(tag); ++ ++ if ("minecraft:player_head".equals(data.getString("id"))) { ++ updateSkullOwner(tag); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private static void updateAttributeModifiers(final MapType tag) { ++ final ListType attributes = tag.getList("AttributeModifiers", ObjectType.MAP); ++ if (attributes == null) { ++ return; ++ } ++ ++ for (int i = 0, len = attributes.size(); i < len; ++i) { ++ replaceUUIDLeastMost(attributes.getMap(i), "UUID", "UUID"); ++ } ++ } ++ ++ private static void updateSkullOwner(final MapType tag) { ++ replaceUUIDString(tag.getMap("SkullOwner"), "Id", "Id"); ++ } ++ ++ private V2514() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2516.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2516.java +new file mode 100644 +index 0000000000000000000000000000000000000000..99f65d84ffaa75db3d2b4568c92d85d3ef20b77f +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2516.java +@@ -0,0 +1,37 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++ ++public final class V2516 { ++ ++ private static final int VERSION = MCVersions.V20W12A + 1; ++ ++ public static void register() { ++ final DataConverter, MapType> gossipUUIDConverter = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final ListType gossips = data.getList("Gossips", ObjectType.MAP); ++ ++ if (gossips == null) { ++ return null; ++ } ++ ++ for (int i = 0, len = gossips.size(); i < len; ++i) { ++ V2514.replaceUUIDLeastMost(gossips.getMap(i), "Target", "Target"); ++ } ++ ++ return null; ++ } ++ }; ++ ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:villager", gossipUUIDConverter); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:zombie_villager", gossipUUIDConverter); ++ } ++ ++ private V2516() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2518.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2518.java +new file mode 100644 +index 0000000000000000000000000000000000000000..35eccf43fd7e31071a9d64883212cddf021ae861 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2518.java +@@ -0,0 +1,65 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V2518 { ++ ++ private static final int VERSION = MCVersions.V20W12A + 3; ++ ++ private static final Map FACING_RENAMES = new HashMap<>( ++ ImmutableMap.builder() ++ .put("down", "down_south") ++ .put("up", "up_north") ++ .put("north", "north_up") ++ .put("south", "south_up") ++ .put("west", "west_up") ++ .put("east", "east_up") ++ .build() ++ ); ++ ++ public static void register() { ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:jigsaw", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final String type = data.getString("attachement_type", "minecraft:empty"); ++ final String pool = data.getString("target_pool", "minecraft:empty"); ++ data.remove("attachement_type"); ++ data.remove("target_pool"); ++ ++ data.setString("name", type); ++ data.setString("target", type); ++ data.setString("pool", pool); ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.BLOCK_STATE.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (!"minecraft:jigsaw".equals(data.getString("Name"))) { ++ return null; ++ } ++ ++ final MapType properties = data.getMap("Properties"); ++ if (properties == null) { ++ return null; ++ } ++ ++ final String facing = properties.getString("facing", "north"); ++ properties.remove("facing"); ++ properties.setString("orientation", FACING_RENAMES.getOrDefault(facing, facing)); ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V2518() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2519.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2519.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7cb7106037b18c0cf8ddff1f9ba25d4f987a6326 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2519.java +@@ -0,0 +1,18 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++ ++public final class V2519 { ++ ++ private static final int VERSION = MCVersions.V20W12A + 4; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ registerMob("minecraft:strider"); ++ } ++ ++ private V2519() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2522.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2522.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9a4d47d78596e2275745673f31f772f0252f2cda +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2522.java +@@ -0,0 +1,18 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++ ++public final class V2522 { ++ ++ private static final int VERSION = MCVersions.V20W13B + 1; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ registerMob("minecraft:zoglin"); ++ } ++ ++ private V2522() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2523.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2523.java +new file mode 100644 +index 0000000000000000000000000000000000000000..af295e8800b6eb5c13bbb88d21b391e5cc4bee1a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2523.java +@@ -0,0 +1,41 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.attributes.ConverterAbstractAttributesRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V2523 { ++ ++ private static final int VERSION = MCVersions.V20W13B + 2; ++ ++ private static final Map RENAMES = new HashMap<>( ++ ImmutableMap.builder() ++ .put("generic.maxHealth", "minecraft:generic.max_health") ++ .put("Max Health", "minecraft:generic.max_health") ++ .put("zombie.spawnReinforcements", "minecraft:zombie.spawn_reinforcements") ++ .put("Spawn Reinforcements Chance", "minecraft:zombie.spawn_reinforcements") ++ .put("horse.jumpStrength", "minecraft:horse.jump_strength") ++ .put("Jump Strength", "minecraft:horse.jump_strength") ++ .put("generic.followRange", "minecraft:generic.follow_range") ++ .put("Follow Range", "minecraft:generic.follow_range") ++ .put("generic.knockbackResistance", "minecraft:generic.knockback_resistance") ++ .put("Knockback Resistance", "minecraft:generic.knockback_resistance") ++ .put("generic.movementSpeed", "minecraft:generic.movement_speed") ++ .put("Movement Speed", "minecraft:generic.movement_speed") ++ .put("generic.flyingSpeed", "minecraft:generic.flying_speed") ++ .put("Flying Speed", "minecraft:generic.flying_speed") ++ .put("generic.attackDamage", "minecraft:generic.attack_damage") ++ .put("generic.attackKnockback", "minecraft:generic.attack_knockback") ++ .put("generic.attackSpeed", "minecraft:generic.attack_speed") ++ .put("generic.armorToughness", "minecraft:generic.armor_toughness") ++ .build() ++ ); ++ ++ public static void register() { ++ ConverterAbstractAttributesRename.register(VERSION, RENAMES::get); ++ } ++ ++ private V2523() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2527.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2527.java +new file mode 100644 +index 0000000000000000000000000000000000000000..157f4b1673f7b71942949d979890b30a5f9e2ca3 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2527.java +@@ -0,0 +1,123 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import com.mojang.datafixers.DataFixUtils; ++import net.minecraft.util.Mth; ++ ++public final class V2527 { ++ ++ private static final int VERSION = MCVersions.V20W16A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType level = data.getMap("Level"); ++ ++ if (level == null) { ++ return null; ++ } ++ ++ final ListType sections = level.getList("Sections", ObjectType.MAP); ++ if (sections != null) { ++ for (int i = 0, len = sections.size(); i < len; ++i) { ++ final MapType section = sections.getMap(i); ++ ++ final ListType palette = section.getList("Palette", ObjectType.MAP); ++ ++ if (palette == null) { ++ continue; ++ } ++ ++ final int bits = Math.max(4, DataFixUtils.ceillog2(palette.size())); ++ ++ if (Mth.isPowerOfTwo(bits)) { ++ // fits perfectly ++ continue; ++ } ++ ++ final long[] states = section.getLongs("BlockStates"); ++ if (states == null) { ++ // wat ++ continue; ++ } ++ ++ section.setLongs("BlockStates", addPadding(4096, bits, states)); ++ } ++ } ++ ++ final MapType heightMaps = level.getMap("Heightmaps"); ++ if (heightMaps != null) { ++ for (final String key : heightMaps.keys()) { ++ final long[] old = heightMaps.getLongs(key); ++ heightMaps.setLongs(key, addPadding(256, 9, old)); ++ } ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ public static long[] addPadding(final int indices, final int bits, final long[] old) { ++ int k = old.length; ++ if (k == 0) { ++ return old; ++ } else { ++ long l = (1L << bits) - 1L; ++ int m = 64 / bits; ++ int n = (indices + m - 1) / m; ++ long[] padded = new long[n]; ++ int o = 0; ++ int p = 0; ++ long q = 0L; ++ int r = 0; ++ long s = old[0]; ++ long t = k > 1 ? old[1] : 0L; ++ ++ for(int u = 0; u < indices; ++u) { ++ int v = u * bits; ++ int w = v >> 6; ++ int x = (u + 1) * bits - 1 >> 6; ++ int y = v ^ w << 6; ++ if (w != r) { ++ s = t; ++ t = w + 1 < k ? old[w + 1] : 0L; ++ r = w; ++ } ++ ++ long ab; ++ int ac; ++ if (w == x) { ++ ab = s >>> y & l; ++ } else { ++ ac = 64 - y; ++ ab = (s >>> y | t << ac) & l; ++ } ++ ++ ac = p + bits; ++ if (ac >= 64) { ++ padded[o++] = q; ++ q = ab; ++ p = bits; ++ } else { ++ q |= ab << p; ++ p = ac; ++ } ++ } ++ ++ if (q != 0L) { ++ padded[o] = q; ++ } ++ ++ return padded; ++ } ++ } ++ ++ private V2527() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2528.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2528.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e7197d098b3d6269d3a4fd9be0432d85f0504dfd +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2528.java +@@ -0,0 +1,30 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++ ++public final class V2528 { ++ ++ private static final int VERSION = MCVersions.V20W16A + 2; ++ ++ public static void register() { ++ ConverterAbstractItemRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:soul_fire_torch", "minecraft:soul_torch", ++ "minecraft:soul_fire_lantern", "minecraft:soul_lantern" ++ ) ++ )::get); ++ ConverterAbstractBlockRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:soul_fire_torch", "minecraft:soul_torch", ++ "minecraft:soul_fire_wall_torch", "minecraft:soul_wall_torch", ++ "minecraft:soul_fire_lantern", "minecraft:soul_lantern" ++ ) ++ )::get); ++ } ++ ++ private V2528() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2529.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2529.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4e54a4ee0c14109609d8d8f1bc6c0c5dabf4fb07 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2529.java +@@ -0,0 +1,25 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V2529 { ++ ++ private static final int VERSION = MCVersions.V20W17A; ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:strider", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (data.getBoolean("NoGravity")) { ++ data.setBoolean("NoGravity", false); ++ } ++ return null; ++ } ++ }); ++ } ++ ++ private V2529() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2531.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2531.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9306ab25feae6315e48aeeb71de960bdf62bcf76 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2531.java +@@ -0,0 +1,63 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V2531 { ++ ++ private static final int VERSION = MCVersions.V20W17A + 2; ++ ++ private static boolean isConnected(final String facing) { ++ return !"none".equals(facing); ++ } ++ ++ public static void register() { ++ MCTypeRegistry.BLOCK_STATE.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (!"minecraft:redstone_wire".equals(data.getString("Name"))) { ++ return null; ++ } ++ ++ final MapType properties = data.getMap("Properties"); ++ ++ if (properties == null) { ++ return null; ++ } ++ ++ ++ final String east = properties.getString("east", "none"); ++ final String west = properties.getString("west", "none"); ++ final String north = properties.getString("north", "none"); ++ final String south = properties.getString("south", "none"); ++ ++ final boolean connectedX = isConnected(east) || isConnected(west); ++ final boolean connectedZ = isConnected(north) || isConnected(south); ++ ++ final String newEast = !isConnected(east) && !connectedZ ? "side" : east; ++ final String newWest = !isConnected(west) && !connectedZ ? "side" : west; ++ final String newNorth = !isConnected(north) && !connectedX ? "side" : north; ++ final String newSouth = !isConnected(south) && !connectedX ? "side" : south; ++ ++ if (properties.hasKey("east")) { ++ properties.setString("east", newEast); ++ } ++ if (properties.hasKey("west")) { ++ properties.setString("west", newWest); ++ } ++ if (properties.hasKey("north")) { ++ properties.setString("north", newNorth); ++ } ++ if (properties.hasKey("south")) { ++ properties.setString("south", newSouth); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V2531() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2533.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2533.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f8d493674380d53398c853899da76024c9d84984 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2533.java +@@ -0,0 +1,42 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++ ++public final class V2533 { ++ ++ private static final int VERSION = MCVersions.V20W18A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:villager", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final ListType attributes = data.getList("Attributes", ObjectType.MAP); ++ ++ if (attributes == null) { ++ return null; ++ } ++ ++ for (int i = 0, len = attributes.size(); i < len; ++i) { ++ final MapType attribute = attributes.getMap(i); ++ ++ if (!"generic.follow_range".equals(attribute.getString("Name"))) { ++ continue; ++ } ++ ++ if (attribute.getDouble("Base") == 16.0) { ++ attribute.setDouble("Base", 48.0); ++ } ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V2533() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2535.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2535.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c0f6135fff38100c1955d64ee3f4ff984308e503 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2535.java +@@ -0,0 +1,34 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++ ++public final class V2535 { ++ ++ private static final int VERSION = MCVersions.V20W19A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:shulker", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ // Mojang uses doubles for whatever reason... rotation is in FLOAT. by using double here ++ // the entity load will just ignore rotation and set it to 0... ++ final ListType rotation = data.getList("Rotation", ObjectType.FLOAT); ++ ++ if (rotation == null || rotation.size() == 0) { ++ return null; ++ } ++ ++ rotation.setFloat(0, rotation.getFloat(0) - 180.0F); ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V2535() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2538.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2538.java +new file mode 100644 +index 0000000000000000000000000000000000000000..99d1df6362b290fdaa65385168ff6588647a8056 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2538.java +@@ -0,0 +1,43 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V2538 { ++ ++ private static final int VERSION = MCVersions.V20W20B + 1; ++ private static final String[] MERGE_KEYS = new String[] { ++ "RandomSeed", ++ "generatorName", ++ "generatorOptions", ++ "generatorVersion", ++ "legacy_custom_options", ++ "MapFeatures", ++ "BonusChest" ++ }; ++ ++ public static void register() { ++ MCTypeRegistry.LEVEL.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType worldGenSettings = data.getOrCreateMap("WorldGenSettings"); ++ ++ for (final String key : MERGE_KEYS) { ++ final Object value = data.getGeneric(key); ++ if (value == null) { ++ continue; ++ } ++ ++ data.remove(key); ++ worldGenSettings.setGeneric(key, value); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V2538() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2550.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2550.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f64f2c2d6051b7e7024a0ebc42c1dd8dc6434cf9 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2550.java +@@ -0,0 +1,346 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.Types; ++import com.google.common.collect.ImmutableMap; ++import org.apache.commons.lang3.math.NumberUtils; ++import java.util.HashMap; ++import java.util.Locale; ++import java.util.Map; ++ ++public final class V2550 { ++ ++ private static final int VERSION = MCVersions.V20W20B + 13; ++ ++ private static final Map DEFAULTS = new HashMap<>( ++ ImmutableMap.builder() ++ .put("minecraft:village", new StructureFeatureConfiguration(32, 8, 10387312)) ++ .put("minecraft:desert_pyramid", new StructureFeatureConfiguration(32, 8, 14357617)) ++ .put("minecraft:igloo", new StructureFeatureConfiguration(32, 8, 14357618)) ++ .put("minecraft:jungle_pyramid", new StructureFeatureConfiguration(32, 8, 14357619)) ++ .put("minecraft:swamp_hut", new StructureFeatureConfiguration(32, 8, 14357620)) ++ .put("minecraft:pillager_outpost", new StructureFeatureConfiguration(32, 8, 165745296)) ++ .put("minecraft:monument", new StructureFeatureConfiguration(32, 5, 10387313)) ++ .put("minecraft:endcity", new StructureFeatureConfiguration(20, 11, 10387313)) ++ .put("minecraft:mansion", new StructureFeatureConfiguration(80, 20, 10387319)) ++ .build() ++ ); ++ ++ private static record StructureFeatureConfiguration(int spacing, int separation, int salt) { ++ ++ public MapType serialize() { ++ final MapType ret = Types.NBT.createEmptyMap(); ++ ++ ret.setInt("spacing", this.spacing); ++ ret.setInt("separation", this.separation); ++ ret.setInt("salt", this.salt); ++ ++ return ret; ++ } ++ } ++ ++ public static void register() { ++ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final long seed = data.getLong("RandomSeed"); ++ String generatorName = data.getString("generatorName"); ++ if (generatorName != null) { ++ generatorName = generatorName.toLowerCase(Locale.ROOT); ++ } ++ String legacyCustomOptions = data.getString("legacy_custom_options"); ++ if (legacyCustomOptions == null) { ++ legacyCustomOptions = "customized".equals(generatorName) ? data.getString("generatorOptions") : null; ++ } ++ ++ final MapType generator; ++ boolean caves = false; ++ ++ if ("customized".equals(generatorName) || generatorName == null) { ++ generator = defaultOverworld(seed); ++ } else { ++ switch (generatorName) { ++ case "flat": { ++ final MapType generatorOptions = data.getMap("generatorOptions"); ++ ++ final MapType structures = fixFlatStructures(generatorOptions); ++ final MapType settings = Types.NBT.createEmptyMap(); ++ generator = Types.NBT.createEmptyMap(); ++ generator.setString("type", "minecraft:flat"); ++ generator.setMap("settings", settings); ++ ++ settings.setMap("structures", structures); ++ ++ ListType layers = generatorOptions.getList("layers", ObjectType.MAP); ++ if (layers == null) { ++ layers = Types.NBT.createEmptyList(); ++ ++ final int[] heights = new int[] { 1, 2, 1 }; ++ final String[] blocks = new String[] { "minecraft:bedrock", "minecraft:dirt", "minecraft:grass_block" }; ++ for (int i = 0; i < 3; ++i) { ++ final MapType layer = Types.NBT.createEmptyMap(); ++ layer.setInt("height", heights[i]); ++ layer.setString("block", blocks[i]); ++ layers.addMap(layer); ++ } ++ } ++ ++ settings.setList("layers", layers); ++ settings.setString("biome", generatorOptions.getString("biome", "minecraft:plains")); ++ ++ break; ++ } ++ ++ case "debug_all_block_states": { ++ generator = Types.NBT.createEmptyMap(); ++ generator.setString("type", "minecraft:debug"); ++ break; ++ } ++ ++ case "buffet": { ++ final MapType generatorOptions = data.getMap("generatorOptions"); ++ final MapType chunkGenerator = generatorOptions == null ? null : generatorOptions.getMap("chunk_generator"); ++ final String chunkGeneratorType = chunkGenerator == null ? null : chunkGenerator.getString("type"); ++ ++ final String newType; ++ if ("minecraft:caves".equals(chunkGeneratorType)) { ++ newType = "minecraft:caves"; ++ caves = true; ++ } else if ("minecraft:floating_islands".equals(chunkGeneratorType)) { ++ newType = "minecraft:floating_islands"; ++ } else { ++ newType = "minecraft:overworld"; ++ } ++ ++ MapType biomeSource = generatorOptions == null ? null : generatorOptions.getMap("biome_source"); ++ if (biomeSource == null) { ++ biomeSource = Types.NBT.createEmptyMap(); ++ biomeSource.setString("type", "minecraft:fixed"); ++ } ++ ++ if ("minecraft:fixed".equals(biomeSource.getString("type"))) { ++ final MapType options = biomeSource.getMap("options"); ++ final ListType biomes = options == null ? null : options.getList("biomes", ObjectType.STRING); ++ final String biome = biomes == null || biomes.size() == 0 ? "minecraft:ocean" : biomes.getString(0); ++ biomeSource.remove("options"); ++ biomeSource.setString("biome", biome); ++ } ++ ++ generator = noise(seed, newType, biomeSource); ++ break; ++ } ++ ++ default: { ++ boolean defaultGen = generatorName.equals("default"); ++ boolean default11Gen = generatorName.equals("default_1_1") || defaultGen && data.getInt("generatorVersion") == 0; ++ boolean amplified = generatorName.equals("amplified"); ++ boolean largeBiomes = generatorName.equals("largebiomes"); ++ ++ generator = noise(seed, amplified ? "minecraft:amplified" : "minecraft:overworld", ++ vanillaBiomeSource(seed, default11Gen, largeBiomes)); ++ break; ++ } ++ } ++ } ++ ++ final boolean mapFeatures = data.getBoolean("MapFeatures", true); ++ final boolean bonusChest = data.getBoolean("BonusChest", false); ++ ++ final MapType ret = Types.NBT.createEmptyMap(); ++ ++ ret.setLong("seed", seed); ++ ret.setBoolean("generate_features", mapFeatures); ++ ret.setBoolean("bonus_chest", bonusChest); ++ ret.setMap("dimensions", vanillaLevels(seed, generator, caves)); ++ if (legacyCustomOptions != null) { ++ ret.setString("legacy_custom_options", legacyCustomOptions); ++ } ++ ++ return ret; ++ } ++ }); ++ } ++ ++ public static MapType noise(final long seed, final String worldType, final MapType biomeSource) { ++ final MapType ret = Types.NBT.createEmptyMap(); ++ ++ ret.setString("type", "minecraft:noise"); ++ ret.setMap("biome_source", biomeSource); ++ ret.setLong("seed", seed); ++ ret.setString("settings", worldType); ++ ++ return ret; ++ } ++ ++ public static MapType vanillaBiomeSource(final long seed, final boolean default11Gen, final boolean largeBiomes) { ++ final MapType ret = Types.NBT.createEmptyMap(); ++ ++ ret.setString("type", "minecraft:vanilla_layered"); ++ ret.setLong("seed", seed); ++ ret.setBoolean("large_biomes", largeBiomes); ++ if (default11Gen) { ++ ret.setBoolean("legacy_biome_init_layer", default11Gen); ++ } ++ ++ return ret; ++ } ++ ++ public static MapType fixFlatStructures(final MapType generatorOptions) { ++ int distance = 32; ++ int spread = 3; ++ int count = 128; ++ boolean stronghold = false; ++ final Map newStructures = new HashMap<>(); ++ ++ if (generatorOptions == null) { ++ stronghold = true; ++ newStructures.put("minecraft:village", DEFAULTS.get("minecraft:village")); ++ } ++ ++ final MapType oldStructures = generatorOptions == null ? null : generatorOptions.getMap("structures"); ++ if (oldStructures != null) { ++ for (final String structureName : oldStructures.keys()) { ++ final MapType structureValues = oldStructures.getMap(structureName); ++ if (structureValues == null) { ++ continue; ++ } ++ ++ for (final String structureValueKey : structureValues.keys()) { ++ final String structureValue = structureValues.getString(structureValueKey); ++ ++ if ("stronghold".equals(structureName)) { ++ stronghold = true; ++ switch (structureValueKey) { ++ case "distance": ++ distance = getInt(structureValue, distance, 1); ++ break; ++ case "spread": ++ spread = getInt(structureValue, spread, 1); ++ break; ++ case "count": ++ count = getInt(structureValue, count, 1); ++ break; ++ } ++ } else { ++ switch (structureValueKey) { ++ case "distance": ++ switch (structureName) { ++ case "village": ++ setSpacing(newStructures, "minecraft:village", structureValue, 9); ++ break; ++ case "biome_1": ++ setSpacing(newStructures, "minecraft:desert_pyramid", structureValue, 9); ++ setSpacing(newStructures, "minecraft:igloo", structureValue, 9); ++ setSpacing(newStructures, "minecraft:jungle_pyramid", structureValue, 9); ++ setSpacing(newStructures, "minecraft:swamp_hut", structureValue, 9); ++ setSpacing(newStructures, "minecraft:pillager_outpost", structureValue, 9); ++ break; ++ case "endcity": ++ setSpacing(newStructures, "minecraft:endcity", structureValue, 1); ++ break; ++ case "mansion": ++ setSpacing(newStructures, "minecraft:mansion", structureValue, 1); ++ break; ++ default: ++ break; ++ } ++ case "separation": ++ if ("oceanmonument".equals(structureName)) { ++ final StructureFeatureConfiguration structure = newStructures.getOrDefault("minecraft:monument", DEFAULTS.get("minecraft:monument")); ++ final int newSpacing = getInt(structureValue, structure.separation, 1); ++ newStructures.put("minecraft:monument", new StructureFeatureConfiguration(newSpacing, structure.separation, structure.salt)); ++ } ++ ++ break; ++ case "spacing": ++ if ("oceanmonument".equals(structureName)) { ++ setSpacing(newStructures, "minecraft:monument", structureValue, 1); ++ } ++ ++ break; ++ } ++ } ++ } ++ } ++ } ++ ++ final MapType ret = Types.NBT.createEmptyMap(); ++ final MapType structuresSerialized = Types.NBT.createEmptyMap(); ++ ret.setMap("structures", structuresSerialized); ++ for (final String key : newStructures.keySet()) { ++ structuresSerialized.setMap(key, newStructures.get(key).serialize()); ++ } ++ ++ if (stronghold) { ++ final MapType strongholdData = Types.NBT.createEmptyMap(); ++ ret.setMap("stronghold", strongholdData); ++ ++ strongholdData.setInt("distance", distance); ++ strongholdData.setInt("spread", spread); ++ strongholdData.setInt("count", count); ++ } ++ ++ return ret; ++ } ++ ++ public static MapType vanillaLevels(final long seed, final MapType generator, final boolean caves) { ++ final MapType ret = Types.NBT.createEmptyMap(); ++ ++ final MapType overworld = Types.NBT.createEmptyMap(); ++ final MapType nether = Types.NBT.createEmptyMap(); ++ final MapType end = Types.NBT.createEmptyMap(); ++ ++ ret.setMap("minecraft:overworld", overworld); ++ ret.setMap("minecraft:the_nether", nether); ++ ret.setMap("minecraft:the_end", end); ++ ++ // overworld ++ overworld.setString("type", caves ? "minecraft:overworld_caves" : "minecraft:overworld"); ++ overworld.setMap("generator", generator); ++ ++ // nether ++ nether.setString("type", "minecraft:the_nether"); ++ final MapType netherBiomeSource = Types.NBT.createEmptyMap(); ++ netherBiomeSource.setString("type", "minecraft:multi_noise"); ++ netherBiomeSource.setLong("seed", seed); ++ netherBiomeSource.setString("preset", "minecraft:nether"); ++ ++ nether.setMap("generator", noise(seed, "minecraft:nether", netherBiomeSource)); ++ ++ // end ++ end.setString("type", "minecraft:the_end"); ++ final MapType endBiomeSource = Types.NBT.createEmptyMap(); ++ endBiomeSource.setString("type", "minecraft:the_end"); ++ endBiomeSource.setLong("seed", seed); ++ end.setMap("generator", noise(seed,"minecraft:end", endBiomeSource)); ++ ++ return ret; ++ } ++ ++ public static MapType defaultOverworld(final long seed) { ++ return noise(seed, "minecraft:overworld", vanillaBiomeSource(seed, false, false)); ++ } ++ ++ private static int getInt(final String value, final int dfl) { ++ return NumberUtils.toInt(value, dfl); ++ } ++ ++ private static int getInt(final String value, final int dfl, final int minVal) { ++ return Math.max(minVal, getInt(value, dfl)); ++ } ++ ++ private static void setSpacing(final Map structures, final String structureName, ++ final String value, final int minVal) { ++ final StructureFeatureConfiguration structure = structures.getOrDefault(structureName, DEFAULTS.get(structureName)); ++ final int newSpacing = getInt(value, structure.spacing, minVal); ++ ++ structures.put(structureName, new StructureFeatureConfiguration(newSpacing, structure.separation, structure.salt)); ++ } ++ ++ private V2550() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2551.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2551.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9cfecd222ef41fdb4f31517a0821d7532386285f +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2551.java +@@ -0,0 +1,103 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++ ++public final class V2551 { ++ ++ private static final int VERSION = MCVersions.V20W20B + 14; ++ ++ public static void register() { ++ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ final MapType dimensions = data.getMap("dimensions"); ++ ++ if (dimensions == null) { ++ return null; ++ } ++ ++ for (final String dimension : dimensions.keys()) { ++ final MapType dimensionData = dimensions.getMap(dimension); ++ if (dimensionData == null) { ++ continue; ++ } ++ ++ final MapType generator = dimensionData.getMap("generator"); ++ if (generator == null) { ++ continue; ++ } ++ ++ final String type = generator.getString("type"); ++ if (type == null) { ++ continue; ++ } ++ ++ switch (type) { ++ case "minecraft:flat": { ++ final MapType settings = generator.getMap("settings"); ++ if (settings == null) { ++ continue; ++ } ++ ++ WalkerUtils.convert(MCTypeRegistry.BIOME, settings, "biome", fromVersion, toVersion); ++ ++ final ListType layers = settings.getList("layers", ObjectType.MAP); ++ if (layers != null) { ++ for (int i = 0, len = layers.size(); i < len; ++i) { ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, layers.getMap(i), "block", fromVersion, toVersion); ++ } ++ } ++ ++ break; ++ } ++ case "minecraft:noise": { ++ final MapType settings = generator.getMap("settings"); ++ if (settings != null) { ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, settings, "default_block", fromVersion, toVersion); ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, settings, "default_fluid", fromVersion, toVersion); ++ } ++ ++ final MapType biomeSource = generator.getMap("biome_source"); ++ if (biomeSource != null) { ++ final String biomeSourceType = biomeSource.getString("type", ""); ++ switch (biomeSourceType) { ++ case "minecraft:fixed": { ++ WalkerUtils.convert(MCTypeRegistry.BIOME, biomeSource, "biome", fromVersion, toVersion); ++ break; ++ } ++ ++ case "minecraft:multi_noise": { ++ // Vanilla's schema is wrong. It should be DSL.fields("biomes", DSL.list(DSL.fields("biome"))) ++ // But it just contains the list part. That obviously can never be the case, because ++ // the root object is a compound, not a list. ++ ++ final ListType biomes = biomeSource.getList("biomes", ObjectType.MAP); ++ if (biomes != null) { ++ for (int i = 0, len = biomes.size(); i < len; ++i) { ++ WalkerUtils.convert(MCTypeRegistry.BIOME, biomes.getMap(i), "biome", fromVersion, toVersion); ++ } ++ } ++ break; ++ } ++ ++ case "minecraft:checkerboard": { ++ WalkerUtils.convertList(MCTypeRegistry.BIOME, biomeSource, "biomes", fromVersion, toVersion); ++ break; ++ } ++ } ++ } ++ ++ break; ++ } ++ } ++ } ++ ++ return null; ++ }); ++ } ++ ++ private V2551() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2552.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2552.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9e6c7dc40d509cf424976831382425ab7eceb024 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2552.java +@@ -0,0 +1,22 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++ ++public final class V2552 { ++ ++ private static final int VERSION = MCVersions.V20W20B + 15; ++ ++ public static void register() { ++ ConverterAbstractStringValueTypeRename.register(VERSION, MCTypeRegistry.BIOME, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:nether", "minecraft:nether_wastes" ++ ) ++ )::get); ++ } ++ ++ private V2552() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2553.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2553.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f019774923bf08fc0f7dc7cafd5fb66fdd7427f8 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2553.java +@@ -0,0 +1,77 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V2553 { ++ ++ private static final int VERSION = MCVersions.V20W20B + 16; ++ ++ public static final Map BIOME_RENAMES = new HashMap<>( ++ ImmutableMap.builder() ++ .put("minecraft:extreme_hills", "minecraft:mountains") ++ .put("minecraft:swampland", "minecraft:swamp") ++ .put("minecraft:hell", "minecraft:nether_wastes") ++ .put("minecraft:sky", "minecraft:the_end") ++ .put("minecraft:ice_flats", "minecraft:snowy_tundra") ++ .put("minecraft:ice_mountains", "minecraft:snowy_mountains") ++ .put("minecraft:mushroom_island", "minecraft:mushroom_fields") ++ .put("minecraft:mushroom_island_shore", "minecraft:mushroom_field_shore") ++ .put("minecraft:beaches", "minecraft:beach") ++ .put("minecraft:forest_hills", "minecraft:wooded_hills") ++ .put("minecraft:smaller_extreme_hills", "minecraft:mountain_edge") ++ .put("minecraft:stone_beach", "minecraft:stone_shore") ++ .put("minecraft:cold_beach", "minecraft:snowy_beach") ++ .put("minecraft:roofed_forest", "minecraft:dark_forest") ++ .put("minecraft:taiga_cold", "minecraft:snowy_taiga") ++ .put("minecraft:taiga_cold_hills", "minecraft:snowy_taiga_hills") ++ .put("minecraft:redwood_taiga", "minecraft:giant_tree_taiga") ++ .put("minecraft:redwood_taiga_hills", "minecraft:giant_tree_taiga_hills") ++ .put("minecraft:extreme_hills_with_trees", "minecraft:wooded_mountains") ++ .put("minecraft:savanna_rock", "minecraft:savanna_plateau") ++ .put("minecraft:mesa", "minecraft:badlands") ++ .put("minecraft:mesa_rock", "minecraft:wooded_badlands_plateau") ++ .put("minecraft:mesa_clear_rock", "minecraft:badlands_plateau") ++ .put("minecraft:sky_island_low", "minecraft:small_end_islands") ++ .put("minecraft:sky_island_medium", "minecraft:end_midlands") ++ .put("minecraft:sky_island_high", "minecraft:end_highlands") ++ .put("minecraft:sky_island_barren", "minecraft:end_barrens") ++ .put("minecraft:void", "minecraft:the_void") ++ .put("minecraft:mutated_plains", "minecraft:sunflower_plains") ++ .put("minecraft:mutated_desert", "minecraft:desert_lakes") ++ .put("minecraft:mutated_extreme_hills", "minecraft:gravelly_mountains") ++ .put("minecraft:mutated_forest", "minecraft:flower_forest") ++ .put("minecraft:mutated_taiga", "minecraft:taiga_mountains") ++ .put("minecraft:mutated_swampland", "minecraft:swamp_hills") ++ .put("minecraft:mutated_ice_flats", "minecraft:ice_spikes") ++ .put("minecraft:mutated_jungle", "minecraft:modified_jungle") ++ .put("minecraft:mutated_jungle_edge", "minecraft:modified_jungle_edge") ++ .put("minecraft:mutated_birch_forest", "minecraft:tall_birch_forest") ++ .put("minecraft:mutated_birch_forest_hills", "minecraft:tall_birch_hills") ++ .put("minecraft:mutated_roofed_forest", "minecraft:dark_forest_hills") ++ .put("minecraft:mutated_taiga_cold", "minecraft:snowy_taiga_mountains") ++ .put("minecraft:mutated_redwood_taiga", "minecraft:giant_spruce_taiga") ++ .put("minecraft:mutated_redwood_taiga_hills", "minecraft:giant_spruce_taiga_hills") ++ .put("minecraft:mutated_extreme_hills_with_trees", "minecraft:modified_gravelly_mountains") ++ .put("minecraft:mutated_savanna", "minecraft:shattered_savanna") ++ .put("minecraft:mutated_savanna_rock", "minecraft:shattered_savanna_plateau") ++ .put("minecraft:mutated_mesa", "minecraft:eroded_badlands") ++ .put("minecraft:mutated_mesa_rock", "minecraft:modified_wooded_badlands_plateau") ++ .put("minecraft:mutated_mesa_clear_rock", "minecraft:modified_badlands_plateau") ++ .put("minecraft:warm_deep_ocean", "minecraft:deep_warm_ocean") ++ .put("minecraft:lukewarm_deep_ocean", "minecraft:deep_lukewarm_ocean") ++ .put("minecraft:cold_deep_ocean", "minecraft:deep_cold_ocean") ++ .put("minecraft:frozen_deep_ocean", "minecraft:deep_frozen_ocean") ++ .build() ++ ); ++ ++ public static void register() { ++ ConverterAbstractStringValueTypeRename.register(VERSION, MCTypeRegistry.BIOME, BIOME_RENAMES::get); ++ } ++ ++ private V2553() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2558.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2558.java +new file mode 100644 +index 0000000000000000000000000000000000000000..137a530c1b979e7257b77f405885aa9f4d376c11 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2558.java +@@ -0,0 +1,48 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.options.ConverterAbstractOptionsRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.Types; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++ ++public final class V2558 { ++ ++ private static final int VERSION = MCVersions.V1_16_PRE2 + 1; ++ ++ public static void register() { ++ ConverterAbstractOptionsRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "key_key.swapHands", "key_key.swapOffhand" ++ ) ++ )::get); ++ ++ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ MapType dimensions = data.getMap("dimensions"); ++ if (dimensions == null) { ++ dimensions = Types.NBT.createEmptyMap(); ++ data.setMap("dimensions", dimensions); ++ } ++ ++ if (dimensions.isEmpty()) { ++ data.setMap("dimensions", recreateSettings(data)); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private static MapType recreateSettings(final MapType data) { ++ final long seed = data.getLong("seed"); ++ ++ return V2550.vanillaLevels(seed, V2550.defaultOverworld(seed), false); ++ } ++ ++ private V2558() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2568.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2568.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e50fbc38dbf9198c0c652b506e50780eca368bb0 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2568.java +@@ -0,0 +1,18 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++ ++public final class V2568 { ++ ++ private static final int VERSION = MCVersions.V1_16_1 + 1; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ registerMob("minecraft:piglin_brute"); ++ } ++ ++ private V2568() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2671.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2671.java +new file mode 100644 +index 0000000000000000000000000000000000000000..140bfff947e540452f3794eda2f1e2122f8d3f27 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2671.java +@@ -0,0 +1,18 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++ ++public final class V2671 { ++ ++ private static final int VERSION = MCVersions.V1_16_5 + 85; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ registerMob("minecraft:goat"); ++ } ++ ++ private V2671() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2679.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2679.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7ec79da7e8871d6beca05a25c70d8c6811531faa +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2679.java +@@ -0,0 +1,38 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V2679 { ++ ++ private static final int VERSION = MCVersions.V1_16_5 + 93; ++ ++ public static void register() { ++ MCTypeRegistry.BLOCK_STATE.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (!"minecraft:cauldron".equals(data.getString("Name"))) { ++ return null; ++ } ++ ++ final MapType properties = data.getMap("Properties"); ++ ++ if (properties == null) { ++ return null; ++ } ++ ++ if (properties.getString("level", "0").equals("0")) { ++ data.remove("Properties"); ++ return null; ++ } else { ++ data.setString("Name", "minecraft:water_cauldron"); ++ return null; ++ } ++ } ++ }); ++ } ++ ++ private V2679() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2680.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2680.java +new file mode 100644 +index 0000000000000000000000000000000000000000..87cbe1c717635908a30c57028346a1abeb21e6a6 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2680.java +@@ -0,0 +1,27 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++ ++public final class V2680 { ++ ++ private static final int VERSION = MCVersions.V1_16_5 + 94; ++ ++ public static void register() { ++ ConverterAbstractItemRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:grass_path", "minecraft:dirt_path" ++ ) ++ )::get); ++ ConverterAbstractBlockRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:grass_path", "minecraft:dirt_path" ++ ) ++ )::get); ++ } ++ ++ private V2680() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2684.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2684.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0c996642f561d2471a506a34f5efe6dae5cd1fb3 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2684.java +@@ -0,0 +1,16 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.game_event.GameEventListenerWalker; ++ ++public final class V2684 { ++ ++ private static final int VERSION = MCVersions.V20W48A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:sculk_sensor", new GameEventListenerWalker()); ++ } ++ ++ private V2684() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2686.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2686.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1f1685cb0e1427e88dc5c970b0cb58aae0393396 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2686.java +@@ -0,0 +1,18 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++ ++public final class V2686 { ++ ++ private static final int VERSION = MCVersions.V20W49A + 1; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ registerMob("minecraft:axolotl"); ++ } ++ ++ private V2686() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2688.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2688.java +new file mode 100644 +index 0000000000000000000000000000000000000000..15a7bf7b7ea883d7a3cee9183b92e12838efd690 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2688.java +@@ -0,0 +1,22 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; ++ ++public final class V2688 { ++ ++ private static final int VERSION = MCVersions.V20W51A + 1; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ registerMob("minecraft:glow_squid"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:glow_item_frame", new DataWalkerItems("Item")); ++ } ++ ++ private V2688() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2690.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2690.java +new file mode 100644 +index 0000000000000000000000000000000000000000..39ffcec6e78229dd62abfd42c1ac64c3ccccc6dc +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2690.java +@@ -0,0 +1,45 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V2690 { ++ ++ private static final int VERSION = MCVersions.V21W05A; ++ ++ private static final Map RENAMES = new HashMap<>( ++ ImmutableMap.builder() ++ .put("minecraft:weathered_copper_block", "minecraft:oxidized_copper_block") ++ .put("minecraft:semi_weathered_copper_block", "minecraft:weathered_copper_block") ++ .put("minecraft:lightly_weathered_copper_block", "minecraft:exposed_copper_block") ++ .put("minecraft:weathered_cut_copper", "minecraft:oxidized_cut_copper") ++ .put("minecraft:semi_weathered_cut_copper", "minecraft:weathered_cut_copper") ++ .put("minecraft:lightly_weathered_cut_copper", "minecraft:exposed_cut_copper") ++ .put("minecraft:weathered_cut_copper_stairs", "minecraft:oxidized_cut_copper_stairs") ++ .put("minecraft:semi_weathered_cut_copper_stairs", "minecraft:weathered_cut_copper_stairs") ++ .put("minecraft:lightly_weathered_cut_copper_stairs", "minecraft:exposed_cut_copper_stairs") ++ .put("minecraft:weathered_cut_copper_slab", "minecraft:oxidized_cut_copper_slab") ++ .put("minecraft:semi_weathered_cut_copper_slab", "minecraft:weathered_cut_copper_slab") ++ .put("minecraft:lightly_weathered_cut_copper_slab", "minecraft:exposed_cut_copper_slab") ++ .put("minecraft:waxed_semi_weathered_copper", "minecraft:waxed_weathered_copper") ++ .put("minecraft:waxed_lightly_weathered_copper", "minecraft:waxed_exposed_copper") ++ .put("minecraft:waxed_semi_weathered_cut_copper", "minecraft:waxed_weathered_cut_copper") ++ .put("minecraft:waxed_lightly_weathered_cut_copper", "minecraft:waxed_exposed_cut_copper") ++ .put("minecraft:waxed_semi_weathered_cut_copper_stairs", "minecraft:waxed_weathered_cut_copper_stairs") ++ .put("minecraft:waxed_lightly_weathered_cut_copper_stairs", "minecraft:waxed_exposed_cut_copper_stairs") ++ .put("minecraft:waxed_semi_weathered_cut_copper_slab", "minecraft:waxed_weathered_cut_copper_slab") ++ .put("minecraft:waxed_lightly_weathered_cut_copper_slab", "minecraft:waxed_exposed_cut_copper_slab") ++ .build() ++ ); ++ ++ public static void register() { ++ ConverterAbstractItemRename.register(VERSION, RENAMES::get); ++ ConverterAbstractBlockRename.register(VERSION, RENAMES::get); ++ } ++ ++ private V2690() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2691.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2691.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bb87bcedfc2ed8d19b266e925beca0f54b50a0ab +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2691.java +@@ -0,0 +1,29 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V2691 { ++ ++ private static final int VERSION = MCVersions.V21W05A + 1; ++ ++ private static final Map RENAMES = new HashMap<>( ++ ImmutableMap.builder() ++ .put("minecraft:waxed_copper", "minecraft:waxed_copper_block") ++ .put("minecraft:oxidized_copper_block", "minecraft:oxidized_copper") ++ .put("minecraft:weathered_copper_block", "minecraft:weathered_copper") ++ .put("minecraft:exposed_copper_block", "minecraft:exposed_copper") ++ .build() ++ ); ++ ++ public static void register() { ++ ConverterAbstractItemRename.register(VERSION, RENAMES::get); ++ ConverterAbstractBlockRename.register(VERSION, RENAMES::get); ++ } ++ ++ private V2691() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2693.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2693.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a242e8e9a7a7c80c00ec0d64542b3d7dc3103e24 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2693.java +@@ -0,0 +1,16 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.AddFlagIfAbsent; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++ ++public final class V2693 { ++ ++ private static final int VERSION = MCVersions.V21W05B + 1; ++ ++ public static void register() { ++ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureConverter(new AddFlagIfAbsent(VERSION, "has_increased_height_already", false)); ++ } ++ ++ private V2693() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2696.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2696.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ce568002e54924e001c12271f0bde7183bc23c61 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2696.java +@@ -0,0 +1,42 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V2696 { ++ ++ private static final int VERSION = MCVersions.V21W07A + 1; ++ ++ private static final Map RENAMES = new HashMap<>( ++ ImmutableMap.builder() ++ .put("minecraft:grimstone", "minecraft:deepslate") ++ .put("minecraft:grimstone_slab", "minecraft:cobbled_deepslate_slab") ++ .put("minecraft:grimstone_stairs", "minecraft:cobbled_deepslate_stairs") ++ .put("minecraft:grimstone_wall", "minecraft:cobbled_deepslate_wall") ++ .put("minecraft:polished_grimstone", "minecraft:polished_deepslate") ++ .put("minecraft:polished_grimstone_slab", "minecraft:polished_deepslate_slab") ++ .put("minecraft:polished_grimstone_stairs", "minecraft:polished_deepslate_stairs") ++ .put("minecraft:polished_grimstone_wall", "minecraft:polished_deepslate_wall") ++ .put("minecraft:grimstone_tiles", "minecraft:deepslate_tiles") ++ .put("minecraft:grimstone_tile_slab", "minecraft:deepslate_tile_slab") ++ .put("minecraft:grimstone_tile_stairs", "minecraft:deepslate_tile_stairs") ++ .put("minecraft:grimstone_tile_wall", "minecraft:deepslate_tile_wall") ++ .put("minecraft:grimstone_bricks", "minecraft:deepslate_bricks") ++ .put("minecraft:grimstone_brick_slab", "minecraft:deepslate_brick_slab") ++ .put("minecraft:grimstone_brick_stairs", "minecraft:deepslate_brick_stairs") ++ .put("minecraft:grimstone_brick_wall", "minecraft:deepslate_brick_wall") ++ .put("minecraft:chiseled_grimstone", "minecraft:chiseled_deepslate") ++ .build() ++ ); ++ ++ public static void register() { ++ ConverterAbstractItemRename.register(VERSION, RENAMES::get); ++ ConverterAbstractBlockRename.register(VERSION, RENAMES::get); ++ } ++ ++ private V2696() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2700.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2700.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e6a2f29b20aa6d7cd431fc63c2d8ed70dc9a2ab8 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2700.java +@@ -0,0 +1,22 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++ ++public final class V2700 { ++ ++ private static final int VERSION = MCVersions.V21W10A + 1; ++ ++ public static void register() { ++ ConverterAbstractBlockRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:cave_vines_head", "minecraft:cave_vines", ++ "minecraft:cave_vines_body", "minecraft:cave_vines_plant" ++ ) ++ )::get); ++ } ++ ++ private V2700() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2701.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2701.java +new file mode 100644 +index 0000000000000000000000000000000000000000..dc1604f48a9f15721e709f2e128210085520c15e +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2701.java +@@ -0,0 +1,205 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import com.google.common.collect.Sets; ++import java.util.Set; ++import java.util.regex.Matcher; ++import java.util.regex.Pattern; ++ ++public final class V2701 { ++ ++ private static final int VERSION = MCVersions.V21W10A + 2; ++ ++ private static final Pattern INDEX_PATTERN = Pattern.compile("\\[(\\d+)\\]"); ++ ++ private static final Set PIECE_TYPE = Sets.newHashSet( ++ "minecraft:jigsaw", ++ "minecraft:nvi", ++ "minecraft:pcp", ++ "minecraft:bastionremnant", ++ "minecraft:runtime" ++ ); ++ private static final Set FEATURES = Sets.newHashSet( ++ "minecraft:tree", ++ "minecraft:flower", ++ "minecraft:block_pile", ++ "minecraft:random_patch" ++ ); ++ ++ public static void register() { ++ MCTypeRegistry.STRUCTURE_FEATURE.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final ListType children = data.getList("Children", ObjectType.MAP); ++ ++ if (children == null) { ++ return null; ++ } ++ ++ for (int i = 0, len = children.size(); i < len; ++i) { ++ final MapType child = children.getMap(i); ++ ++ if (!PIECE_TYPE.contains(child.getString("id"))) { ++ continue; ++ } ++ ++ final String poolElement = child.getString("pool_element"); ++ if (!"minecraft:feature_pool_element".equals(poolElement)) { ++ continue; ++ } ++ ++ final MapType feature = child.getMap("feature"); ++ if (feature == null) { ++ continue; ++ } ++ ++ final String replacement = convertToString(feature); ++ ++ if (replacement != null) { ++ child.setString("feature", replacement); ++ } ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private static String getNestedString(final MapType root, final String... paths) { ++ if (paths.length == 0) { ++ throw new IllegalArgumentException("Missing path"); ++ } ++ ++ Object current = root.getGeneric(paths[0]); ++ ++ for (int i = 1; i < paths.length; ++i) { ++ final String path = paths[i]; ++ ++ final Matcher indexMatcher = INDEX_PATTERN.matcher(path); ++ if (!indexMatcher.matches()) { ++ current = (current instanceof MapType) ? ((MapType)current).getGeneric(path) : null; ++ if (current == null) { ++ break; ++ } ++ continue; ++ } ++ ++ final int index = Integer.parseInt(indexMatcher.group(1)); ++ if (!(current instanceof ListType)) { ++ current = null; ++ break; ++ } else { ++ final ListType list = (ListType)current; ++ if (index >= 0 && index < list.size()) { ++ current = list.getGeneric(index); ++ } else { ++ current = null; ++ break; ++ } ++ } ++ } ++ ++ return current instanceof String ? (String)current : ""; ++ } ++ ++ private static String convertToString(final MapType feature) { ++ return getReplacement( ++ getNestedString(feature, "type"), ++ getNestedString(feature, "name"), ++ getNestedString(feature, "config", "state_provider", "type"), ++ getNestedString(feature, "config", "state_provider", "state", "Name"), ++ getNestedString(feature, "config", "state_provider", "entries", "[0]", "data", "Name"), ++ getNestedString(feature, "config", "foliage_placer", "type"), ++ getNestedString(feature, "config", "leaves_provider", "state", "Name") ++ ); ++ } ++ ++ private static String getReplacement(final String type, final String name, final String stateType, final String stateName, ++ final String firstEntryName, final String foliageName, final String leavesName) { ++ final String actualType; ++ if (!type.isEmpty()) { ++ actualType = type; ++ } else { ++ if (name.isEmpty()) { ++ return null; ++ } ++ ++ if ("minecraft:normal_tree".equals(name)) { ++ actualType = "minecraft:tree"; ++ } else { ++ actualType = name; ++ } ++ } ++ ++ if (FEATURES.contains(actualType)) { ++ if ("minecraft:random_patch".equals(actualType)) { ++ if ("minecraft:simple_state_provider".equals(stateType)) { ++ if ("minecraft:sweet_berry_bush".equals(stateName)) { ++ return "minecraft:patch_berry_bush"; ++ } ++ ++ if ("minecraft:cactus".equals(stateName)) { ++ return "minecraft:patch_cactus"; ++ } ++ } else if ("minecraft:weighted_state_provider".equals(stateType) && ("minecraft:grass".equals(firstEntryName) || "minecraft:fern".equals(firstEntryName))) { ++ return "minecraft:patch_taiga_grass"; ++ } ++ } else if ("minecraft:block_pile".equals(actualType)) { ++ if (!"minecraft:simple_state_provider".equals(stateType) && !"minecraft:rotated_block_provider".equals(stateType)) { ++ if ("minecraft:weighted_state_provider".equals(stateType)) { ++ if ("minecraft:packed_ice".equals(firstEntryName) || "minecraft:blue_ice".equals(firstEntryName)) { ++ return "minecraft:pile_ice"; ++ } ++ ++ if ("minecraft:jack_o_lantern".equals(firstEntryName) || "minecraft:pumpkin".equals(firstEntryName)) { ++ return "minecraft:pile_pumpkin"; ++ } ++ } ++ } else { ++ if ("minecraft:hay_block".equals(stateName)) { ++ return "minecraft:pile_hay"; ++ } ++ ++ if ("minecraft:melon".equals(stateName)) { ++ return "minecraft:pile_melon"; ++ } ++ ++ if ("minecraft:snow".equals(stateName)) { ++ return "minecraft:pile_snow"; ++ } ++ } ++ } else { ++ if ("minecraft:flower".equals(actualType)) { ++ return "minecraft:flower_plain"; ++ } ++ ++ if ("minecraft:tree".equals(actualType)) { ++ if ("minecraft:acacia_foliage_placer".equals(foliageName)) { ++ return "minecraft:acacia"; ++ } ++ ++ if ("minecraft:blob_foliage_placer".equals(foliageName) && "minecraft:oak_leaves".equals(leavesName)) { ++ return "minecraft:oak"; ++ } ++ ++ if ("minecraft:pine_foliage_placer".equals(foliageName)) { ++ return "minecraft:pine"; ++ } ++ ++ if ("minecraft:spruce_foliage_placer".equals(foliageName)) { ++ return "minecraft:spruce"; ++ } ++ } ++ } ++ } ++ ++ return null; ++ } ++ ++ private V2701() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2702.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2702.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cc89ca8a01c2589c807be2a7560bcc6051417379 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2702.java +@@ -0,0 +1,35 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V2702 { ++ ++ private static final int VERSION = MCVersions.V21W10A + 3; ++ ++ public static void register() { ++ final DataConverter, MapType> arrowConverter = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (data.hasKey("pickup")) { ++ return null; ++ } ++ ++ final boolean player = data.getBoolean("player", true); ++ data.remove("player"); ++ ++ data.setByte("pickup", player ? (byte)1 : (byte)0); ++ ++ return null; ++ } ++ }; ++ ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:arrow", arrowConverter); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:spectral_arrow", arrowConverter); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:trident", arrowConverter); ++ } ++ ++ private V2702() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2707.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2707.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3c5fc48f39c08249a61199c7f72dddee65fd98af +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2707.java +@@ -0,0 +1,22 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.AddFlagIfAbsent; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++ ++public final class V2707 { ++ ++ private static final int VERSION = MCVersions.V21W14A + 1; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureConverter(new AddFlagIfAbsent(VERSION, "has_increased_height_already", true)); ++ ++ registerMob("minecraft:marker"); // ????????????? ++ } ++ ++ private V2707() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2710.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2710.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0967c8c794869a972c1283cab6b3f3cef1d77aec +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2710.java +@@ -0,0 +1,21 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.stats.ConverterAbstractStatsRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++ ++public final class V2710 { ++ ++ private static final int VERSION = MCVersions.V21W15A + 1; ++ ++ public static void register() { ++ ConverterAbstractStatsRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:play_one_minute", "minecraft:play_time" ++ ) ++ )::get); ++ } ++ ++ private V2710() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2717.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2717.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e0d6b2f6b00e0bfce205efa889de1765ef22793a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2717.java +@@ -0,0 +1,25 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V2717 { ++ ++ private static final int VERSION = MCVersions.V1_17_PRE1 + 1; ++ ++ public static void register() { ++ final Map rename = new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:azalea_leaves_flowers", "minecraft:flowering_azalea_leaves" ++ ) ++ ); ++ ConverterAbstractItemRename.register(VERSION, rename::get); ++ ConverterAbstractBlockRename.register(VERSION, rename::get); ++ } ++ ++ private V2717() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2825.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2825.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cd00c9398791967be6dd10f7183c61902431b27a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2825.java +@@ -0,0 +1,16 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.AddFlagIfAbsent; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++ ++public final class V2825 { ++ ++ private static final int VERSION = MCVersions.V1_17_1 + 95; ++ ++ public static void register() { ++ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureConverter(new AddFlagIfAbsent(VERSION, "has_increased_height_already", false)); ++ } ++ ++ private V2825() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2831.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2831.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1b692e4866d99c89705289ad1f386f467382b1c7 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2831.java +@@ -0,0 +1,71 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.Types; ++ ++public final class V2831 { ++ ++ private static final int VERSION = MCVersions.V1_17_1 + 101; ++ ++ public static void register() { ++ MCTypeRegistry.UNTAGGED_SPAWNER.addStructureWalker(VERSION, (final MapType root, final long fromVersion, final long toVersion) -> { ++ final ListType spawnPotentials = root.getList("SpawnPotentials", ObjectType.MAP); ++ if (spawnPotentials != null) { ++ for (int i = 0, len = spawnPotentials.size(); i < len; ++i) { ++ final MapType spawnPotential = spawnPotentials.getMap(i); ++ ++ WalkerUtils.convert(MCTypeRegistry.ENTITY, spawnPotential.getMap("data"), "entity", fromVersion, toVersion); ++ } ++ } ++ ++ WalkerUtils.convert(MCTypeRegistry.ENTITY, root.getMap("SpawnData"), "entity", fromVersion, toVersion); ++ ++ return null; ++ }); ++ ++ MCTypeRegistry.UNTAGGED_SPAWNER.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType root, final long sourceVersion, final long toVersion) { ++ final MapType spawnData = root.getMap("SpawnData"); ++ if (spawnData != null) { ++ final MapType wrapped = Types.NBT.createEmptyMap(); ++ root.setMap("SpawnData", wrapped); ++ ++ wrapped.setMap("entity", spawnData); ++ } ++ ++ final ListType spawnPotentials = root.getList("SpawnPotentials", ObjectType.MAP); ++ if (spawnPotentials != null) { ++ for (int i = 0, len = spawnPotentials.size(); i < len; ++i) { ++ final MapType spawnPotential = spawnPotentials.getMap(i); ++ ++ // new format of weighted list (SpawnPotentials): ++ // root.data -> data ++ // root.weight -> weight ++ ++ final MapType entity = spawnPotential.getMap("Entity"); ++ final int weight = spawnPotential.getInt("Weight", 1); ++ spawnPotential.remove("Entity"); ++ spawnPotential.remove("Weight"); ++ spawnPotential.setInt("weight", weight); ++ ++ final MapType data = Types.NBT.createEmptyMap(); ++ spawnPotential.setMap("data", data); ++ ++ data.setMap("entity", entity); ++ } ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V2831() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2832.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2832.java +new file mode 100644 +index 0000000000000000000000000000000000000000..21d1617d222d0b82b1c5222a0ef1a1fa9da02ab8 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2832.java +@@ -0,0 +1,929 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.Types; ++import com.mojang.logging.LogUtils; ++import it.unimi.dsi.fastutil.ints.Int2IntLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.ints.IntIterator; ++import it.unimi.dsi.fastutil.ints.IntOpenHashSet; ++import org.apache.commons.lang3.mutable.MutableBoolean; ++import org.slf4j.Logger; ++import java.util.Arrays; ++import java.util.BitSet; ++import java.util.HashSet; ++import java.util.Set; ++ ++public final class V2832 { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ private static final int VERSION = MCVersions.V1_17_1 + 102; ++ ++ private static final String[] BIOMES_BY_ID = new String[256]; // rip datapacks ++ static { ++ BIOMES_BY_ID[0] = "minecraft:ocean"; ++ BIOMES_BY_ID[1] = "minecraft:plains"; ++ BIOMES_BY_ID[2] = "minecraft:desert"; ++ BIOMES_BY_ID[3] = "minecraft:mountains"; ++ BIOMES_BY_ID[4] = "minecraft:forest"; ++ BIOMES_BY_ID[5] = "minecraft:taiga"; ++ BIOMES_BY_ID[6] = "minecraft:swamp"; ++ BIOMES_BY_ID[7] = "minecraft:river"; ++ BIOMES_BY_ID[8] = "minecraft:nether_wastes"; ++ BIOMES_BY_ID[9] = "minecraft:the_end"; ++ BIOMES_BY_ID[10] = "minecraft:frozen_ocean"; ++ BIOMES_BY_ID[11] = "minecraft:frozen_river"; ++ BIOMES_BY_ID[12] = "minecraft:snowy_tundra"; ++ BIOMES_BY_ID[13] = "minecraft:snowy_mountains"; ++ BIOMES_BY_ID[14] = "minecraft:mushroom_fields"; ++ BIOMES_BY_ID[15] = "minecraft:mushroom_field_shore"; ++ BIOMES_BY_ID[16] = "minecraft:beach"; ++ BIOMES_BY_ID[17] = "minecraft:desert_hills"; ++ BIOMES_BY_ID[18] = "minecraft:wooded_hills"; ++ BIOMES_BY_ID[19] = "minecraft:taiga_hills"; ++ BIOMES_BY_ID[20] = "minecraft:mountain_edge"; ++ BIOMES_BY_ID[21] = "minecraft:jungle"; ++ BIOMES_BY_ID[22] = "minecraft:jungle_hills"; ++ BIOMES_BY_ID[23] = "minecraft:jungle_edge"; ++ BIOMES_BY_ID[24] = "minecraft:deep_ocean"; ++ BIOMES_BY_ID[25] = "minecraft:stone_shore"; ++ BIOMES_BY_ID[26] = "minecraft:snowy_beach"; ++ BIOMES_BY_ID[27] = "minecraft:birch_forest"; ++ BIOMES_BY_ID[28] = "minecraft:birch_forest_hills"; ++ BIOMES_BY_ID[29] = "minecraft:dark_forest"; ++ BIOMES_BY_ID[30] = "minecraft:snowy_taiga"; ++ BIOMES_BY_ID[31] = "minecraft:snowy_taiga_hills"; ++ BIOMES_BY_ID[32] = "minecraft:giant_tree_taiga"; ++ BIOMES_BY_ID[33] = "minecraft:giant_tree_taiga_hills"; ++ BIOMES_BY_ID[34] = "minecraft:wooded_mountains"; ++ BIOMES_BY_ID[35] = "minecraft:savanna"; ++ BIOMES_BY_ID[36] = "minecraft:savanna_plateau"; ++ BIOMES_BY_ID[37] = "minecraft:badlands"; ++ BIOMES_BY_ID[38] = "minecraft:wooded_badlands_plateau"; ++ BIOMES_BY_ID[39] = "minecraft:badlands_plateau"; ++ BIOMES_BY_ID[40] = "minecraft:small_end_islands"; ++ BIOMES_BY_ID[41] = "minecraft:end_midlands"; ++ BIOMES_BY_ID[42] = "minecraft:end_highlands"; ++ BIOMES_BY_ID[43] = "minecraft:end_barrens"; ++ BIOMES_BY_ID[44] = "minecraft:warm_ocean"; ++ BIOMES_BY_ID[45] = "minecraft:lukewarm_ocean"; ++ BIOMES_BY_ID[46] = "minecraft:cold_ocean"; ++ BIOMES_BY_ID[47] = "minecraft:deep_warm_ocean"; ++ BIOMES_BY_ID[48] = "minecraft:deep_lukewarm_ocean"; ++ BIOMES_BY_ID[49] = "minecraft:deep_cold_ocean"; ++ BIOMES_BY_ID[50] = "minecraft:deep_frozen_ocean"; ++ BIOMES_BY_ID[127] = "minecraft:the_void"; ++ BIOMES_BY_ID[129] = "minecraft:sunflower_plains"; ++ BIOMES_BY_ID[130] = "minecraft:desert_lakes"; ++ BIOMES_BY_ID[131] = "minecraft:gravelly_mountains"; ++ BIOMES_BY_ID[132] = "minecraft:flower_forest"; ++ BIOMES_BY_ID[133] = "minecraft:taiga_mountains"; ++ BIOMES_BY_ID[134] = "minecraft:swamp_hills"; ++ BIOMES_BY_ID[140] = "minecraft:ice_spikes"; ++ BIOMES_BY_ID[149] = "minecraft:modified_jungle"; ++ BIOMES_BY_ID[151] = "minecraft:modified_jungle_edge"; ++ BIOMES_BY_ID[155] = "minecraft:tall_birch_forest"; ++ BIOMES_BY_ID[156] = "minecraft:tall_birch_hills"; ++ BIOMES_BY_ID[157] = "minecraft:dark_forest_hills"; ++ BIOMES_BY_ID[158] = "minecraft:snowy_taiga_mountains"; ++ BIOMES_BY_ID[160] = "minecraft:giant_spruce_taiga"; ++ BIOMES_BY_ID[161] = "minecraft:giant_spruce_taiga_hills"; ++ BIOMES_BY_ID[162] = "minecraft:modified_gravelly_mountains"; ++ BIOMES_BY_ID[163] = "minecraft:shattered_savanna"; ++ BIOMES_BY_ID[164] = "minecraft:shattered_savanna_plateau"; ++ BIOMES_BY_ID[165] = "minecraft:eroded_badlands"; ++ BIOMES_BY_ID[166] = "minecraft:modified_wooded_badlands_plateau"; ++ BIOMES_BY_ID[167] = "minecraft:modified_badlands_plateau"; ++ BIOMES_BY_ID[168] = "minecraft:bamboo_jungle"; ++ BIOMES_BY_ID[169] = "minecraft:bamboo_jungle_hills"; ++ BIOMES_BY_ID[170] = "minecraft:soul_sand_valley"; ++ BIOMES_BY_ID[171] = "minecraft:crimson_forest"; ++ BIOMES_BY_ID[172] = "minecraft:warped_forest"; ++ BIOMES_BY_ID[173] = "minecraft:basalt_deltas"; ++ BIOMES_BY_ID[174] = "minecraft:dripstone_caves"; ++ BIOMES_BY_ID[175] = "minecraft:lush_caves"; ++ BIOMES_BY_ID[177] = "minecraft:meadow"; ++ BIOMES_BY_ID[178] = "minecraft:grove"; ++ BIOMES_BY_ID[179] = "minecraft:snowy_slopes"; ++ BIOMES_BY_ID[180] = "minecraft:snowcapped_peaks"; ++ BIOMES_BY_ID[181] = "minecraft:lofty_peaks"; ++ BIOMES_BY_ID[182] = "minecraft:stony_peaks"; ++ } ++ ++ private static final String[] HEIGHTMAP_TYPES = new String[] { ++ "WORLD_SURFACE_WG", ++ "WORLD_SURFACE", ++ "WORLD_SURFACE_IGNORE_SNOW", ++ "OCEAN_FLOOR_WG", ++ "OCEAN_FLOOR", ++ "MOTION_BLOCKING", ++ "MOTION_BLOCKING_NO_LEAVES" ++ }; ++ ++ private static final Set STATUS_IS_OR_AFTER_SURFACE = new HashSet<>(Arrays.asList( ++ "surface", ++ "carvers", ++ "liquid_carvers", ++ "features", ++ "light", ++ "spawn", ++ "heightmaps", ++ "full" ++ )); ++ private static final Set STATUS_IS_OR_AFTER_NOISE = new HashSet<>(Arrays.asList( ++ "noise", ++ "surface", ++ "carvers", ++ "liquid_carvers", ++ "features", ++ "light", ++ "spawn", ++ "heightmaps", ++ "full" ++ )); ++ private static final Set BLOCKS_BEFORE_FEATURE_STATUS = new HashSet<>(Arrays.asList( ++ "minecraft:air", ++ "minecraft:basalt", ++ "minecraft:bedrock", ++ "minecraft:blackstone", ++ "minecraft:calcite", ++ "minecraft:cave_air", ++ "minecraft:coarse_dirt", ++ "minecraft:crimson_nylium", ++ "minecraft:dirt", ++ "minecraft:end_stone", ++ "minecraft:grass_block", ++ "minecraft:gravel", ++ "minecraft:ice", ++ "minecraft:lava", ++ "minecraft:mycelium", ++ "minecraft:nether_wart_block", ++ "minecraft:netherrack", ++ "minecraft:orange_terracotta", ++ "minecraft:packed_ice", ++ "minecraft:podzol", ++ "minecraft:powder_snow", ++ "minecraft:red_sand", ++ "minecraft:red_sandstone", ++ "minecraft:sand", ++ "minecraft:sandstone", ++ "minecraft:snow_block", ++ "minecraft:soul_sand", ++ "minecraft:soul_soil", ++ "minecraft:stone", ++ "minecraft:terracotta", ++ "minecraft:warped_nylium", ++ "minecraft:warped_wart_block", ++ "minecraft:water", ++ "minecraft:white_terracotta" ++ )); ++ ++ private static int getObjectsPerValue(final long[] val) { ++ return (4096 + val.length - 1) / (val.length); // expression is invalid if it returns > 64 ++ } ++ ++ private static long[] resize(final long[] val, final int oldBitsPerObject, final int newBitsPerObject) { ++ final long oldMask = (1L << oldBitsPerObject) - 1; // works even if bitsPerObject == 64 ++ final long newMask = (1L << newBitsPerObject) - 1; ++ final int oldObjectsPerValue = 64 / oldBitsPerObject; ++ final int newObjectsPerValue = 64 / newBitsPerObject; ++ ++ if (newBitsPerObject == oldBitsPerObject) { ++ return val; ++ } ++ ++ final int items = 4096; ++ ++ final long[] ret = new long[(items + newObjectsPerValue - 1) / newObjectsPerValue]; ++ ++ final int expectedSize = ((items + oldObjectsPerValue - 1) / oldObjectsPerValue); ++ if (val.length != expectedSize) { ++ throw new IllegalStateException("Expected size: " + expectedSize + ", got: " + val.length); ++ } ++ ++ int shift = 0; ++ int idx = 0; ++ long newCurr = 0L; ++ ++ int currItem = 0; ++ for (int i = 0; i < val.length; ++i) { ++ final long oldCurr = val[i]; ++ ++ for (int objIdx = 0; currItem < items && objIdx + oldBitsPerObject <= 64; objIdx += oldBitsPerObject, ++currItem) { ++ final long value = (oldCurr >> objIdx) & oldMask; ++ ++ if ((value & newMask) != value) { ++ throw new IllegalStateException("Old data storage has values that cannot be moved into new palette (would erase data)!"); ++ } ++ ++ newCurr |= value << shift; ++ shift += newBitsPerObject; ++ ++ if (shift + newBitsPerObject > 64) { // will next write overflow? ++ // must move to next idx ++ ret[idx++] = newCurr; ++ shift = 0; ++ newCurr = 0L; ++ } ++ } ++ } ++ ++ // don't forget to write the last one ++ if (shift != 0) { ++ ret[idx] = newCurr; ++ } ++ ++ return ret; ++ } ++ ++ private static void fixLithiumChunks(final MapType data) { ++ // See https://github.com/CaffeineMC/lithium-fabric/issues/279 ++ final MapType level = data.getMap("Level"); ++ if (level == null) { ++ return; ++ } ++ ++ final int chunkX = level.getInt("xPos"); ++ final int chunkZ = level.getInt("zPos"); ++ ++ final ListType sections = level.getList("Sections", ObjectType.MAP); ++ if (sections == null) { ++ return; ++ } ++ ++ for (int i = 0, len = sections.size(); i < len; ++i) { ++ final MapType section = sections.getMap(i); ++ ++ final int sectionY = section.getInt("Y"); ++ ++ final ListType palette = section.getList("Palette", ObjectType.MAP); ++ final long[] blockStates = section.getLongs("BlockStates"); ++ ++ if (palette == null || blockStates == null) { ++ continue; ++ } ++ ++ final int expectedBits = Math.max(4, ceilLog2(palette.size())); ++ final int gotObjectsPerValue = getObjectsPerValue(blockStates); ++ final int gotBits = 64 / gotObjectsPerValue; ++ ++ if (expectedBits == gotBits) { ++ continue; ++ } ++ ++ try { ++ section.setLongs("BlockStates", resize(blockStates, gotBits, expectedBits)); ++ } catch (final Exception ex) { ++ LOGGER.error("Failed to rewrite mismatched palette and data storage for section y: " + sectionY ++ + " for chunk [" + chunkX + "," + chunkZ + "], palette entries: " + palette.size() + ", data storage size: " ++ + blockStates.length, ++ ex ++ ); ++ } ++ } ++ } ++ ++ public static void register() { ++ // See V2551 for the layout of world gen settings ++ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ // converters were added to older versions note whether the world has increased height already or not ++ final boolean noHeightFlag = !data.hasKey("has_increased_height_already"); ++ final boolean hasIncreasedHeight = data.getBoolean("has_increased_height_already", true); ++ data.remove("has_increased_height_already"); ++ ++ final MapType dimensions = data.getMap("dimensions"); ++ if (dimensions == null) { ++ // wat ++ return null; ++ } ++ ++ // only care about overworld ++ final MapType overworld = dimensions.getMap("minecraft:overworld"); ++ if (overworld == null) { ++ // wat ++ return null; ++ } ++ ++ final MapType generator = overworld.getMap("generator"); ++ if (generator == null) { ++ // wat ++ return null; ++ } ++ ++ final String type = generator.getString("type", ""); ++ switch (type) { ++ case "minecraft:noise": { ++ final MapType biomeSource = generator.getMap("biome_source"); ++ final String sourceType = biomeSource.getString("type"); ++ ++ boolean largeBiomes = false; ++ ++ if ("minecraft:vanilla_layered".equals(sourceType) || (noHeightFlag && "minecraft:multi_noise".equals(sourceType))) { ++ largeBiomes = biomeSource.getBoolean("large_biomes"); ++ ++ final MapType newBiomeSource = Types.NBT.createEmptyMap(); ++ generator.setMap("biome_source", newBiomeSource); ++ ++ newBiomeSource.setString("preset", "minecraft:overworld"); ++ newBiomeSource.setString("type", "minecraft:multi_noise"); ++ } ++ ++ if (largeBiomes) { ++ if ("minecraft:overworld".equals(generator.getString("settings"))) { ++ generator.setString("settings", "minecraft:large_biomes"); ++ } ++ } ++ ++ break; ++ } ++ case "minecraft:flat": { ++ if (!hasIncreasedHeight) { ++ final MapType settings = generator.getMap("settings"); ++ if (settings == null) { ++ break; ++ } ++ ++ updateLayers(settings.getList("layers", ObjectType.MAP)); ++ } ++ break; ++ } ++ default: ++ // do nothing ++ break; ++ } ++ ++ return null; ++ } ++ }); ++ ++ ++ // It looks like DFU will only support worlds in the old height format or the new one, any custom one isn't supported ++ // and by not supported I mean it will just treat it as the old format... maybe at least throw in that case? ++ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ // The below covers padPaletteEntries - this was written BEFORE that code was added to the datafixer - ++ // and this still works, so I'm keeping it. Don't fix what isn't broken. ++ fixLithiumChunks(data); // See https://github.com/CaffeineMC/lithium-fabric/issues/279 ++ ++ final MapType level = data.getMap("Level"); ++ ++ if (level == null) { ++ return null; ++ } ++ ++ final MapType context = data.getMap("__context"); // Passed through by ChunkStorage ++ final String dimension = context == null ? "" : context.getString("dimension", ""); ++ final String generator = context == null ? "" : context.getString("generator", ""); ++ final boolean isOverworld = "minecraft:overworld".equals(dimension); ++ final int minSection = isOverworld ? -4 : 0; ++ final MutableBoolean isAlreadyExtended = new MutableBoolean(); ++ ++ final MapType[] newBiomes = createBiomeSections(level, isOverworld, minSection, isAlreadyExtended); ++ final MapType wrappedEmptyBlockPalette = getEmptyBlockPalette(); ++ ++ ListType sections = level.getList("Sections", ObjectType.MAP); ++ if (sections == null) { ++ level.setList("Sections", sections = Types.NBT.createEmptyList()); ++ } ++ ++ // must update sections for two things: ++ // 1. the biomes are now stored per section, so we must insert the biomes palette into each section (and create them if they don't exist) ++ // 2. each section must now have block states (or at least DFU is ensuring they do, but current code does not require) ++ V2841.SimplePaletteReader bottomSection = null; ++ final Set allBlocks = new HashSet<>(); ++ final IntOpenHashSet existingSections = new IntOpenHashSet(); ++ ++ for (int i = 0, len = sections.size(); i < len; ++i) { ++ final MapType section = sections.getMap(i); ++ ++ final int y = section.getInt("Y"); ++ final int sectionIndex = y - minSection; ++ ++ existingSections.add(y); ++ ++ // add in relevant biome section ++ if (sectionIndex >= 0 && sectionIndex < newBiomes.length) { ++ // exclude out of bounds sections (i.e the light sections above and below the world) ++ section.setMap("biomes", newBiomes[sectionIndex]); ++ } ++ ++ // update palette ++ final ListType palette = section.getList("Palette", ObjectType.MAP); ++ final long[] blockStates = section.getLongs("BlockStates"); ++ ++ section.remove("Palette"); ++ section.remove("BlockStates"); ++ ++ if (palette != null) { ++ for (int j = 0, len2 = palette.size(); j < len2; ++j) { ++ allBlocks.add(V2841.getBlockId(palette.getMap(j))); ++ } ++ } ++ ++ final MapType palettedContainer; ++ if (palette != null && blockStates != null) { ++ // only if both exist, same as DFU, same as legacy chunk loading code ++ section.setMap("block_states", palettedContainer = wrapPaletteOptimised(palette, blockStates)); ++ } else { ++ section.setMap("block_states", palettedContainer = wrappedEmptyBlockPalette.copy()); // must write a palette now, copy so that later edits do not edit them all ++ } ++ ++ if (section.getInt("Y", Integer.MAX_VALUE) == 0) { ++ bottomSection = new V2841.SimplePaletteReader(palettedContainer.getList("palette", ObjectType.MAP), palettedContainer.getLongs("data")); ++ } ++ } ++ ++ // all existing sections updated, now we must create new sections just for the biomes migration ++ for (int sectionIndex = 0; sectionIndex < newBiomes.length; ++sectionIndex) { ++ final int sectionY = sectionIndex + minSection; ++ if (!existingSections.add(sectionY)) { ++ // exists already ++ continue; ++ } ++ ++ final MapType newSection = Types.NBT.createEmptyMap(); ++ sections.addMap(newSection); ++ ++ newSection.setByte("Y", (byte)sectionY); ++ // must write a palette now, copy so that later edits do not edit them all ++ newSection.setMap("block_states", wrappedEmptyBlockPalette.copy()); ++ ++ newSection.setGeneric("biomes", newBiomes[sectionIndex]); ++ } ++ ++ // update status so interpolation can take place ++ predictChunkStatusBeforeSurface(level, allBlocks); ++ ++ // done with sections, update the rest of the chunk ++ updateChunkData(level, isOverworld, isAlreadyExtended.getValue(), "minecraft:noise".equals(generator), bottomSection); ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ final MapType dimensions = data.getMap("dimensions"); ++ ++ if (dimensions == null) { ++ return null; ++ } ++ ++ for (final String dimension : dimensions.keys()) { ++ final MapType dimensionData = dimensions.getMap(dimension); ++ if (dimensionData == null) { ++ continue; ++ } ++ ++ final MapType generator = dimensionData.getMap("generator"); ++ if (generator == null) { ++ continue; ++ } ++ ++ final String type = generator.getString("type"); ++ if (type == null) { ++ continue; ++ } ++ ++ switch (type) { ++ case "minecraft:flat": { ++ final MapType settings = generator.getMap("settings"); ++ if (settings == null) { ++ continue; ++ } ++ ++ WalkerUtils.convert(MCTypeRegistry.BIOME, settings, "biome", fromVersion, toVersion); ++ ++ final ListType layers = settings.getList("layers", ObjectType.MAP); ++ if (layers != null) { ++ for (int i = 0, len = layers.size(); i < len; ++i) { ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, layers.getMap(i), "block", fromVersion, toVersion); ++ } ++ } ++ ++ break; ++ } ++ case "minecraft:noise": { ++ final MapType settings = generator.getMap("settings"); ++ if (settings != null) { ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, settings, "default_block", fromVersion, toVersion); ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, settings, "default_fluid", fromVersion, toVersion); ++ } ++ ++ final MapType biomeSource = generator.getMap("biome_source"); ++ if (biomeSource != null) { ++ final String biomeSourceType = biomeSource.getString("type", ""); ++ switch (biomeSourceType) { ++ case "minecraft:fixed": { ++ WalkerUtils.convert(MCTypeRegistry.BIOME, biomeSource, "biome", fromVersion, toVersion); ++ break; ++ } ++ ++ case "minecraft:multi_noise": { ++ WalkerUtils.convert(MCTypeRegistry.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST, biomeSource, "preset", fromVersion, toVersion); ++ ++ // Vanilla's schema is _still_ wrong. It should be DSL.fields("biomes", DSL.list(DSL.fields("biome"))) ++ // But it just contains the list part. That obviously can never be the case, because ++ // the root object is a compound, not a list. ++ ++ final ListType biomes = biomeSource.getList("biomes", ObjectType.MAP); ++ if (biomes != null) { ++ for (int i = 0, len = biomes.size(); i < len; ++i) { ++ WalkerUtils.convert(MCTypeRegistry.BIOME, biomes.getMap(i), "biome", fromVersion, toVersion); ++ } ++ } ++ ++ break; ++ } ++ ++ case "minecraft:checkerboard": { ++ WalkerUtils.convertList(MCTypeRegistry.BIOME, biomeSource, "biomes", fromVersion, toVersion); ++ break; ++ } ++ } ++ } ++ ++ break; ++ } ++ } ++ } ++ ++ return null; ++ }); ++ ++ MCTypeRegistry.CHUNK.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ final MapType level = data.getMap("Level"); ++ if (level == null) { ++ return null; ++ } ++ ++ WalkerUtils.convertList(MCTypeRegistry.ENTITY, level, "Entities", fromVersion, toVersion); ++ WalkerUtils.convertList(MCTypeRegistry.TILE_ENTITY, level, "TileEntities", fromVersion, toVersion); ++ ++ final ListType tileTicks = level.getList("TileTicks", ObjectType.MAP); ++ if (tileTicks != null) { ++ for (int i = 0, len = tileTicks.size(); i < len; ++i) { ++ final MapType tileTick = tileTicks.getMap(i); ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, tileTick, "i", fromVersion, toVersion); ++ } ++ } ++ ++ final ListType sections = level.getList("Sections", ObjectType.MAP); ++ if (sections != null) { ++ for (int i = 0, len = sections.size(); i < len; ++i) { ++ final MapType section = sections.getMap(i); ++ ++ WalkerUtils.convertList(MCTypeRegistry.BIOME, section.getMap("biomes"), "palette", fromVersion, toVersion); ++ WalkerUtils.convertList(MCTypeRegistry.BLOCK_STATE, section.getMap("block_states"), "palette", fromVersion, toVersion); ++ } ++ } ++ ++ WalkerUtils.convertValues(MCTypeRegistry.STRUCTURE_FEATURE, level.getMap("Structures"), "Starts", fromVersion, toVersion); ++ ++ return null; ++ }); ++ } ++ ++ private static void predictChunkStatusBeforeSurface(final MapType level, final Set chunkBlocks) { ++ final String status = level.getString("Status", "empty"); ++ if (STATUS_IS_OR_AFTER_SURFACE.contains(status)) { ++ return; ++ } ++ ++ chunkBlocks.remove("minecraft:air"); ++ final boolean chunkNotEmpty = !chunkBlocks.isEmpty(); ++ chunkBlocks.removeAll(BLOCKS_BEFORE_FEATURE_STATUS); ++ final boolean chunkFeatureStatus = !chunkBlocks.isEmpty(); ++ ++ final String update; ++ if (chunkFeatureStatus) { ++ update = "liquid_carvers"; ++ } else if (!"noise".equals(status) && !chunkNotEmpty) { ++ update = "biomes".equals(status) ? "structure_references" : status; ++ } else { ++ update = "noise"; ++ } ++ ++ level.setString("Status", update); ++ } ++ ++ private static MapType getEmptyBlockPalette() { ++ final MapType airBlockState = Types.NBT.createEmptyMap(); ++ airBlockState.setString("Name", "minecraft:air"); ++ ++ final ListType emptyBlockPalette = Types.NBT.createEmptyList(); ++ emptyBlockPalette.addMap(airBlockState); ++ ++ return V2832.wrapPalette(emptyBlockPalette); ++ } ++ ++ private static void shiftUpgradeData(final MapType upgradeData, final int shift) { ++ if (upgradeData == null) { ++ return; ++ } ++ ++ final MapType indices = upgradeData.getMap("Indices"); ++ if (indices == null) { ++ return; ++ } ++ ++ RenameHelper.renameKeys(indices, (final String input) -> { ++ return Integer.toString(Integer.parseInt(input) + shift); ++ }); ++ } ++ ++ private static void updateChunkData(final MapType level, final boolean wantExtendedHeight, final boolean isAlreadyExtended, ++ final boolean onNoiseGenerator, final V2841.SimplePaletteReader bottomSection) { ++ level.remove("Biomes"); ++ if (!wantExtendedHeight) { ++ padCarvingMasks(level, 16, 0); ++ return; ++ } ++ ++ if (isAlreadyExtended) { ++ padCarvingMasks(level, 24, 0); ++ return; ++ } ++ ++ offsetHeightmaps(level); ++ // Difference from DFU: Still convert the Lights data. Just because it's being removed in a later version doesn't mean ++ // that it should be removed here. ++ // Generally, converters act only on the current version to bring it to the next. This principle allows the converter ++ // for the next version to assume that it acts on its current version, not some in-between of the current version ++ // and some future version that did not exist at the time it was written. This allows converters to be written and tested ++ // only with knowledge of the current version and the next version. ++ addEmptyListPadding(level, "Lights"); ++ addEmptyListPadding(level, "LiquidsToBeTicked"); ++ addEmptyListPadding(level, "PostProcessing"); ++ addEmptyListPadding(level, "ToBeTicked"); ++ shiftUpgradeData(level.getMap("UpgradeData"), 4); // https://bugs.mojang.com/browse/MC-238076 - fixed now, Mojang fix is identical. No change required. ++ padCarvingMasks(level, 24, 4); ++ ++ if (!onNoiseGenerator) { ++ return; ++ } ++ ++ final String status = level.getString("Status"); ++ if (status == null || "empty".equals(status)) { ++ return; ++ } ++ ++ final MapType blendingData = Types.NBT.createEmptyMap(); ++ level.setMap("blending_data", blendingData); ++ ++ blendingData.setBoolean("old_noise", STATUS_IS_OR_AFTER_NOISE.contains(status)); ++ ++ if (bottomSection == null) { ++ return; ++ } ++ ++ final BitSet missingBedrock = new BitSet(256); ++ boolean hasBedrock = status.equals("noise"); ++ ++ for (int z = 0; z <= 15; ++z) { ++ for (int x = 0; x <= 15; ++x) { ++ final MapType state = bottomSection.getState(x, 0, z); ++ final String blockId = V2841.getBlockId(state); ++ final boolean isBedrock = state != null && "minecraft:bedrock".equals(blockId); ++ final boolean isAir = state != null && "minecraft:air".equals(blockId); ++ if (isAir) { ++ missingBedrock.set((z << 4) | x); ++ } ++ ++ hasBedrock |= isBedrock; ++ } ++ } ++ ++ if (hasBedrock && missingBedrock.cardinality() != missingBedrock.size()) { ++ final String targetStatus = "full".equals(status) ? "heightmaps" : status; ++ ++ final MapType belowZeroRetrogen = Types.NBT.createEmptyMap(); ++ level.setMap("below_zero_retrogen", belowZeroRetrogen); ++ ++ belowZeroRetrogen.setString("target_status", targetStatus); ++ belowZeroRetrogen.setLongs("missing_bedrock", missingBedrock.toLongArray()); ++ ++ level.setString("Status", "empty"); ++ } ++ ++ level.setBoolean("isLightOn", false); ++ } ++ ++ private static void padCarvingMasks(final MapType level, final int newSize, final int offset) { ++ final MapType carvingMasks = level.getMap("CarvingMasks"); ++ if (carvingMasks == null) { ++ // if empty, DFU still writes ++ level.setMap("CarvingMasks", Types.NBT.createEmptyMap()); ++ return; ++ } ++ ++ for (final String key : carvingMasks.keys()) { ++ final long[] old = BitSet.valueOf(carvingMasks.getBytes(key)).toLongArray(); ++ final long[] newVal = new long[64 * newSize]; ++ ++ System.arraycopy(old, 0, newVal, 64 * offset, old.length); ++ ++ carvingMasks.setLongs(key, newVal); // no CME: key exists already ++ } ++ } ++ ++ private static void addEmptyListPadding(final MapType level, final String path) { ++ ListType list = level.getListUnchecked(path); ++ if (list != null && list.size() == 24) { ++ return; ++ } ++ ++ if (list == null) { ++ // difference from DFU: Don't create the damn thing! ++ return; ++ } ++ ++ ++ // offset the section array to the new format ++ for (int i = 0; i < 4; ++i) { ++ // always create new copies, so that modifying one doesn't modify ALL of them! ++ list.addList(0, Types.NBT.createEmptyList()); // add below ++ list.addList(Types.NBT.createEmptyList()); // add above ++ } ++ } ++ ++ private static void offsetHeightmaps(final MapType level) { ++ final MapType heightmaps = level.getMap("Heightmaps"); ++ if (heightmaps == null) { ++ return; ++ } ++ ++ for (final String key : HEIGHTMAP_TYPES) { ++ offsetHeightmap(heightmaps.getLongs(key)); ++ } ++ } ++ ++ private static void offsetHeightmap(final long[] heightmap) { ++ if (heightmap == null) { ++ return; ++ } ++ ++ // heightmaps are configured to have 9 bits per value, with 256 total values ++ // heightmaps are also relative to the lowest position ++ for (int idx = 0, len = heightmap.length; idx < len; ++idx) { ++ long curr = heightmap[idx]; ++ long next = 0L; ++ ++ for (int objIdx = 0; objIdx + 9 <= 64; objIdx += 9) { ++ final long value = (curr >> objIdx) & 511L; ++ if (value != 0L) { ++ final long offset = Math.min(511L, value + 64L); ++ ++ next |= (offset << objIdx); ++ } ++ } ++ ++ heightmap[idx] = next; ++ } ++ } ++ ++ private static MapType[] createBiomeSections(final MapType level, final boolean wantExtendedHeight, ++ final int minSection, final MutableBoolean isAlreadyExtended) { ++ final MapType[] ret = new MapType[wantExtendedHeight ? 24 : 16]; ++ ++ final int[] biomes = level.getInts("Biomes"); ++ ++ if (biomes != null && biomes.length == 1536) { // magic value for 24 sections of biomes (24 * 4^3) ++ isAlreadyExtended.setValue(true); ++ for (int sectionIndex = 0; sectionIndex < 24; ++sectionIndex) { ++ ret[sectionIndex] = createBiomeSection(biomes, sectionIndex * 64, -1); // -1 is all 1s ++ } ++ } else if (biomes != null && biomes.length == 1024) { // magic value for 24 sections of biomes (16 * 4^3) ++ for (int sectionY = 0; sectionY < 16; ++sectionY) { ++ ret[sectionY - minSection] = createBiomeSection(biomes, sectionY * 64, -1); // -1 is all 1s ++ } ++ ++ if (wantExtendedHeight) { ++ // must set the new sections at top and bottom ++ final MapType bottomCopy = createBiomeSection(biomes, 0, 15); // just want the biomes at y = 0 ++ final MapType topCopy = createBiomeSection(biomes, 1008, 15); // just want the biomes at y = 252 ++ ++ for (int sectionIndex = 0; sectionIndex < 4; ++sectionIndex) { ++ ret[sectionIndex] = bottomCopy.copy(); // copy palette so that later possible modifications don't trash all sections ++ } ++ ++ for (int sectionIndex = 20; sectionIndex < 24; ++sectionIndex) { ++ ret[sectionIndex] = topCopy.copy(); // copy palette so that later possible modifications don't trash all sections ++ } ++ } ++ } else { ++ final ListType palette = Types.NBT.createEmptyList(); ++ palette.addString("minecraft:plains"); ++ ++ for (int i = 0; i < ret.length; ++i) { ++ ret[i] = wrapPalette(palette.copy()); // copy palette so that later possible modifications don't trash all sections ++ } ++ } ++ ++ return ret; ++ } ++ ++ private static MapType createBiomeSection(final int[] biomes, final int offset, final int mask) { ++ final Int2IntLinkedOpenHashMap paletteId = new Int2IntLinkedOpenHashMap(); ++ ++ for (int idx = 0; idx < 64; ++idx) { ++ final int biome = biomes[offset + (idx & mask)]; ++ paletteId.putIfAbsent(biome, paletteId.size()); ++ } ++ ++ final ListType paletteString = Types.NBT.createEmptyList(); ++ for (final IntIterator iterator = paletteId.keySet().iterator(); iterator.hasNext();) { ++ final int biomeId = iterator.nextInt(); ++ final String biome = biomeId >= 0 && biomeId < BIOMES_BY_ID.length ? BIOMES_BY_ID[biomeId] : null; ++ paletteString.addString(biome == null ? "minecraft:plains" : biome); ++ } ++ ++ final int bitsPerObject = ceilLog2(paletteString.size()); ++ if (bitsPerObject == 0) { ++ return wrapPalette(paletteString); ++ } ++ ++ // manually create packed integer data ++ final int objectsPerValue = 64 / bitsPerObject; ++ final long[] packed = new long[(64 + objectsPerValue - 1) / objectsPerValue]; ++ ++ int shift = 0; ++ int idx = 0; ++ long curr = 0; ++ ++ for (int biome_idx = 0; biome_idx < 64; ++biome_idx) { ++ final int biome = biomes[offset + (biome_idx & mask)]; ++ ++ curr |= ((long)paletteId.get(biome)) << shift; ++ ++ shift += bitsPerObject; ++ ++ if (shift + bitsPerObject > 64) { // will next write overflow? ++ // must move to next idx ++ packed[idx++] = curr; ++ shift = 0; ++ curr = 0L; ++ } ++ } ++ ++ // don't forget to write the last one ++ if (shift != 0) { ++ packed[idx] = curr; ++ } ++ ++ return wrapPalette(paletteString, packed); ++ } ++ ++ private static MapType wrapPalette(final ListType palette) { ++ return wrapPalette(palette, null); ++ } ++ ++ private static MapType wrapPalette(final ListType palette, final long[] blockStates) { ++ final MapType ret = Types.NBT.createEmptyMap(); ++ ret.setList("palette", palette); ++ if (blockStates != null) { ++ ret.setLongs("data", blockStates); ++ } ++ ++ return ret; ++ } ++ ++ private static MapType wrapPaletteOptimised(final ListType palette, final long[] blockStates) { ++ if (palette.size() == 1) { ++ return wrapPalette(palette); ++ } ++ ++ return wrapPalette(palette, blockStates); ++ } ++ ++ public static int ceilLog2(final int value) { ++ return value == 0 ? 0 : Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros ++ } ++ ++ private static void updateLayers(final ListType layers) { ++ if (layers == null) { ++ return; ++ } ++ ++ layers.addMap(0, createEmptyLayer()); // add at the bottom ++ } ++ ++ private static MapType createEmptyLayer() { ++ final MapType ret = Types.NBT.createEmptyMap(); ++ ret.setInt("height", 64); ++ ret.setString("block", "minecraft:air"); ++ ++ return ret; ++ } ++ ++ private V2832() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2833.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2833.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4c58b3b53d8526cbde6cf1e8c90cabdaceaf2a03 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2833.java +@@ -0,0 +1,31 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V2833 { ++ ++ private static final int VERSION = MCVersions.V1_17_1 + 103; ++ ++ public static void register() { ++ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType dimensions = data.getMap("dimensions"); ++ ++ for (final String dimensionKey : dimensions.keys()) { ++ final MapType dimension = dimensions.getMap(dimensionKey); ++ if (!dimension.hasKey("type")) { ++ throw new IllegalStateException("Unable load old custom worlds."); ++ } ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V2833() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2838.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2838.java +new file mode 100644 +index 0000000000000000000000000000000000000000..356963228d884a0a74e6d7c9922b4ced627bbfb0 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2838.java +@@ -0,0 +1,62 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V2838 { ++ ++ private static final int VERSION = MCVersions.V21W40A; ++ ++ public static final Map BIOME_UPDATE = new HashMap<>( ++ ImmutableMap.builder() ++ .put("minecraft:badlands_plateau", "minecraft:badlands") ++ .put("minecraft:bamboo_jungle_hills", "minecraft:bamboo_jungle") ++ .put("minecraft:birch_forest_hills", "minecraft:birch_forest") ++ .put("minecraft:dark_forest_hills", "minecraft:dark_forest") ++ .put("minecraft:desert_hills", "minecraft:desert") ++ .put("minecraft:desert_lakes", "minecraft:desert") ++ .put("minecraft:giant_spruce_taiga_hills", "minecraft:old_growth_spruce_taiga") ++ .put("minecraft:giant_spruce_taiga", "minecraft:old_growth_spruce_taiga") ++ .put("minecraft:giant_tree_taiga_hills", "minecraft:old_growth_pine_taiga") ++ .put("minecraft:giant_tree_taiga", "minecraft:old_growth_pine_taiga") ++ .put("minecraft:gravelly_mountains", "minecraft:windswept_gravelly_hills") ++ .put("minecraft:jungle_edge", "minecraft:sparse_jungle") ++ .put("minecraft:jungle_hills", "minecraft:jungle") ++ .put("minecraft:modified_badlands_plateau", "minecraft:badlands") ++ .put("minecraft:modified_gravelly_mountains", "minecraft:windswept_gravelly_hills") ++ .put("minecraft:modified_jungle_edge", "minecraft:sparse_jungle") ++ .put("minecraft:modified_jungle", "minecraft:jungle") ++ .put("minecraft:modified_wooded_badlands_plateau", "minecraft:wooded_badlands") ++ .put("minecraft:mountain_edge", "minecraft:windswept_hills") ++ .put("minecraft:mountains", "minecraft:windswept_hills") ++ .put("minecraft:mushroom_field_shore", "minecraft:mushroom_fields") ++ .put("minecraft:shattered_savanna", "minecraft:windswept_savanna") ++ .put("minecraft:shattered_savanna_plateau", "minecraft:windswept_savanna") ++ .put("minecraft:snowy_mountains", "minecraft:snowy_plains") ++ .put("minecraft:snowy_taiga_hills", "minecraft:snowy_taiga") ++ .put("minecraft:snowy_taiga_mountains", "minecraft:snowy_taiga") ++ .put("minecraft:snowy_tundra", "minecraft:snowy_plains") ++ .put("minecraft:stone_shore", "minecraft:stony_shore") ++ .put("minecraft:swamp_hills", "minecraft:swamp") ++ .put("minecraft:taiga_hills", "minecraft:taiga") ++ .put("minecraft:taiga_mountains", "minecraft:taiga") ++ .put("minecraft:tall_birch_forest", "minecraft:old_growth_birch_forest") ++ .put("minecraft:tall_birch_hills", "minecraft:old_growth_birch_forest") ++ .put("minecraft:wooded_badlands_plateau", "minecraft:wooded_badlands") ++ .put("minecraft:wooded_hills", "minecraft:forest") ++ .put("minecraft:wooded_mountains", "minecraft:windswept_forest") ++ .put("minecraft:lofty_peaks", "minecraft:jagged_peaks") ++ .put("minecraft:snowcapped_peaks", "minecraft:frozen_peaks") ++ .build() ++ ); ++ ++ public static void register() { ++ ConverterAbstractStringValueTypeRename.register(VERSION, MCTypeRegistry.BIOME, BIOME_UPDATE::get); ++ } ++ ++ private V2838() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2841.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2841.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bd8117a101d308e59251f927feb0692a6b22f547 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2841.java +@@ -0,0 +1,210 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.Types; ++import ca.spottedleaf.dataconverter.util.IntegerUtil; ++import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; ++import java.util.Arrays; ++import java.util.HashSet; ++import java.util.Set; ++ ++public final class V2841 { ++ ++ private static final int VERSION = MCVersions.V21W42A + 1; ++ ++ private static final Set ALWAYS_WATERLOGGED = new HashSet<>( ++ Arrays.asList( ++ "minecraft:bubble_column", ++ "minecraft:kelp", ++ "minecraft:kelp_plant", ++ "minecraft:seagrass", ++ "minecraft:tall_seagrass" ++ ) ++ ); ++ ++ public static void register() { ++ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType root, final long sourceVersion, final long toVersion) { ++ final MapType level = root.getMap("Level"); ++ if (level == null) { ++ return null; ++ } ++ ++ { ++ // Why it's renamed here and not the next data version is beyond me. ++ final MapType liquidTicks = level.getMap("LiquidTicks"); ++ if (liquidTicks != null) { ++ level.remove("LiquidTicks"); ++ level.setMap("fluid_ticks", liquidTicks); ++ } ++ } ++ ++ final Int2ObjectOpenHashMap sectionBlocks = new Int2ObjectOpenHashMap<>(); ++ final ListType sections = level.getList("Sections", ObjectType.MAP); ++ int minSection = 0; // TODO wtf is this ++ if (sections != null) { ++ for (int i = 0, len = sections.size(); i < len; ++i) { ++ final MapType section = sections.getMap(i); ++ ++ final int sectionY = section.getInt("Y"); ++ if (sectionY < minSection && section.hasKey("biomes")) { ++ minSection = sectionY; ++ } ++ ++ final MapType blockStates = section.getMap("block_states"); ++ if (blockStates == null) { ++ continue; ++ } ++ ++ sectionBlocks.put(sectionY, new SimplePaletteReader(section.getList("palette", ObjectType.MAP), section.getLongs("data"))); ++ } ++ } ++ ++ level.setByte("yPos", (byte)minSection); // TODO ??????????????????????????????????????? ++ ++ if (level.hasKey("fluid_ticks") || level.hasKey("TileTicks")) { ++ return null; ++ } ++ ++ final int sectionX = level.getInt("xPos"); ++ final int sectionZ = level.getInt("zPos"); ++ ++ final ListType fluidTicks = level.getList("LiquidsToBeTicked", ObjectType.LIST); ++ final ListType blockTicks = level.getList("ToBeTicked", ObjectType.LIST); ++ level.remove("LiquidsToBeTicked"); ++ level.remove("ToBeTicked"); ++ ++ level.setList("fluid_ticks", migrateTickList(fluidTicks, false, sectionBlocks, sectionX, minSection, sectionZ)); ++ level.setList("TileTicks", migrateTickList(blockTicks, true, sectionBlocks, sectionX, minSection, sectionZ)); ++ ++ return null; ++ } ++ }); ++ } ++ ++ public static ListType migrateTickList(final ListType ticks, final boolean blockTicks, final Int2ObjectOpenHashMap sectionBlocks, ++ final int sectionX, final int minSection, final int sectionZ) { ++ final ListType ret = Types.NBT.createEmptyList(); ++ ++ if (ticks == null) { ++ return ret; ++ } ++ ++ for (int sectionIndex = 0, totalSections = ticks.size(); sectionIndex < totalSections; ++sectionIndex) { ++ final int sectionY = sectionIndex + minSection; ++ final ListType sectionTicks = ticks.getList(sectionIndex); ++ final SimplePaletteReader palette = sectionBlocks.get(sectionY); ++ ++ for (int i = 0, len = sectionTicks.size(); i < len; ++i) { ++ final int localIndex = sectionTicks.getShort(i) & 0xFFFF; ++ final MapType blockState = palette == null ? null : palette.getState(localIndex); ++ final String subjectId = blockTicks ? getBlockId(blockState) : getLiquidId(blockState); ++ ++ ret.addMap(createNewTick(subjectId, localIndex, sectionX, sectionY, sectionZ)); ++ } ++ } ++ ++ return ret; ++ } ++ ++ public static MapType createNewTick(final String subjectId, final int localIndex, final int sectionX, final int sectionY, final int sectionZ) { ++ final int newX = (localIndex & 15) + (sectionX << 4); ++ final int newZ = ((localIndex >> 4) & 15) + (sectionZ << 4); ++ final int newY = ((localIndex >> 8) & 15) + (sectionY << 4); ++ ++ final MapType ret = Types.NBT.createEmptyMap(); ++ ++ ret.setString("i", subjectId); ++ ret.setInt("x", newX); ++ ret.setInt("y", newY); ++ ret.setInt("z", newZ); ++ ret.setInt("t", 0); ++ ret.setInt("p", 0); ++ ++ return ret; ++ } ++ ++ public static String getBlockId(final MapType blockState) { ++ return blockState == null ? "minecraft:air" : blockState.getString("Name", "minecraft:air"); ++ } ++ ++ private static String getLiquidId(final MapType blockState) { ++ if (blockState == null) { ++ return "minecraft:empty"; ++ } ++ ++ final String name = blockState.getString("Name"); ++ if (ALWAYS_WATERLOGGED.contains(name)) { ++ return "minecraft:water"; ++ } ++ ++ final MapType properties = blockState.getMap("Properties"); ++ // Correctly read block state properties as strings - https://github.com/PaperMC/DataConverter/issues/6 ++ if ("minecraft:water".equals(name)) { ++ return properties != null && "0".equals(properties.getString("level")) ? "minecraft:water" : "minecraft:flowing_water"; ++ } else if ("minecraft:lava".equals(name)) { ++ return properties != null && "0".equals(properties.getString("level")) ? "minecraft:lava" : "minecraft:flowing_lava"; ++ } ++ ++ return (properties != null && "true".equals(properties.getString("waterlogged"))) ? "minecraft:water" : "minecraft:empty"; ++ } ++ ++ private V2841() {} ++ ++ public static final class SimplePaletteReader { ++ ++ public final ListType palette; ++ public final long[] data; ++ private final int bitsPerValue; ++ private final long mask; ++ private final int valuesPerLong; ++ ++ public SimplePaletteReader(final ListType palette, final long[] data) { ++ this.palette = palette == null ? null : (palette.size() == 0 ? null : palette); ++ this.data = data; ++ this.bitsPerValue = Math.max(4, IntegerUtil.ceilLog2(this.palette == null ? 0 : this.palette.size())); ++ this.mask = (1L << this.bitsPerValue) - 1L; ++ this.valuesPerLong = (int)(64L / this.bitsPerValue); ++ } ++ ++ public MapType getState(final int x, final int y, final int z) { ++ final int index = x | (z << 4) | (y << 8); ++ return this.getState(index); ++ } ++ ++ public MapType getState(final int index) { ++ final ListType palette = this.palette; ++ if (palette == null) { ++ return null; ++ } ++ ++ final int paletteSize = palette.size(); ++ if (paletteSize == 1) { ++ return palette.getMap(0); ++ } ++ ++ // x86 div computes mod. no loss here using mod ++ // if needed, can compute magic mul and shift for div values using IntegerUtil ++ final int dataIndex = index / this.valuesPerLong; ++ final int localIndex = (index % this.valuesPerLong) * this.bitsPerValue; ++ final long[] data = this.data; ++ if (dataIndex < 0 || dataIndex >= data.length) { ++ return null; ++ } ++ ++ final long value = data[dataIndex]; ++ final int paletteIndex = (int)((value >>> localIndex) & this.mask); ++ if (paletteIndex < 0 || paletteIndex >= paletteSize) { ++ return null; ++ } ++ ++ return palette.getMap(paletteIndex); ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2842.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2842.java +new file mode 100644 +index 0000000000000000000000000000000000000000..03b3d8e2b97a346a45e6c57cb07474baa3bb6096 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2842.java +@@ -0,0 +1,78 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++ ++public final class V2842 { ++ ++ private static final int VERSION = MCVersions.V21W42A + 2; ++ ++ public static void register() { ++ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType root, final long sourceVersion, final long toVersion) { ++ final MapType level = root.getMap("Level"); ++ root.remove("Level"); ++ ++ if (!root.isEmpty()) { ++ for (final String key : root.keys()) { ++ if (level.hasKey(key)) { ++ // Don't clobber Level's data ++ continue; ++ } ++ level.setGeneric(key, root.getGeneric(key)); ++ } ++ } ++ ++ // Rename top level first ++ RenameHelper.renameSingle(level, "TileEntities", "block_entities"); ++ RenameHelper.renameSingle(level, "TileTicks", "block_ticks"); ++ RenameHelper.renameSingle(level, "Entities", "entities"); ++ RenameHelper.renameSingle(level, "Sections", "sections"); ++ RenameHelper.renameSingle(level, "Structures", "structures"); ++ ++ // 2nd level ++ final MapType structures = level.getMap("structures"); ++ if (structures != null) { ++ RenameHelper.renameSingle(structures, "Starts", "starts"); ++ } ++ ++ return level; // Level is now root tag. ++ } ++ }); ++ ++ MCTypeRegistry.CHUNK.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convertList(MCTypeRegistry.ENTITY, data, "entities", fromVersion, toVersion); ++ WalkerUtils.convertList(MCTypeRegistry.TILE_ENTITY, data, "block_entities", fromVersion, toVersion); ++ ++ final ListType blockTicks = data.getList("block_ticks", ObjectType.MAP); ++ if (blockTicks != null) { ++ for (int i = 0, len = blockTicks.size(); i < len; ++i) { ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, blockTicks.getMap(i), "i", fromVersion, toVersion); ++ } ++ } ++ ++ final ListType sections = data.getList("sections", ObjectType.MAP); ++ if (sections != null) { ++ for (int i = 0, len = sections.size(); i < len; ++i) { ++ final MapType section = sections.getMap(i); ++ ++ WalkerUtils.convertList(MCTypeRegistry.BIOME, section.getMap("biomes"), "palette", fromVersion, toVersion); ++ WalkerUtils.convertList(MCTypeRegistry.BLOCK_STATE, section.getMap("block_states"), "palette", fromVersion, toVersion); ++ } ++ } ++ ++ WalkerUtils.convertValues(MCTypeRegistry.STRUCTURE_FEATURE, data.getMap("structures"), "starts", fromVersion, toVersion); ++ ++ return null; ++ }); ++ } ++ ++ private V2842() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2843.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2843.java +new file mode 100644 +index 0000000000000000000000000000000000000000..28a7596b62f8a918f342e2d06eda050a7f2bad0b +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2843.java +@@ -0,0 +1,111 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.Types; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V2843 { ++ ++ private static final int VERSION = MCVersions.V21W42A + 3; ++ ++ public static void register() { ++ ConverterAbstractStringValueTypeRename.register(VERSION, MCTypeRegistry.BIOME, ++ new HashMap<>( ++ Map.of("minecraft:deep_warm_ocean", "minecraft:warm_ocean") ++ )::get ++ ); ++ ++ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ private static void moveOutOfBoundTicks(final ListType ticks, final MapType chunkRoot, final int chunkX, final int chunkZ, final String intoKey) { ++ if (ticks == null) { ++ return; ++ } ++ ++ ListType outOfBounds = null; ++ for (int i = 0, len = ticks.size(); i < len; ++i) { ++ final MapType tick = ticks.getMap(i); ++ final int x = tick.getInt("x"); ++ final int z = tick.getInt("z"); ++ // anything > 1 is lost, anything less than 1 stays. ++ if (Math.max(Math.abs(chunkX - (x >> 4)), Math.abs(chunkZ - (z >> 4))) == 1) { ++ // DFU doesn't remove, so neither do we. ++ if (outOfBounds == null) { ++ outOfBounds = Types.NBT.createEmptyList(); ++ } ++ outOfBounds.addMap(tick.copy()); ++ } ++ } ++ ++ if (outOfBounds != null) { ++ MapType upgradeData = chunkRoot.getMap("UpgradeData"); ++ if (upgradeData == null) { ++ chunkRoot.setMap("UpgradeData", upgradeData = Types.NBT.createEmptyMap()); ++ } ++ upgradeData.setList(intoKey, outOfBounds); ++ } ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ // After renames, so use new names ++ final int x = data.getInt("xPos"); ++ final int z = data.getInt("zPos"); ++ ++ moveOutOfBoundTicks(data.getList("block_ticks", ObjectType.MAP), data, x, z, "neighbor_block_ticks"); ++ moveOutOfBoundTicks(data.getList("fluid_ticks", ObjectType.MAP), data, x, z, "neighbor_fluid_ticks"); ++ ++ return null; ++ } ++ }); ++ ++ // DFU is missing schema for UpgradeData block names ++ MCTypeRegistry.CHUNK.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convertList(MCTypeRegistry.ENTITY, data, "entities", fromVersion, toVersion); ++ WalkerUtils.convertList(MCTypeRegistry.TILE_ENTITY, data, "block_entities", fromVersion, toVersion); ++ ++ final ListType blockTicks = data.getList("block_ticks", ObjectType.MAP); ++ if (blockTicks != null) { ++ for (int i = 0, len = blockTicks.size(); i < len; ++i) { ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, blockTicks.getMap(i), "i", fromVersion, toVersion); ++ } ++ } ++ ++ final MapType upgradeData = data.getMap("UpgradeData"); ++ if (upgradeData != null) { ++ // Even though UpgradeData will retrieve the block from the World when the type no longer exists, ++ // the type from the world may not match what was actually queued. So, even though it may look like we ++ // can skip the walker here, we actually don't if we want to be thorough. ++ final ListType neighbourBlockTicks = upgradeData.getList("neighbor_block_ticks", ObjectType.MAP); ++ if (neighbourBlockTicks != null) { ++ for (int i = 0, len = neighbourBlockTicks.size(); i < len; ++i) { ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, neighbourBlockTicks.getMap(i), "i", fromVersion, toVersion); ++ } ++ } ++ } ++ ++ final ListType sections = data.getList("sections", ObjectType.MAP); ++ if (sections != null) { ++ for (int i = 0, len = sections.size(); i < len; ++i) { ++ final MapType section = sections.getMap(i); ++ ++ WalkerUtils.convertList(MCTypeRegistry.BIOME, section.getMap("biomes"), "palette", fromVersion, toVersion); ++ WalkerUtils.convertList(MCTypeRegistry.BLOCK_STATE, section.getMap("block_states"), "palette", fromVersion, toVersion); ++ } ++ } ++ ++ WalkerUtils.convertValues(MCTypeRegistry.STRUCTURE_FEATURE, data.getMap("structures"), "starts", fromVersion, toVersion); ++ ++ return null; ++ }); ++ } ++ ++ private V2843() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2846.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2846.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e32224267d53d82ba15942141a5cb7a19eb380f2 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2846.java +@@ -0,0 +1,23 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.advancements.ConverterAbstractAdvancementsRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++ ++public final class V2846 { ++ ++ private static final int VERSION = MCVersions.V21W44A + 1; ++ ++ public static void register() { ++ ConverterAbstractAdvancementsRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:husbandry/play_jukebox_in_meadows", "minecraft:adventure/play_jukebox_in_meadows", ++ "minecraft:adventure/caves_and_cliff", "minecraft:adventure/fall_from_world_height", ++ "minecraft:adventure/ride_strider_in_overworld_lava", "minecraft:nether/ride_strider_in_overworld_lava" ++ ) ++ )::get); ++ } ++ ++ private V2846() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2852.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2852.java +new file mode 100644 +index 0000000000000000000000000000000000000000..224ee1d9a3ba68e5a617c2c0846be47feef0bed1 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2852.java +@@ -0,0 +1,31 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V2852 { ++ ++ private static final int VERSION = MCVersions.V1_18_PRE5 + 1; ++ ++ public static void register() { ++ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType dimensions = data.getMap("dimensions"); ++ ++ for (final String dimensionKey : dimensions.keys()) { ++ final MapType dimension = dimensions.getMap(dimensionKey); ++ if (!dimension.hasKey("type")) { ++ throw new IllegalStateException("Unable load old custom worlds."); ++ } ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V2852() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2967.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2967.java +new file mode 100644 +index 0000000000000000000000000000000000000000..eddfdecffcaf5e4cd7e5c3a79864816ffbaacae1 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2967.java +@@ -0,0 +1,58 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V2967 { ++ ++ private static final int VERSION = MCVersions.V22W05A; ++ ++ public static void register() { ++ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType dimensions = data.getMap("dimensions"); ++ ++ if (dimensions == null) { ++ return null; ++ } ++ ++ for (final String dimension : dimensions.keys()) { ++ final MapType dimensionData = dimensions.getMap(dimension); ++ if (dimensionData == null) { ++ continue; ++ } ++ ++ final MapType generator = dimensionData.getMap("generator"); ++ if (generator == null) { ++ continue; ++ } ++ ++ final MapType settings = generator.getMap("settings"); ++ if (settings == null) { ++ continue; ++ } ++ ++ final MapType structures = settings.getMap("structures"); ++ if (structures == null) { ++ continue; ++ } ++ ++ for (final String structureKey : structures.keys()) { ++ structures.getMap(structureKey).setString("type", "minecraft:random_spread"); ++ } ++ ++ final MapType stronghold = structures.getMap("stronghold"); ++ stronghold.setString("type", "minecraft:concentric_rings"); ++ structures.setMap("minecraft:stronghold", stronghold.copy()); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V2967() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2970.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2970.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ce083fca547170bb1e1014e868beaf535e940fc3 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2970.java +@@ -0,0 +1,209 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import com.google.common.collect.ImmutableMap; ++import com.mojang.logging.LogUtils; ++import it.unimi.dsi.fastutil.objects.Object2IntMap; ++import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; ++import org.slf4j.Logger; ++import java.util.HashMap; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Locale; ++import java.util.Map; ++ ++public final class V2970 { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ private static final int VERSION = MCVersions.V22W07A + 1; ++ private static final Map CONVERSION_MAP = new HashMap<>( ++ ImmutableMap.builder() ++ .put("mineshaft", BiomeRemap.create(Map.of(List.of("minecraft:badlands", "minecraft:eroded_badlands", "minecraft:wooded_badlands"), "minecraft:mineshaft_mesa"), "minecraft:mineshaft")) ++ .put("shipwreck", BiomeRemap.create(Map.of(List.of("minecraft:beach", "minecraft:snowy_beach"), "minecraft:shipwreck_beached"), "minecraft:shipwreck")) ++ .put("ocean_ruin", BiomeRemap.create(Map.of(List.of("minecraft:warm_ocean", "minecraft:lukewarm_ocean", "minecraft:deep_lukewarm_ocean"), "minecraft:ocean_ruin_warm"), "minecraft:ocean_ruin_cold")) ++ .put("village", BiomeRemap.create(Map.of(List.of("minecraft:desert"), "minecraft:village_desert", List.of("minecraft:savanna"), "minecraft:village_savanna", List.of("minecraft:snowy_plains"), "minecraft:village_snowy", List.of("minecraft:taiga"), "minecraft:village_taiga"), "minecraft:village_plains")) ++ .put("ruined_portal", BiomeRemap.create(Map.of(List.of("minecraft:desert"), "minecraft:ruined_portal_desert", List.of("minecraft:badlands", "minecraft:eroded_badlands", "minecraft:wooded_badlands", "minecraft:windswept_hills", "minecraft:windswept_forest", "minecraft:windswept_gravelly_hills", "minecraft:savanna_plateau", "minecraft:windswept_savanna", "minecraft:stony_shore", "minecraft:meadow", "minecraft:frozen_peaks", "minecraft:jagged_peaks", "minecraft:stony_peaks", "minecraft:snowy_slopes"), "minecraft:ruined_portal_mountain", List.of("minecraft:bamboo_jungle", "minecraft:jungle", "minecraft:sparse_jungle"), "minecraft:ruined_portal_jungle", List.of("minecraft:deep_frozen_ocean", "minecraft:deep_cold_ocean", "minecraft:deep_ocean", "minecraft:deep_lukewarm_ocean", "minecraft:frozen_ocean", "minecraft:ocean", "minecraft:cold_ocean", "minecraft:lukewarm_ocean", "minecraft:warm_ocean"), "minecraft:ruined_portal_ocean"), "minecraft:ruined_portal")) // Fix MC-248814, ruined_portal_standard->ruined_portal ++ .put("pillager_outpost", BiomeRemap.create("minecraft:pillager_outpost")) ++ .put("mansion", BiomeRemap.create("minecraft:mansion")) ++ .put("jungle_pyramid", BiomeRemap.create("minecraft:jungle_pyramid")) ++ .put("desert_pyramid", BiomeRemap.create("minecraft:desert_pyramid")) ++ .put("igloo", BiomeRemap.create("minecraft:igloo")) ++ .put("swamp_hut", BiomeRemap.create("minecraft:swamp_hut")) ++ .put("stronghold", BiomeRemap.create("minecraft:stronghold")) ++ .put("monument", BiomeRemap.create("minecraft:monument")) ++ .put("fortress", BiomeRemap.create("minecraft:fortress")) ++ .put("endcity", BiomeRemap.create("minecraft:end_city")) ++ .put("buried_treasure", BiomeRemap.create("minecraft:buried_treasure")) ++ .put("nether_fossil", BiomeRemap.create("minecraft:nether_fossil")) ++ .put("bastion_remnant", BiomeRemap.create("minecraft:bastion_remnant")) ++ .build() ++ ); ++ ++ public static void register() { ++ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ private static Object2IntOpenHashMap countBiomes(final MapType chunk) { ++ final ListType sections = chunk.getList("sections", ObjectType.MAP); ++ if (sections == null) { ++ return null; ++ } ++ ++ final Object2IntOpenHashMap ret = new Object2IntOpenHashMap<>(); ++ ++ for (int i = 0, len = sections.size(); i < len; ++i) { ++ final MapType section = sections.getMap(i); ++ ++ final MapType biomes = section.getMap("biomes"); ++ ++ if (biomes == null) { ++ continue; ++ } ++ ++ final ListType palette = biomes.getList("palette", ObjectType.STRING); ++ ++ if (palette == null) { ++ continue; ++ } ++ ++ for (int k = 0, len2 = palette.size(); k < len2; ++k) { ++ ret.addTo(palette.getString(k), 1); ++ } ++ } ++ ++ return ret; ++ } ++ ++ private static String getStructureConverted(String id, final Object2IntOpenHashMap biomeCount) { ++ id = id.toLowerCase(Locale.ROOT); ++ final BiomeRemap remap = CONVERSION_MAP.get(id); ++ if (remap == null) { ++ return null; ++ } ++ if (remap.biomeToNewStructure == null || biomeCount == null) { ++ return remap.dfl; ++ } ++ ++ final Object2IntOpenHashMap remapCount = new Object2IntOpenHashMap<>(); ++ ++ for (final Iterator> iterator = biomeCount.object2IntEntrySet().fastIterator(); iterator.hasNext();) { ++ final Object2IntMap.Entry entry = iterator.next(); ++ final String remappedStructure = remap.biomeToNewStructure.get(entry.getKey()); ++ if (remappedStructure != null) { ++ remapCount.addTo(remappedStructure, entry.getIntValue()); ++ } ++ } ++ ++ String converted = remap.dfl; ++ int maxCount = 0; ++ ++ for (final Iterator> iterator = remapCount.object2IntEntrySet().fastIterator(); iterator.hasNext();) { ++ final Object2IntMap.Entry entry = iterator.next(); ++ final int count = entry.getIntValue(); ++ if (count > maxCount) { ++ maxCount = count; ++ converted = entry.getKey(); ++ } ++ } ++ ++ return converted; ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType structures = data.getMap("structures"); ++ if (structures == null || structures.isEmpty()) { ++ return null; ++ } ++ ++ final Object2IntOpenHashMap biomeCounts = countBiomes(data); ++ ++ final MapType starts = structures.getMap("starts"); ++ final MapType references = structures.getMap("References"); ++ ++ if (starts != null) { ++ final MapType newStarts = data.getTypeUtil().createEmptyMap(); ++ structures.setMap("starts", newStarts); ++ ++ for (final String key : starts.keys()) { ++ final MapType value = starts.getMap(key); ++ if ("INVALID".equals(value.getString("id", "INVALID"))) { ++ continue; ++ } ++ ++ final String remapped = getStructureConverted(key, biomeCounts); ++ ++ if (remapped == null) { ++ LOGGER.warn("Encountered unknown structure in dataconverter: " + key); ++ continue; ++ } ++ ++ value.setString("id", remapped); ++ newStarts.setMap(remapped, value); ++ } ++ } ++ ++ // This TRULY is a guess, no idea what biomes the referent has. ++ if (references != null) { ++ final MapType newReferences = data.getTypeUtil().createEmptyMap(); ++ structures.setMap("References", newReferences); ++ for (final String key : references.keys()) { ++ final long[] value = references.getLongs(key); ++ if (value.length == 0) { ++ continue; ++ } ++ ++ final String newKey = getStructureConverted(key, biomeCounts); ++ if (newKey == null) { ++ LOGGER.warn("Encountered unknown structure reference in dataconverter: " + key); ++ continue; ++ } ++ ++ newReferences.setLongs(newKey, value); ++ } ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V2970() {} ++ ++ private static final class BiomeRemap { ++ ++ public final Map biomeToNewStructure; ++ public final String dfl; ++ ++ private BiomeRemap(final Map biomeToNewStructure, final String dfl) { ++ this.biomeToNewStructure = biomeToNewStructure; ++ this.dfl = dfl; ++ } ++ ++ public static BiomeRemap create(final String newId) { ++ return new BiomeRemap(null, newId); ++ } ++ ++ public static BiomeRemap create(final Map, String> biomeMap, final String newId) { ++ final Map biomeToNewStructure = new HashMap<>(); ++ ++ for (final Map.Entry, String> entry : biomeMap.entrySet()) { ++ final List biomes = entry.getKey(); ++ final String newBiomeStructure = entry.getValue(); ++ ++ for (int i = 0, len = biomes.size(); i < len; ++i) { ++ final String biome = biomes.get(i); ++ if (biomeToNewStructure.putIfAbsent(biome, newBiomeStructure) != null) { ++ throw new IllegalStateException("Duplicate biome remap: " + biome + " -> " + newBiomeStructure + ", but already mapped to " + biomeToNewStructure.get(biome)); ++ } ++ } ++ } ++ ++ return new BiomeRemap(biomeToNewStructure, newId); ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3077.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3077.java +new file mode 100644 +index 0000000000000000000000000000000000000000..06fe7dd2580cd8eaad9e0c7de8d0e27287d1b0a9 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3077.java +@@ -0,0 +1,40 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++ ++public final class V3077 { ++ ++ private static final int VERSION = MCVersions.V1_18_2 + 102; ++ ++ public static void register() { ++ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final boolean isLightOn = data.getBoolean("isLightOn"); ++ if (isLightOn) { ++ return null; ++ } ++ ++ final ListType sections = data.getList("sections", ObjectType.MAP); ++ if (sections == null) { ++ return null; ++ } ++ ++ for (int i = 0, len = sections.size(); i < len; ++i) { ++ final MapType section = sections.getMap(i); ++ section.remove("BlockLight"); ++ section.remove("SkyLight"); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V3077() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3078.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3078.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a0e89f59c75f6d34480f4b8c4f8fa013dddc5d52 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3078.java +@@ -0,0 +1,23 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.game_event.GameEventListenerWalker; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++ ++public final class V3078 { ++ ++ private static final int VERSION = MCVersions.V1_18_2 + 103; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ registerMob("minecraft:frog"); ++ registerMob("minecraft:tadpole"); ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:sculk_shrieker", new GameEventListenerWalker()); ++ } ++ ++ private V3078() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3081.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3081.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fb108cb7f50755b52f537a888ca155aa9db39b3a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3081.java +@@ -0,0 +1,21 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.game_event.GameEventListenerWalker; ++ ++public final class V3081 { ++ ++ private static final int VERSION = MCVersions.V22W11A + 1; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ registerMob("minecraft:warden"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:warden", new GameEventListenerWalker()); ++ } ++ ++ private V3081() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3082.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3082.java +new file mode 100644 +index 0000000000000000000000000000000000000000..79768b25a32a5333f8cb6ec6e8c422478a6891df +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3082.java +@@ -0,0 +1,16 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++ ++public final class V3082 { ++ ++ private static final int VERSION = MCVersions.V22W11A + 2; ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:chest_boat", new DataWalkerItemLists("Items")); ++ } ++ ++ private V3082() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3083.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3083.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e6e70ff4a28446fd8c4c663e0e44791ec4e5ac0a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3083.java +@@ -0,0 +1,23 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.game_event.GameEventListenerWalker; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++ ++public final class V3083 { ++ ++ private static final int VERSION = MCVersions.V22W12A + 1; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ registerMob("minecraft:allay"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:allay", new DataWalkerItemLists("Inventory")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:allay", new GameEventListenerWalker()); ++ } ++ ++ private V3083() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3084.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3084.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6a096226995e89285054b4ab35ed3e14ae4da694 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3084.java +@@ -0,0 +1,42 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.util.NamespaceUtil; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V3084 { ++ ++ private static final int VERSION = MCVersions.V22W12A + 2; ++ ++ private static final Map GAME_EVENT_RENAMES = new HashMap<>( ++ ImmutableMap.builder() ++ .put("minecraft:block_press", "minecraft:block_activate") ++ .put("minecraft:block_switch", "minecraft:block_activate") ++ .put("minecraft:block_unpress", "minecraft:block_deactivate") ++ .put("minecraft:block_unswitch", "minecraft:block_deactivate") ++ .put("minecraft:drinking_finish", "minecraft:drink") ++ .put("minecraft:elytra_free_fall", "minecraft:elytra_glide") ++ .put("minecraft:entity_damaged", "minecraft:entity_damage") ++ .put("minecraft:entity_dying", "minecraft:entity_die") ++ .put("minecraft:entity_killed", "minecraft:entity_die") ++ .put("minecraft:mob_interact", "minecraft:entity_interact") ++ .put("minecraft:ravager_roar", "minecraft:entity_roar") ++ .put("minecraft:ring_bell", "minecraft:block_change") ++ .put("minecraft:shulker_close", "minecraft:container_close") ++ .put("minecraft:shulker_open", "minecraft:container_open") ++ .put("minecraft:wolf_shaking", "minecraft:entity_shake") ++ .build() ++ ); ++ ++ public static void register() { ++ ConverterAbstractStringValueTypeRename.register(VERSION, MCTypeRegistry.GAME_EVENT_NAME, (final String name) -> { ++ return GAME_EVENT_RENAMES.get(NamespaceUtil.correctNamespace(name)); ++ }); ++ } ++ ++ private V3084() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3086.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3086.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f06412a417ba12111c9e8f30b747ed3ad6dcbcb6 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3086.java +@@ -0,0 +1,54 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.advancements.ConverterCriteriaRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterEntityToVariant; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import com.google.common.collect.ImmutableMap; ++import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V3086 { ++ ++ private static final int VERSION = MCVersions.V22W13A + 1; ++ ++ private static final Int2ObjectOpenHashMap CAT_ID_CONVERSION = new Int2ObjectOpenHashMap<>(); ++ static { ++ CAT_ID_CONVERSION.defaultReturnValue("minecraft:tabby"); ++ CAT_ID_CONVERSION.put(0, "minecraft:tabby"); ++ CAT_ID_CONVERSION.put(1, "minecraft:black"); ++ CAT_ID_CONVERSION.put(2, "minecraft:red"); ++ CAT_ID_CONVERSION.put(3, "minecraft:siamese"); ++ CAT_ID_CONVERSION.put(4, "minecraft:british"); ++ CAT_ID_CONVERSION.put(5, "minecraft:calico"); ++ CAT_ID_CONVERSION.put(6, "minecraft:persian"); ++ CAT_ID_CONVERSION.put(7, "minecraft:ragdoll"); ++ CAT_ID_CONVERSION.put(8, "minecraft:white"); ++ CAT_ID_CONVERSION.put(9, "minecraft:jellie"); ++ CAT_ID_CONVERSION.put(10, "minecraft:all_black"); ++ } ++ ++ private static final Map CAT_ADVANCEMENTS_CONVERSION = new HashMap<>( ++ ImmutableMap.builder() ++ .put("textures/entity/cat/tabby.png", "minecraft:tabby") ++ .put("textures/entity/cat/black.png", "minecraft:black") ++ .put("textures/entity/cat/red.png", "minecraft:red") ++ .put("textures/entity/cat/siamese.png", "minecraft:siamese") ++ .put("textures/entity/cat/british_shorthair.png", "minecraft:british") ++ .put("textures/entity/cat/calico.png", "minecraft:calico") ++ .put("textures/entity/cat/persian.png", "minecraft:persian") ++ .put("textures/entity/cat/ragdoll.png", "minecraft:ragdoll") ++ .put("textures/entity/cat/white.png", "minecraft:white") ++ .put("textures/entity/cat/jellie.png", "minecraft:jellie") ++ .put("textures/entity/cat/all_black.png", "minecraft:all_black") ++ .build() ++ ); ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:cat", new ConverterEntityToVariant(VERSION, "CatType", CAT_ID_CONVERSION::get)); ++ MCTypeRegistry.ADVANCEMENTS.addStructureConverter(new ConverterCriteriaRename(VERSION, "minecraft:husbandry/complete_catalogue", CAT_ADVANCEMENTS_CONVERSION::get)); ++ } ++ ++ private V3086() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3087.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3087.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b296229502491b54f6352ee1f9db0023296b36ec +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3087.java +@@ -0,0 +1,24 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterEntityToVariant; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; ++ ++public final class V3087 { ++ ++ private static final int VERSION = MCVersions.V22W13A + 2; ++ ++ private static final Int2ObjectOpenHashMap FROG_ID_CONVERSION = new Int2ObjectOpenHashMap<>(); ++ static { ++ FROG_ID_CONVERSION.put(0, "minecraft:temperate"); ++ FROG_ID_CONVERSION.put(1, "minecraft:warm"); ++ FROG_ID_CONVERSION.put(2, "minecraft:cold"); ++ } ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:frog", new ConverterEntityToVariant(VERSION, "Variant", FROG_ID_CONVERSION::get)); ++ } ++ ++ private V3087() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3088.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3088.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2752dfd1a7ff896e2ed736846980da5adde6e657 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3088.java +@@ -0,0 +1,25 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.chunk.ConverterAddBlendingData; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++ ++public final class V3088 { ++ ++ // this class originally targeted 3079 but was changed to target a later version without changing the converter, zero clue why ++ // this class then targeted 3088 but was changed to target 3441 ++ // to maintain integrity of the data version, I chose to extract the converter to a separate class and use it in both versions ++ // the reason it is important to never change old converters once released is that it creates _two_ versions under the same id. ++ // Consider the case where a user force upgrades their world, but does not load the chunk. Then, consider the case where ++ // the user does not force upgrade their world. Then, Mojang comes along and makes a decision like this and now both ++ // players load the chunk - they went through a different conversion process, which ultimately creates two versions. ++ // Unfortunately this fix doesn't exactly resolve it, as anyone running Mojang's converters will now be different ++ // from DataConverter's. It's broadly a dumb situation all around that could be avoided if Mojang wasn't being careless here. ++ private static final int VERSION = MCVersions.V22W14A; ++ ++ public static void register() { ++ MCTypeRegistry.CHUNK.addStructureConverter(new ConverterAddBlendingData(VERSION)); ++ } ++ ++ private V3088() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3090.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3090.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b1cfe038364e12d542d93c3108887173b2e05262 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3090.java +@@ -0,0 +1,25 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V3090 { ++ ++ private static final int VERSION = MCVersions.V22W15A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:painting", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ RenameHelper.renameSingle(data, "Motive", "variant"); ++ RenameHelper.renameSingle(data, "Facing", "facing"); ++ return null; ++ } ++ }); ++ } ++ ++ private V3090() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3093.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3093.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d0677d68b393da9b151c7b2add2fbbd8608e315f +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3093.java +@@ -0,0 +1,24 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V3093 { ++ ++ private static final int VERSION = MCVersions.V22W17A; ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:goat", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ data.setBoolean("HasLeftHorn", true); ++ data.setBoolean("HasRightHorn", true); ++ return null; ++ } ++ }); ++ } ++ ++ private V3093() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3094.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3094.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9b9ef34db7dbae8574c4bb3d474592e7991441d5 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3094.java +@@ -0,0 +1,44 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V3094 { ++ ++ private static final int VERSION = MCVersions.V22W17A + 1; ++ ++ private static final String[] SOUND_VARIANT_TO_INSTRUMENT = new String[] { ++ "minecraft:ponder_goat_horn", ++ "minecraft:sing_goat_horn", ++ "minecraft:seek_goat_horn", ++ "minecraft:feel_goat_horn", ++ "minecraft:admire_goat_horn", ++ "minecraft:call_goat_horn", ++ "minecraft:yearn_goat_horn", ++ "minecraft:dream_goat_horn" ++ }; ++ ++ public static void register() { ++ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:goat_horn", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType tag = data.getMap("tag"); ++ ++ if (tag == null) { ++ return null; ++ } ++ ++ final int soundVariant = tag.getInt("SoundVariant"); ++ tag.remove("SoundVariant"); ++ ++ tag.setString("instrument", SOUND_VARIANT_TO_INSTRUMENT[soundVariant < 0 || soundVariant >= SOUND_VARIANT_TO_INSTRUMENT.length ? 0 : soundVariant]); ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V3094() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3097.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3097.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c70d6dc72d1d913904b71640fe3476c644449ee2 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3097.java +@@ -0,0 +1,63 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.advancements.ConverterCriteriaRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterEntityVariantRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.poi.ConverterPoiDelete; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import java.util.HashMap; ++import java.util.HashSet; ++import java.util.Map; ++import java.util.Set; ++ ++public final class V3097 { ++ ++ private static final int VERSION = MCVersions.V22W19A + 1; ++ ++ public static void register() { ++ final DataConverter, MapType> removeFilteredBookText = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType tag = data.getMap("tag"); ++ if (tag == null) { ++ return null; ++ } ++ ++ tag.remove("filtered_title"); ++ tag.remove("filtered_pages"); ++ ++ return null; ++ } ++ }; ++ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:writable_book", removeFilteredBookText); ++ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:written_book", removeFilteredBookText); ++ ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:sign", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ data.remove("FilteredText1"); ++ data.remove("FilteredText2"); ++ data.remove("FilteredText3"); ++ data.remove("FilteredText4"); ++ ++ return null; ++ } ++ }); ++ ++ final Map britishRenamer = new HashMap<>(Map.of( ++ "minecraft:british", "minecraft:british_shorthair" ++ )); ++ final Set poiRemove = new HashSet<>(Set.of( ++ "minecraft:unemployed", ++ "minecraft:nitwit" ++ )); ++ ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:cat", new ConverterEntityVariantRename(VERSION, britishRenamer::get)); ++ MCTypeRegistry.ADVANCEMENTS.addStructureConverter(new ConverterCriteriaRename(VERSION, "minecraft:husbandry/complete_catalogue", britishRenamer::get)); ++ MCTypeRegistry.POI_CHUNK.addStructureConverter(new ConverterPoiDelete(VERSION, poiRemove::contains)); ++ } ++ ++ private V3097() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3108.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3108.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0a1ef2e55a1f9cd6381a2c6fdc04f81ee190ba81 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3108.java +@@ -0,0 +1,29 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V3108 { ++ ++ private static final int VERSION = MCVersions.V1_19_1_PRE1 + 1; ++ ++ public static void register() { ++ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType context = data.getMap("__context"); ++ if ("minecraft:overworld".equals(context == null ? null : context.getString("dimension"))) { ++ return null; ++ } ++ ++ data.remove("blending_data"); ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V3108() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3201.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3201.java +new file mode 100644 +index 0000000000000000000000000000000000000000..04b84c8466d4fa8f2ad21aae2de44273c05495b6 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3201.java +@@ -0,0 +1,35 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V3201 { ++ ++ private static final int VERSION = MCVersions.V1_19_2 + 81; ++ ++ public static void register() { ++ MCTypeRegistry.OPTIONS.addStructureConverter(new DataConverter<>(VERSION) { ++ private static void fixList(final MapType data, final String target) { ++ if (data == null) { ++ return; ++ } ++ final String curr = data.getString(target); ++ if (curr == null) { ++ return; ++ } ++ data.setString(target, curr.replace("\"programer_art\"", "\"programmer_art\"")); ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ fixList(data, "resourcePacks"); ++ fixList(data, "incompatibleResourcePacks"); ++ return null; ++ } ++ }); ++ } ++ ++ private V3201() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3203.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3203.java +new file mode 100644 +index 0000000000000000000000000000000000000000..db9eb946638447445649f4576b3698c0774e44bb +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3203.java +@@ -0,0 +1,20 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++ ++public final class V3203 { ++ ++ private static final int VERSION = MCVersions.V1_19_2 + 83; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ registerMob("minecraft:camel"); ++ } ++ ++ private V3203() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3204.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3204.java +new file mode 100644 +index 0000000000000000000000000000000000000000..87053c0c1de258770e7630830307ab484915aad8 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3204.java +@@ -0,0 +1,16 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++ ++public final class V3204 { ++ ++ private static final int VERSION = MCVersions.V1_19_2 + 84; ++ ++ public static void register() { ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:chiseled_bookshelf", new DataWalkerItemLists("Items")); ++ } ++ ++ private V3204() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3209.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3209.java +new file mode 100644 +index 0000000000000000000000000000000000000000..85270a36c5f75b1c6be49e461b302c3339c95750 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3209.java +@@ -0,0 +1,18 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemstack.ConverterFlattenSpawnEgg; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++ ++public final class V3209 { ++ ++ private static final int VERSION = MCVersions.V22W45A + 1; ++ ++ public static void register() { ++ // Note: This converter reads entity id from its sub data, but we need no breakpoint because entity ids are not ++ // remapped this version ++ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:pig_spawn_egg", new ConverterFlattenSpawnEgg(VERSION, 0)); ++ } ++ ++ private V3209() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3214.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3214.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0096664e25dca8d690c6154324f97efdb1ada723 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3214.java +@@ -0,0 +1,30 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V3214 { ++ ++ private static final int VERSION = MCVersions.V1_19_3_PRE3 + 1; ++ ++ public static void register() { ++ MCTypeRegistry.OPTIONS.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final String value = data.getString("ao"); ++ ++ if ("0".equals(value)) { ++ data.setString("ao", "false"); ++ } else if ("1".equals(value) || "2".equals(value)) { ++ data.setString("ao", "true"); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V3214() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3319.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3319.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0a4a1f690f568b8977e9b2caaf7fba15cb3307a5 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3319.java +@@ -0,0 +1,23 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V3319 { ++ ++ private static final int VERSION = MCVersions.V1_19_3 + 101; ++ ++ public static void register() { ++ MCTypeRegistry.OPTIONS.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ data.setBoolean("onboardAccessibility", false); ++ return null; ++ } ++ }); ++ } ++ ++ private V3319() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3322.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3322.java +new file mode 100644 +index 0000000000000000000000000000000000000000..53827b9b8999e7b284f3df0f41c98dbbc7d69c1c +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3322.java +@@ -0,0 +1,84 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++ ++import java.util.HashSet; ++import java.util.Set; ++ ++public final class V3322 { ++ ++ private static final int VERSION = MCVersions.V23W04A + 1; ++ ++ private static final Set EFFECT_ITEM_TYPES = new HashSet<>( ++ Set.of( ++ "minecraft:potion", ++ "minecraft:splash_potion", ++ "minecraft:lingering_potion", ++ "minecraft:tipped_arrow" ++ ) ++ ); ++ ++ private static void updateEffectList(final MapType root, final String path) { ++ if (root == null) { ++ return; ++ } ++ ++ final ListType effects = root.getList(path, ObjectType.MAP); ++ ++ if (effects == null) { ++ return; ++ } ++ ++ for (int i = 0, len = effects.size(); i < len; ++i) { ++ final MapType data = effects.getMap(i); ++ final MapType factorData = data.getMap("FactorCalculationData"); ++ if (factorData == null) { ++ continue; ++ } ++ ++ final int timestamp = factorData.getInt("effect_changed_timestamp", -1); ++ factorData.remove("effect_changed_timestamp"); ++ ++ final int duration = data.getInt("Duration", -1); ++ ++ final int ticksActive = timestamp - duration; ++ factorData.setInt("ticks_active", ticksActive); ++ } ++ } ++ ++ public static void register() { ++ final DataConverter, MapType> entityEffectFix = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ updateEffectList(data, "Effects"); ++ updateEffectList(data, "ActiveEffects"); ++ updateEffectList(data, "CustomPotionEffects"); ++ return null; ++ } ++ }; ++ ++ MCTypeRegistry.PLAYER.addStructureConverter(entityEffectFix); ++ MCTypeRegistry.ENTITY.addStructureConverter(entityEffectFix); ++ ++ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final String id = data.getString("id"); ++ if (!EFFECT_ITEM_TYPES.contains(id)) { ++ return null; ++ } ++ ++ updateEffectList(data.getMap("tag"), "CustomPotionEffects"); ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V3322() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3325.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3325.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2120a5928446f2597fb261d5d6e91c3c9700cb22 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3325.java +@@ -0,0 +1,19 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; ++ ++public final class V3325 { ++ ++ private static final int VERSION = MCVersions.V23W05A + 2; ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:item_display", new DataWalkerItems("item")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:block_display", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "block_state")); ++ // text_display is a simple entity ++ } ++ ++ private V3325() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3326.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3326.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6a8d3f6fd18d941e5b0b18fc5208b7fe1f9fd724 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3326.java +@@ -0,0 +1,20 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++ ++public final class V3326 { ++ ++ private static final int VERSION = MCVersions.V23W06A; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ registerMob("minecraft:sniffer"); ++ } ++ ++ private V3326() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3327.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3327.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7051d4f01b6f43f3d435d21d65b83ba702ffda41 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3327.java +@@ -0,0 +1,19 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerListPaths; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; ++ ++public final class V3327 { ++ ++ private static final int VERSION = MCVersions.V23W06A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:decorated_pot", new DataWalkerListPaths<>(MCTypeRegistry.ITEM_NAME, "shards")); ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:decorated_pot", new DataWalkerItems("item")); ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:suspicious_sand", new DataWalkerItems("item")); ++ } ++ ++ private V3327() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3328.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3328.java +new file mode 100644 +index 0000000000000000000000000000000000000000..75a3cbc8e6749abd4bceff710d2f7c3ca6df9d70 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3328.java +@@ -0,0 +1,15 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++ ++public final class V3328 { ++ ++ private static final int VERSION = MCVersions.V23W06A + 2; ++ ++ public static void register() { ++ // registers simple entity "minecraft:interaction" ++ } ++ ++ private V3328() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3438.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3438.java +new file mode 100644 +index 0000000000000000000000000000000000000000..30c23572a5989f0bd6bff6e424ee59c84bc8d8f1 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3438.java +@@ -0,0 +1,47 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.tileentity.ConverterAbstractTileEntityRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V3438 { ++ ++ public static final int VERSION = MCVersions.V1_19_4 + 101; ++ ++ public static void register() { ++ // brushable block rename ++ MCTypeRegistry.TILE_ENTITY.copyWalkers(VERSION, "minecraft:suspicious_sand", "minecraft:brushable_block"); ++ ++ ConverterAbstractTileEntityRename.register(VERSION, new HashMap<>(Map.of( ++ "minecraft:suspicious_sand", "minecraft:brushable_block" ++ ))::get); ++ ++ ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:brushable_block", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ RenameHelper.renameSingle(data, "loot_table", "LootTable"); ++ RenameHelper.renameSingle(data, "loot_table_seed", "LootTableSeed"); ++ return null; ++ } ++ }); ++ ++ ConverterAbstractItemRename.register(VERSION, new HashMap<>( ++ Map.of( ++ "minecraft:pottery_shard_archer", "minecraft:archer_pottery_shard", ++ "minecraft:pottery_shard_prize", "minecraft:prize_pottery_shard", ++ "minecraft:pottery_shard_arms_up", "minecraft:arms_up_pottery_shard", ++ "minecraft:pottery_shard_skull", "minecraft:skull_pottery_shard" ++ ) ++ )::get); ++ } ++ ++ private V3438() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3439.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3439.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5c09f745e5200393cf4ecdcb5b42466c2e2d94d9 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3439.java +@@ -0,0 +1,96 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.util.ComponentUtils; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V3439 { ++ ++ private static final int VERSION = MCVersions.V1_19_4 + 102; ++ ++ public static void register() { ++ final DataConverter, MapType> signTileUpdater = new DataConverter<>(VERSION) { ++ private static final String DEFAULT_COLOR = "black"; ++ ++ private static ListType migrateToList(final MapType root, final String prefix) { ++ if (root == null) { ++ return null; ++ } ++ ++ final ListType ret = root.getTypeUtil().createEmptyList(); ++ ++ ret.addString(root.getString(prefix.concat("1"), ComponentUtils.EMPTY)); ++ ret.addString(root.getString(prefix.concat("2"), ComponentUtils.EMPTY)); ++ ret.addString(root.getString(prefix.concat("3"), ComponentUtils.EMPTY)); ++ ret.addString(root.getString(prefix.concat("4"), ComponentUtils.EMPTY)); ++ ++ return ret; ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ // front text ++ final MapType frontText = data.getTypeUtil().createEmptyMap(); ++ data.setMap("front_text", frontText); ++ ++ final ListType frontMessages = migrateToList(data, "Text"); ++ frontText.setList("messages", frontMessages); ++ ++ ListType frontFilteredMessages = null; ++ ++ for (int i = 0; i < 4; ++i) { ++ final String filtered = data.getString("FilteredText" + i); ++ if (filtered == null) { ++ if (frontFilteredMessages != null) { ++ frontFilteredMessages.addString(frontMessages.getString(i)); ++ } ++ continue; ++ } ++ ++ if (frontFilteredMessages == null) { ++ frontFilteredMessages = data.getTypeUtil().createEmptyList(); ++ for (int k = 0; k < i; ++k) { ++ frontFilteredMessages.addString(frontMessages.getString(k)); ++ } ++ } ++ ++ frontFilteredMessages.addString(filtered); ++ } ++ ++ if (frontFilteredMessages != null) { ++ frontText.setList("filtered_messages", frontFilteredMessages); ++ } ++ ++ frontText.setString("color", data.getString("Color", DEFAULT_COLOR)); ++ frontText.setBoolean("has_glowing_text", data.getBoolean("GlowingText", false)); ++ frontText.setBoolean("_filtered_correct", true); ++ ++ // back text ++ final MapType backText = data.getTypeUtil().createEmptyMap(); ++ data.setMap("back_text", backText); ++ ++ final ListType blankMessages = data.getTypeUtil().createEmptyList(); ++ backText.setList("messages", blankMessages); ++ ++ for (int i = 0; i < 4; ++i) { ++ blankMessages.addString(ComponentUtils.EMPTY); ++ } ++ ++ backText.setString("color", DEFAULT_COLOR); ++ backText.setBoolean("has_glowing_text", false); ++ ++ // misc ++ data.setBoolean("is_waxed", false); ++ return null; ++ } ++ }; ++ ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:sign", signTileUpdater); ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:hanging_sign", signTileUpdater); ++ } ++ ++ private V3439() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3440.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3440.java +new file mode 100644 +index 0000000000000000000000000000000000000000..914df7885582a1fde398e755dbfaf00e19ce16b3 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3440.java +@@ -0,0 +1,29 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.leveldat.ConverterRemoveFeatureFlag; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.util.NamespaceUtil; ++ ++import java.util.Arrays; ++import java.util.HashSet; ++ ++public final class V3440 { ++ ++ private static final int VERSION = MCVersions.V1_19_4 + 103; ++ ++ public static void register() { ++ // Note: MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST is namespaced string ++ ConverterAbstractStringValueTypeRename.register(VERSION, MCTypeRegistry.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST, (final String in) -> { ++ return "minecraft:overworld_update_1_20".equals(NamespaceUtil.correctNamespace(in)) ? "minecraft:overworld" : null; ++ }); ++ MCTypeRegistry.LEVEL.addStructureConverter(new ConverterRemoveFeatureFlag(VERSION, new HashSet<>( ++ Arrays.asList( ++ "minecraft:update_1_20" ++ ) ++ ))); ++ } ++ ++ private V3440() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3441.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3441.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2cf41561b4a229ba4d60540f85ba0a8946bb9753 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3441.java +@@ -0,0 +1,17 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.chunk.ConverterAddBlendingData; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++ ++public final class V3441 { ++ ++ private static final int VERSION = MCVersions.V1_19_4 + 104; ++ ++ public static void register() { ++ // See V3088 for why this converter is duplicated here and in V3088 ++ MCTypeRegistry.CHUNK.addStructureConverter(new ConverterAddBlendingData(VERSION)); ++ } ++ ++ private V3441() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3447.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3447.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5db4a5222bf80f01c128d97ec849b89003837beb +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3447.java +@@ -0,0 +1,49 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V3447 { ++ ++ private static final int VERSION = MCVersions.V23W14A + 2; ++ ++ public static void register() { ++ final String[] targets = new String[] { ++ "minecraft:angler_pottery_shard", ++ "minecraft:archer_pottery_shard", ++ "minecraft:arms_up_pottery_shard", ++ "minecraft:blade_pottery_shard", ++ "minecraft:brewer_pottery_shard", ++ "minecraft:burn_pottery_shard", ++ "minecraft:danger_pottery_shard", ++ "minecraft:explorer_pottery_shard", ++ "minecraft:friend_pottery_shard", ++ "minecraft:heart_pottery_shard", ++ "minecraft:heartbreak_pottery_shard", ++ "minecraft:howl_pottery_shard", ++ "minecraft:miner_pottery_shard", ++ "minecraft:mourner_pottery_shard", ++ "minecraft:plenty_pottery_shard", ++ "minecraft:prize_pottery_shard", ++ "minecraft:sheaf_pottery_shard", ++ "minecraft:shelter_pottery_shard", ++ "minecraft:skull_pottery_shard", ++ "minecraft:snort_pottery_shard" ++ }; ++ // shard->sherd ++ final Map rename = new HashMap<>(targets.length); ++ ++ for (final String target : targets) { ++ final String replace = target.replace("_pottery_shard", "_pottery_sherd"); ++ if (rename.put(target, replace) != null) { ++ throw new IllegalArgumentException("Duplicate target " + target); ++ } ++ } ++ ++ ConverterAbstractItemRename.register(VERSION, rename::get); ++ } ++ ++ private V3447() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3448.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3448.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6f447d59677be4630b53e948419a0ae2a3414315 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3448.java +@@ -0,0 +1,28 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerListPaths; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V3448 { ++ ++ private static final int VERSION = MCVersions.V23W14A + 3; ++ ++ public static void register() { ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:decorated_pot", new DataWalkerListPaths<>(MCTypeRegistry.ITEM_NAME, "sherds")); ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:decorated_pot", new DataWalkerItems("item")); ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:decorated_pot", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ RenameHelper.renameSingle(data, "shards", "sherds"); ++ return null; ++ } ++ }); ++ } ++ ++ private V3448() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3450.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3450.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9e7f34a40280a7704c4a4d1c03a7dda8e030aff5 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3450.java +@@ -0,0 +1,23 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.chunk.ConverterRenameStatus; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V3450 { ++ ++ private static final int VERSION = MCVersions.V23W16A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.CHUNK.addStructureConverter(new ConverterRenameStatus(VERSION, new HashMap<>( ++ Map.of( ++ "minecraft:liquid_carvers", "minecraft:carvers", ++ "minecraft:heightmaps", "minecraft:spawn" ++ ) ++ )::get)); ++ } ++ ++ private V3450() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3451.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3451.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ea97d596bfb4b9c6b9b7d0534604e042a775ea78 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3451.java +@@ -0,0 +1,38 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++ ++public final class V3451 { ++ ++ private static final int VERSION = MCVersions.V23W16A + 2; ++ ++ public static void register() { ++ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ data.remove("isLightOn"); ++ ++ final ListType sections = data.getList("sections", ObjectType.MAP); ++ if (sections == null) { ++ return null; ++ } ++ ++ for (int i = 0, len = sections.size(); i < len; ++i) { ++ final MapType section = sections.getMap(i); ++ ++ section.remove("BlockLight"); ++ section.remove("SkyLight"); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V3451() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3459.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3459.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e8a1fcd9e67b151a360e11089289154a14dde27c +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3459.java +@@ -0,0 +1,38 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V3459 { ++ ++ private static final int VERSION = MCVersions.V1_20_PRE5 + 1; ++ ++ public static void register() { ++ MCTypeRegistry.LEVEL.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (data.hasKey("DragonFight")) { ++ return null; ++ } ++ ++ final MapType dimensionData = data.getMap("DimensionData"); ++ if (dimensionData == null) { ++ return null; ++ } ++ ++ final MapType endData = dimensionData.getMap("1"); ++ if (endData != null) { ++ final MapType dragonFight = endData.getMap("DragonFight", endData.getTypeUtil().createEmptyMap()).copy(); ++ V3807.flattenBlockPos(dragonFight, "ExitPortalLocation"); ++ data.setMap("DragonFight", dragonFight); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V3459() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3564.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3564.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5c64ec5b9bdcc279bc1b86e6bb0b877003b213cb +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3564.java +@@ -0,0 +1,93 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.util.ComponentUtils; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++ ++public final class V3564 { ++ ++ private static final int VERSION = MCVersions.V1_20_1 + 99; ++ ++ public static void register() { ++ final DataConverter, MapType> converter = new DataConverter<>(VERSION) { ++ ++ private static final String[] LEGACY_FIELDS = new String[] { ++ "Text1", ++ "Text2", ++ "Text3", ++ "Text4", ++ ++ "FilteredText1", ++ "FilteredText2", ++ "FilteredText3", ++ "FilteredText4", ++ ++ "Color", ++ ++ "GlowingText" ++ }; ++ ++ ++ private static void updateText(final MapType text) { ++ if (text == null) { ++ return; ++ } ++ ++ if (text.getBoolean("_filtered_correct", false)) { ++ text.remove("_filtered_correct"); ++ return; ++ } ++ ++ final ListType filteredMessages = text.getList("filtered_messages", ObjectType.STRING); ++ ++ if (filteredMessages == null || filteredMessages.size() == 0) { ++ return; ++ } ++ ++ // should treat null here as empty list ++ final ListType messages = text.getList("messages", ObjectType.STRING); ++ ++ final ListType newFilteredList = filteredMessages.getTypeUtil().createEmptyList(); ++ boolean newFilteredIsEmpty = true; ++ ++ for (int i = 0, len = filteredMessages.size(); i < len; ++i) { ++ final String filtered = filteredMessages.getString(i); ++ final String message = messages != null && i < messages.size() ? messages.getString(i) : ComponentUtils.EMPTY; ++ ++ final String newFiltered = ComponentUtils.EMPTY.equals(filtered) ? message : filtered; ++ ++ newFilteredList.addString(newFiltered); ++ ++ newFilteredIsEmpty = newFilteredIsEmpty && ComponentUtils.EMPTY.equals(newFiltered); ++ } ++ ++ if (newFilteredIsEmpty) { ++ text.remove("filtered_messages"); ++ } else { ++ text.setList("filtered_messages", newFilteredList); ++ } ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ updateText(data.getMap("front_text")); ++ updateText(data.getMap("back_text")); ++ ++ for (final String toRemove : LEGACY_FIELDS) { ++ data.remove(toRemove); ++ } ++ ++ return null; ++ } ++ }; ++ ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:sign", converter); ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:hanging_sign", converter); ++ } ++ ++ private V3564() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3565.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3565.java +new file mode 100644 +index 0000000000000000000000000000000000000000..685021e87236c5b7ce5ee0b5422d7bf856ea6652 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3565.java +@@ -0,0 +1,32 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V3565 { ++ ++ private static final int VERSION = MCVersions.V1_20_1 + 100; ++ ++ public static void register() { ++ MCTypeRegistry.SAVED_DATA_RANDOM_SEQUENCES.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType root, final long sourceVersion, final long toVersion) { ++ final MapType oldData = root.getMap("data"); ++ if (oldData == null) { ++ return null; ++ } ++ ++ final MapType newData = root.getTypeUtil().createEmptyMap(); ++ root.setMap("data", newData); ++ ++ newData.setMap("sequences", oldData); ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V3565() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3566.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3566.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a5b91dc5eb4e9206cbb4489ce14195d7517ef4e0 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3566.java +@@ -0,0 +1,58 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V3566 { ++ ++ private static final int VERSION = MCVersions.V1_20_1 + 101; ++ ++ public static void register() { ++ MCTypeRegistry.SAVED_DATA_SCOREBOARD.addStructureConverter(new DataConverter<>(VERSION) { ++ ++ private static final Map SLOT_RENAMES = new HashMap<>( ++ ImmutableMap.builder() ++ .put("slot_0", "list") ++ .put("slot_1", "sidebar") ++ .put("slot_2", "below_name") ++ .put("slot_3", "sidebar.team.black") ++ .put("slot_4", "sidebar.team.dark_blue") ++ .put("slot_5", "sidebar.team.dark_green") ++ .put("slot_6", "sidebar.team.dark_aqua") ++ .put("slot_7", "sidebar.team.dark_red") ++ .put("slot_8", "sidebar.team.dark_purple") ++ .put("slot_9", "sidebar.team.gold") ++ .put("slot_10", "sidebar.team.gray") ++ .put("slot_11", "sidebar.team.dark_gray") ++ .put("slot_12", "sidebar.team.blue") ++ .put("slot_13", "sidebar.team.green") ++ .put("slot_14", "sidebar.team.aqua") ++ .put("slot_15", "sidebar.team.red") ++ .put("slot_16", "sidebar.team.light_purple") ++ .put("slot_17", "sidebar.team.yellow") ++ .put("slot_18", "sidebar.team.white") ++ .build() ++ ); ++ ++ @Override ++ public MapType convert(final MapType root, final long sourceVersion, final long toVersion) { ++ final MapType data = root.getMap("data"); ++ if (data == null) { ++ return null; ++ } ++ ++ RenameHelper.renameKeys(data.getMap("DisplaySlots"), SLOT_RENAMES::get); ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V3566() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3568.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3568.java +new file mode 100644 +index 0000000000000000000000000000000000000000..39b4bd2d0bb36ef242467e89010ef5fc1490de9b +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3568.java +@@ -0,0 +1,245 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import java.util.HashMap; ++import java.util.HashSet; ++import java.util.Map; ++import java.util.Set; ++ ++public final class V3568 { ++ ++ private static final int VERSION = MCVersions.V23W31A + 1; ++ ++ private static final String[] EFFECT_ID_MAP = new String[34]; ++ static { ++ EFFECT_ID_MAP[1] = "minecraft:speed"; ++ EFFECT_ID_MAP[2] = "minecraft:slowness"; ++ EFFECT_ID_MAP[3] = "minecraft:haste"; ++ EFFECT_ID_MAP[4] = "minecraft:mining_fatigue"; ++ EFFECT_ID_MAP[5] = "minecraft:strength"; ++ EFFECT_ID_MAP[6] = "minecraft:instant_health"; ++ EFFECT_ID_MAP[7] = "minecraft:instant_damage"; ++ EFFECT_ID_MAP[8] = "minecraft:jump_boost"; ++ EFFECT_ID_MAP[9] = "minecraft:nausea"; ++ EFFECT_ID_MAP[10] = "minecraft:regeneration"; ++ EFFECT_ID_MAP[11] = "minecraft:resistance"; ++ EFFECT_ID_MAP[12] = "minecraft:fire_resistance"; ++ EFFECT_ID_MAP[13] = "minecraft:water_breathing"; ++ EFFECT_ID_MAP[14] = "minecraft:invisibility"; ++ EFFECT_ID_MAP[15] = "minecraft:blindness"; ++ EFFECT_ID_MAP[16] = "minecraft:night_vision"; ++ EFFECT_ID_MAP[17] = "minecraft:hunger"; ++ EFFECT_ID_MAP[18] = "minecraft:weakness"; ++ EFFECT_ID_MAP[19] = "minecraft:poison"; ++ EFFECT_ID_MAP[20] = "minecraft:wither"; ++ EFFECT_ID_MAP[21] = "minecraft:health_boost"; ++ EFFECT_ID_MAP[22] = "minecraft:absorption"; ++ EFFECT_ID_MAP[23] = "minecraft:saturation"; ++ EFFECT_ID_MAP[24] = "minecraft:glowing"; ++ EFFECT_ID_MAP[25] = "minecraft:levitation"; ++ EFFECT_ID_MAP[26] = "minecraft:luck"; ++ EFFECT_ID_MAP[27] = "minecraft:unluck"; ++ EFFECT_ID_MAP[28] = "minecraft:slow_falling"; ++ EFFECT_ID_MAP[29] = "minecraft:conduit_power"; ++ EFFECT_ID_MAP[30] = "minecraft:dolphins_grace"; ++ EFFECT_ID_MAP[31] = "minecraft:bad_omen"; ++ EFFECT_ID_MAP[32] = "minecraft:hero_of_the_village"; ++ EFFECT_ID_MAP[33] = "minecraft:darkness"; ++ } ++ private static final Set EFFECT_ITEMS = ++ new HashSet<>( ++ Set.of( ++ "minecraft:potion", ++ "minecraft:splash_potion", ++ "minecraft:lingering_potion", ++ "minecraft:tipped_arrow" ++ ) ++ ); ++ ++ private static String readLegacyEffect(final MapType data, final String path) { ++ final Number id = data.getNumber(path); ++ if (id == null) { ++ return null; ++ } ++ ++ final int castedId = id.intValue(); ++ return castedId >= 0 && castedId < EFFECT_ID_MAP.length ? EFFECT_ID_MAP[castedId] : null; ++ } ++ ++ private static void convertLegacyEffect(final MapType data, final String legacyPath, final String newPath) { ++ final Number id = data.getNumber(legacyPath); ++ data.remove(legacyPath); ++ ++ if (id == null) { ++ return; ++ } ++ ++ final int castedId = id.intValue(); ++ final String newId = castedId >= 0 && castedId < EFFECT_ID_MAP.length ? EFFECT_ID_MAP[castedId] : null; ++ ++ if (newId == null) { ++ return; ++ } ++ ++ data.setString(newPath, newId); ++ } ++ ++ private static final Map MOB_EFFECT_RENAMES = new HashMap<>(); ++ static { ++ MOB_EFFECT_RENAMES.put("Ambient", "ambient"); ++ MOB_EFFECT_RENAMES.put("Amplifier", "amplifier"); ++ MOB_EFFECT_RENAMES.put("Duration", "duration"); ++ MOB_EFFECT_RENAMES.put("ShowParticles", "show_particles"); ++ MOB_EFFECT_RENAMES.put("ShowIcon", "show_icon"); ++ MOB_EFFECT_RENAMES.put("FactorCalculationData", "factor_calculation_data"); ++ MOB_EFFECT_RENAMES.put("HiddenEffect", "hidden_effect"); ++ } ++ ++ private static void convertMobEffect(final MapType mobEffect) { ++ if (mobEffect == null) { ++ return; ++ } ++ ++ convertLegacyEffect(mobEffect, "Id", "id"); ++ ++ for (final Map.Entry rename : MOB_EFFECT_RENAMES.entrySet()) { ++ RenameHelper.renameSingle(mobEffect, rename.getKey(), rename.getValue()); ++ } ++ ++ convertMobEffect(mobEffect.getMap("hidden_effect")); ++ } ++ ++ private static void convertMobEffectList(final MapType data, final String oldPath, final String newPath) { ++ final ListType effects = data.getList(oldPath, ObjectType.MAP); ++ if (effects == null) { ++ return; ++ } ++ ++ for (int i = 0, len = effects.size(); i < len; ++i) { ++ convertMobEffect(effects.getMap(i)); ++ } ++ ++ data.remove(oldPath); ++ data.setList(newPath, effects); ++ } ++ ++ private static void removeAndSet(final MapType data, final String toRemovePath, ++ final String toSetPath, final Object toSet) { ++ data.remove(toRemovePath); ++ if (toSet != null) { ++ data.setGeneric(toSetPath, toSet); ++ } ++ } ++ ++ private static void updateSuspiciousStew(final MapType from, final MapType into) { ++ removeAndSet(into, "EffectId", "id", readLegacyEffect(from, "EffectId")); ++ removeAndSet(into, "EffectDuration", "duration", from.getGeneric("EffectDuration")); ++ } ++ ++ public static void register() { ++ final DataConverter, MapType> beaconConverter = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ convertLegacyEffect(data, "Primary", "primary_effect"); ++ convertLegacyEffect(data, "Secondary", "secondary_effect"); ++ ++ return null; ++ } ++ }; ++ ++ final DataConverter, MapType> mooshroomConverter = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType newEffect = data.getTypeUtil().createEmptyMap(); ++ updateSuspiciousStew(data, newEffect); ++ ++ data.remove("EffectId"); ++ data.remove("EffectDuration"); ++ ++ if (!newEffect.isEmpty()) { ++ final ListType stewEffects = data.getTypeUtil().createEmptyList(); ++ data.setList("stew_effects", stewEffects); ++ ++ stewEffects.addMap(newEffect); ++ } ++ ++ return null; ++ } ++ }; ++ final DataConverter, MapType> arrowConverter = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ convertMobEffectList(data, "CustomPotionEffects", "custom_potion_effects"); ++ return null; ++ } ++ }; ++ final DataConverter, MapType> areaEffectCloudConverter = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ convertMobEffectList(data, "Effects", "effects"); ++ return null; ++ } ++ }; ++ final DataConverter, MapType> livingEntityConverter = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ convertMobEffectList(data, "ActiveEffects", "active_effects"); ++ return null; ++ } ++ }; ++ ++ final DataConverter, MapType> itemConverter = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType root, final long sourceVersion, final long toVersion) { ++ final String id = root.getString("id"); ++ ++ final MapType tag = root.getMap("tag"); ++ ++ if (tag == null) { ++ return null; ++ } ++ ++ if ("minecraft:suspicious_stew".equals(id)) { ++ RenameHelper.renameSingle(tag, "Effects", "effects"); ++ ++ final ListType effects = tag.getList("effects", ObjectType.MAP); ++ ++ if (effects != null) { ++ for (int i = 0, len = effects.size(); i < len; ++i) { ++ final MapType effect = effects.getMap(i); ++ updateSuspiciousStew(effect, effect); ++ } ++ } ++ ++ return null; ++ } ++ ++ if (EFFECT_ITEMS.contains(id)) { ++ convertMobEffectList(tag, "CustomPotionEffects", "custom_potion_effects"); ++ return null; ++ } ++ ++ return null; ++ } ++ }; ++ ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:beacon", beaconConverter); ++ ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:mooshroom", mooshroomConverter); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:arrow", arrowConverter); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:area_effect_cloud", areaEffectCloudConverter); ++ MCTypeRegistry.ENTITY.addStructureConverter(livingEntityConverter); ++ ++ MCTypeRegistry.PLAYER.addStructureConverter(livingEntityConverter); ++ ++ MCTypeRegistry.ITEM_STACK.addStructureConverter(itemConverter); ++ } ++ ++ private V3568() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3682.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3682.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f9e3eb71b268f7bc1940f0199fddfa1ac27401b8 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3682.java +@@ -0,0 +1,16 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++ ++public final class V3682 { ++ ++ private static final int VERSION = MCVersions.V23W41A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:crafter", new DataWalkerItemLists("Items")); ++ } ++ ++ private V3682() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3683.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3683.java +new file mode 100644 +index 0000000000000000000000000000000000000000..855a95b4ef686fa9d2cefdef5664290528e4dc60 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3683.java +@@ -0,0 +1,33 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V3683 { ++ ++ private static final int VERSION = MCVersions.V23W41A + 2; ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:tnt", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ RenameHelper.renameSingle(data, "Fuse", "fuse"); ++ ++ final MapType defaultState = data.getTypeUtil().createEmptyMap(); ++ data.setMap("block_state", defaultState); ++ ++ defaultState.setString("Name", "minecraft:tnt"); ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:tnt", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "block_state")); ++ } ++ ++ private V3683() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3685.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3685.java +new file mode 100644 +index 0000000000000000000000000000000000000000..603467a7a2b6da93181a0a32eedb30d1614b0069 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3685.java +@@ -0,0 +1,64 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.TypeUtil; ++ ++public final class V3685 { ++ ++ private static final int VERSION = MCVersions.V23W42A + 1; ++ ++ private static String getType(final MapType arrow) { ++ return "minecraft:empty".equals(arrow.getString("Potion", "minecraft:empty")) ? "minecraft:arrow" : "minecraft:tipped_arrow"; ++ } ++ ++ private static MapType createItem(final TypeUtil util, final String id, final int count) { ++ final MapType ret = util.createEmptyMap(); ++ ++ ret.setString("id", id); ++ ret.setInt("Count", count); ++ ++ return ret; ++ } ++ ++ private static void registerArrowEntity(final String id) { ++ MCTypeRegistry.ENTITY.addWalker(VERSION, id, new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "inBlockState")); ++ // new: item ++ MCTypeRegistry.ENTITY.addWalker(VERSION, id, new DataWalkerItems("item")); ++ } ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:trident", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ RenameHelper.renameSingle(data, "Trident", "item"); ++ return null; ++ } ++ }); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:arrow", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ data.setMap("item", createItem(data.getTypeUtil(), getType(data), 1)); ++ return null; ++ } ++ }); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:spectral_arrow", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ data.setMap("item", createItem(data.getTypeUtil(), "minecraft:spectral_arrow", 1)); ++ return null; ++ } ++ }); ++ ++ registerArrowEntity("minecraft:trident"); ++ registerArrowEntity("minecraft:spectral_arrow"); ++ registerArrowEntity("minecraft:arrow"); ++ } ++ ++ private V3685() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3689.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3689.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ccda8d0f7c0a284fd4f91622dc33b832b4a95c45 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3689.java +@@ -0,0 +1,37 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++ ++public final class V3689 { ++ ++ private static final int VERSION = MCVersions.V23W44A + 1; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ registerMob("minecraft:breeze"); ++ // minecraft:wind_charge is a simple entity ++ // minecraft:breeze_wind_charge is a simple entity ++ ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:trial_spawner", (final MapType data, final long fromVersion, final long toVersion) -> { ++ final ListType spawnPotentials = data.getList("spawn_potentials", ObjectType.MAP); ++ if (spawnPotentials != null) { ++ for (int i = 0, len = spawnPotentials.size(); i < len; ++i) { ++ WalkerUtils.convert(MCTypeRegistry.ENTITY, spawnPotentials.getMap(i).getMap("data"), "entity", fromVersion, toVersion); ++ } ++ } ++ ++ WalkerUtils.convert(MCTypeRegistry.ENTITY, data.getMap("spawn_data"), "entity", fromVersion, toVersion); ++ return null; ++ }); ++ } ++ ++ private V3689() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3692.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3692.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1fc62f9cadb990790420376d5c80b14775b71a48 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3692.java +@@ -0,0 +1,25 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V3692 { ++ ++ private static final int VERSION = MCVersions.V23W46A + 1; ++ ++ private static final Map GRASS_RENAME = new HashMap<>( ++ Map.of( ++ "minecraft:grass", "minecraft:short_grass" ++ ) ++ ); ++ ++ public static void register() { ++ ConverterAbstractBlockRename.register(VERSION, GRASS_RENAME::get); ++ ConverterAbstractItemRename.register(VERSION, GRASS_RENAME::get); ++ } ++ ++ private V3692() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3799.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3799.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0c34d445825e8b49249af852b1f2f09c8b5a2574 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3799.java +@@ -0,0 +1,14 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++ ++public final class V3799 { ++ ++ private static final int VERSION = MCVersions.V1_20_4 + 99; ++ ++ public static void register() { ++ V100.registerEquipment(VERSION, "minecraft:armadillo"); ++ } ++ ++ private V3799() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3800.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3800.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a40397feb5962bd5f4a44cc85bb359f5f99ff03a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3800.java +@@ -0,0 +1,23 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V3800 { ++ ++ private static final int VERSION = MCVersions.V1_20_4 + 100; ++ ++ public static void register() { ++ final Map renames = new HashMap<>( ++ Map.of( ++ "minecraft:scute", "minecraft:turtle_scute" ++ ) ++ ); ++ ++ ConverterAbstractItemRename.register(VERSION, renames::get); ++ } ++ ++ private V3800() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3803.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3803.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7ff5e2f1a386d75b6d0d6fc3160f2241bf74b262 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3803.java +@@ -0,0 +1,24 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemstack.ConverterEnchantmentsRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V3803 { ++ ++ private static final int VERSION = MCVersions.V23W51B + 1; ++ ++ public static void register() { ++ final Map renames = new HashMap<>( ++ Map.of( ++ "minecraft:sweeping", "minecraft:sweeping_edge" ++ ) ++ ); ++ ++ MCTypeRegistry.ITEM_STACK.addStructureConverter(new ConverterEnchantmentsRename(VERSION, renames::get)); ++ } ++ ++ private V3803() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3807.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3807.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a76916cdb7cf91b8ba5461524472b3e455f02885 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3807.java +@@ -0,0 +1,72 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++ ++public final class V3807 { ++ ++ private static final int VERSION = MCVersions.V24W04A + 1; ++ ++ public static void flattenBlockPos(final MapType data, final String path) { ++ if (data == null) { ++ return; ++ } ++ ++ final MapType pos = data.getMap(path); ++ if (pos == null) { ++ return; ++ } ++ ++ final Number x = pos.getNumber("X"); ++ final Number y = pos.getNumber("Y"); ++ final Number z = pos.getNumber("Z"); ++ ++ if (x == null || y == null || z == null) { ++ return; ++ } ++ ++ data.setInts(path, new int[] { x.intValue(), y.intValue(), z.intValue() }); ++ } ++ ++ public static void register() { ++ // Step 0 ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:vault", (final MapType root, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convert(MCTypeRegistry.ITEM_STACK, root.getMap("config"), "key_item", fromVersion, toVersion); ++ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, root.getMap("server_data"), "items_to_eject", fromVersion, toVersion); ++ WalkerUtils.convert(MCTypeRegistry.ITEM_STACK, root.getMap("shared_data"), "display_item", fromVersion, toVersion); ++ ++ return null; ++ }); ++ ++ // Step 1 ++ MCTypeRegistry.SAVED_DATA_MAP_DATA.addStructureConverter(new DataConverter<>(VERSION, 1) { ++ @Override ++ public MapType convert(final MapType root, final long sourceVersion, final long toVersion) { ++ final MapType data = root.getMap("data"); ++ ++ if (data == null) { ++ return null; ++ } ++ ++ final ListType banners = data.getList("banners", ObjectType.MAP); ++ ++ if (banners == null) { ++ return null; ++ } ++ ++ for (int i = 0, len = banners.size(); i < len; ++i) { ++ V3807.flattenBlockPos(banners.getMap(i), "Pos"); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V3807() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3808.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3808.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cce86ebda50fc258eb28ad2f45e908b29934abec +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3808.java +@@ -0,0 +1,76 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++ ++public final class V3808 { ++ ++ private static final int VERSION = MCVersions.V24W04A + 2; ++ ++ public static void register() { ++ class BodyArmorConverter extends DataConverter, MapType> { ++ private final String path; ++ private final boolean clearArmor; ++ ++ public BodyArmorConverter(final int toVersion, final String path, final boolean clearArmor) { ++ this(toVersion, 0, path, clearArmor); ++ } ++ ++ public BodyArmorConverter(final int toVersion, final int versionStep, final String path, final boolean clearArmor) { ++ super(toVersion, versionStep); ++ ++ this.path = path; ++ this.clearArmor = clearArmor; ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType prev = data.getMap(this.path); ++ ++ if (prev == null) { ++ return null; ++ } ++ ++ data.remove(this.path); ++ ++ data.setMap("body_armor_item", prev); ++ data.setFloat("body_armor_drop_chance", 2.0F); ++ ++ if (this.clearArmor) { ++ final ListType armor = data.getList("ArmorItems", ObjectType.MAP); ++ if (armor.size() > 2) { ++ armor.setMap(2, data.getTypeUtil().createEmptyMap()); ++ } ++ ++ final ListType chances = data.getList("ArmorDropChances", ObjectType.FLOAT); ++ if (chances.size() > 2) { ++ chances.setFloat(2, 0.085F); ++ } ++ } ++ ++ return null; ++ } ++ } ++ ++ // Step 0 ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:horse", new BodyArmorConverter(VERSION, "ArmorItem", true)); ++ ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:horse", new DataWalkerItems("SaddleItem")); ++ V100.registerEquipment(VERSION, "minecraft:horse"); ++ ++ // Step 1 ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:llama", new BodyArmorConverter(VERSION, 1, "DecorItem", false)); ++ ++ MCTypeRegistry.ENTITY.addWalker(VERSION, 1, "minecraft:llama", new DataWalkerItemLists("Items")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, 1, "minecraft:llama", new DataWalkerItems("SaddleItem")); ++ V100.registerEquipment(VERSION, 1, "minecraft:llama"); ++ } ++ ++ private V3808() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3809.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3809.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7b782a6bb8d41f61201323d541f4acfac5af49dc +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3809.java +@@ -0,0 +1,40 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++ ++public final class V3809 { ++ ++ private static final int VERSION = MCVersions.V24W05A; ++ ++ public static void register() { ++ final DataConverter, MapType> slotConverter = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final ListType items = data.getList("Items", ObjectType.MAP); ++ if (items == null) { ++ return null; ++ } ++ ++ for (int i = 0, len = items.size(); i < len; ++i) { ++ final MapType item = items.getMap(i); ++ ++ final int slot = item.getInt("Slot", 2); ++ item.setByte("Slot", (byte)(slot - 2)); ++ } ++ ++ return null; ++ } ++ }; ++ ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:llama", slotConverter); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:mule", slotConverter); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:donkey", slotConverter); ++ } ++ ++ private V3809() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3812.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3812.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f0c0748b003648e5fe06d0b6dd1b21948ac0e54a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3812.java +@@ -0,0 +1,48 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.util.NamespaceUtil; ++ ++public final class V3812 { ++ ++ private static final int VERSION = MCVersions.V24W05B + 1; ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:wolf", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ boolean doubleHealth = false; ++ ++ final ListType attributes = data.getList("Attributes", ObjectType.MAP); ++ if (attributes != null) { ++ for (int i = 0, len = attributes.size(); i < len; ++i) { ++ final MapType attribute = attributes.getMap(i); ++ ++ if (!"minecraft:generic.max_health".equals(NamespaceUtil.correctNamespace(attribute.getString("Name")))) { ++ continue; ++ } ++ ++ final double base = attribute.getDouble("Base", 0.0D); ++ if (base == 20.0D) { ++ attribute.setDouble("Base", 40.0D); ++ doubleHealth = true; ++ } ++ } ++ } ++ ++ if (doubleHealth) { ++ data.setFloat("Health", data.getFloat("Health", 0.0F) * 2.0F); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V3812() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3813.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3813.java +new file mode 100644 +index 0000000000000000000000000000000000000000..920d7734d883d74e8334102b22cabce24a79db7e +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3813.java +@@ -0,0 +1,131 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++ ++public final class V3813 { ++ ++ private static final int VERSION = MCVersions.V24W05B + 2; ++ ++ private static final String[] PATROLLING_MOBS = new String[] { ++ "minecraft:witch", ++ "minecraft:ravager", ++ "minecraft:pillager", ++ "minecraft:illusioner", ++ "minecraft:evoker", ++ "minecraft:vindicator" ++ }; ++ ++ public static void register() { ++ class RootPositionConverter extends DataConverter, MapType> { ++ private final RenamePair[] convert; ++ ++ public RootPositionConverter(final int toVersion, final RenamePair[] convert) { ++ this(toVersion, 0, convert); ++ } ++ ++ public RootPositionConverter(final int toVersion, final int versionStep, final RenamePair[] convert) { ++ super(toVersion, versionStep); ++ this.convert = convert; ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ for (final RenamePair rename : this.convert) { ++ V3807.flattenBlockPos(data, rename.from); ++ RenameHelper.renameSingle(data, rename.from, rename.to); ++ } ++ return null; ++ } ++ } ++ ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:bee", new RootPositionConverter(VERSION, new RenamePair[] { ++ new RenamePair("HivePos", "hive_pos"), ++ new RenamePair("FlowerPos", "flower_pos") ++ })); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:end_crystal", new RootPositionConverter(VERSION, new RenamePair[] { ++ new RenamePair("BeamTarget", "beam_target"), ++ })); ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:wandering_trader", new RootPositionConverter(VERSION, new RenamePair[] { ++ new RenamePair("WanderTarget", "wander_target"), ++ })); ++ ++ final RootPositionConverter patrolConverter = new RootPositionConverter(VERSION, new RenamePair[] { ++ new RenamePair("PatrolTarget", "patrol_target"), ++ }); ++ for (final String id : PATROLLING_MOBS) { ++ MCTypeRegistry.ENTITY.addConverterForId(id, patrolConverter); ++ } ++ ++ MCTypeRegistry.ENTITY.addStructureConverter(new RootPositionConverter(VERSION, new RenamePair[] { ++ new RenamePair("Leash", "leash"), ++ })); ++ ++ ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:beehive", new RootPositionConverter(VERSION, new RenamePair[] { ++ new RenamePair("FlowerPos", "flower_pos"), ++ })); ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:end_gateway", new RootPositionConverter(VERSION, new RenamePair[] { ++ new RenamePair("ExitPortal", "exit_portal"), ++ })); ++ ++ MCTypeRegistry.SAVED_DATA_MAP_DATA.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType root, final long sourceVersion, final long toVersion) { ++ final MapType data = root.getMap("data"); ++ ++ if (data == null) { ++ return null; ++ } ++ ++ final ListType frames = data.getList("frames", ObjectType.MAP); ++ if (frames != null) { ++ for (int i = 0, len = frames.size(); i < len; ++i) { ++ final MapType frame = frames.getMap(i); ++ ++ V3807.flattenBlockPos(frame, "Pos"); ++ ++ RenameHelper.renameSingle(frame, "Pos", "pos"); ++ RenameHelper.renameSingle(frame, "Rotation", "rotation"); ++ RenameHelper.renameSingle(frame, "EntityId", "entity_id"); ++ } ++ } ++ ++ final ListType banners = data.getList("banners", ObjectType.MAP); ++ for (int i = 0, len = banners.size(); i < len; ++i) { ++ final MapType banner = banners.getMap(i); ++ ++ RenameHelper.renameSingle(banner, "Pos", "pos"); ++ RenameHelper.renameSingle(banner, "Color", "color"); ++ RenameHelper.renameSingle(banner, "Name", "name"); ++ } ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:compass", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType tag = data.getMap("tag"); ++ ++ if (tag == null) { ++ return null; ++ } ++ ++ V3807.flattenBlockPos(tag, "LodestonePos"); ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V3813() {} ++ ++ private static record RenamePair(String from, String to) {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3814.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3814.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d3b156129452202aea1f3b9f32142c91b64dc777 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3814.java +@@ -0,0 +1,21 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.attributes.ConverterAbstractAttributesRename; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V3814 { ++ ++ private static final int VERSION = MCVersions.V24W05B + 3; ++ ++ public static void register() { ++ final Map renames = new HashMap<>( ++ Map.of("minecraft:horse.jump_strength", "minecraft:generic.jump_strength") ++ ); ++ ++ ConverterAbstractAttributesRename.register(VERSION, renames::get); ++ } ++ ++ private V3814() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3816.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3816.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f50b81d931a1908d405bb72e0679983a742d5223 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3816.java +@@ -0,0 +1,14 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++ ++public final class V3816 { ++ ++ private static final int VERSION = MCVersions.V24W06A + 1; ++ ++ public static void register() { ++ V100.registerEquipment(VERSION, "minecraft:bogged"); ++ } ++ ++ private V3816() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3818.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3818.java +new file mode 100644 +index 0000000000000000000000000000000000000000..15257d47a2c449290e428402a9fa1e955f07e341 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3818.java +@@ -0,0 +1,339 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.converters.datatypes.DataWalker; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.custom.V3818_Commands; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemstack.ConverterItemStackToDataComponents; ++import ca.spottedleaf.dataconverter.minecraft.converters.particle.ConverterParticleToNBT; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.Types; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V3818 { ++ ++ private static final int VERSION = MCVersions.V24W07A + 1; ++ ++ private static final String[] BANNER_COLOURS = new String[] { ++ "white", ++ "orange", ++ "magenta", ++ "light_blue", ++ "yellow", ++ "lime", ++ "pink", ++ "gray", ++ "light_gray", ++ "cyan", ++ "purple", ++ "blue", ++ "brown", ++ "green", ++ "red", ++ "black", ++ }; ++ ++ public static String getBannerColour(final int id) { ++ return id >= 0 && id < BANNER_COLOURS.length ? BANNER_COLOURS[id] : BANNER_COLOURS[0]; ++ } ++ ++ public static void register() { ++ // Step 0 ++ // Note: No breakpoint needed, nothing nests hotbar ++ MCTypeRegistry.HOTBAR.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ for (final String key : data.keys()) { ++ final ListType itemList = data.getList(key, ObjectType.MAP); ++ if (itemList != null) { ++ for (int i = 0, len = itemList.size(); i < len; ++i) { ++ final MapType item = itemList.getMap(i); ++ ++ final String id = item.getString("id"); ++ final int count = item.getInt("Count"); ++ ++ if ("minecraft:air".equals(id) || count <= 0) { ++ itemList.setMap(i, item.getTypeUtil().createEmptyMap()); ++ } ++ } ++ } ++ } ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:beehive", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ RenameHelper.renameSingle(data, "Bees", "bees"); ++ ++ final ListType bees = data.getList("bees", ObjectType.MAP); ++ if (bees != null) { ++ for (int i = 0, len = bees.size(); i < len; ++i) { ++ final MapType bee = bees.getMap(i); ++ ++ RenameHelper.renameSingle(bee, "EntityData", "entity_data"); ++ RenameHelper.renameSingle(bee, "TicksInHive", "ticks_in_hive"); ++ RenameHelper.renameSingle(bee, "MinOccupationTicks", "min_ticks_in_hive"); ++ } ++ } ++ ++ return null; ++ } ++ }); ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:beehive", (final MapType root, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convertListPath(MCTypeRegistry.ENTITY, root, "bees", "entity_data", fromVersion, toVersion); ++ ++ return null; ++ }); ++ ++ // Step 1 ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:banner", new DataConverter<>(VERSION, 1) { ++ private static final Map PATTERN_UPDATE = new HashMap<>(); ++ static { ++ PATTERN_UPDATE.put("b", "minecraft:base"); ++ PATTERN_UPDATE.put("bl", "minecraft:square_bottom_left"); ++ PATTERN_UPDATE.put("br", "minecraft:square_bottom_right"); ++ PATTERN_UPDATE.put("tl", "minecraft:square_top_left"); ++ PATTERN_UPDATE.put("tr", "minecraft:square_top_right"); ++ PATTERN_UPDATE.put("bs", "minecraft:stripe_bottom"); ++ PATTERN_UPDATE.put("ts", "minecraft:stripe_top"); ++ PATTERN_UPDATE.put("ls", "minecraft:stripe_left"); ++ PATTERN_UPDATE.put("rs", "minecraft:stripe_right"); ++ PATTERN_UPDATE.put("cs", "minecraft:stripe_center"); ++ PATTERN_UPDATE.put("ms", "minecraft:stripe_middle"); ++ PATTERN_UPDATE.put("drs", "minecraft:stripe_downright"); ++ PATTERN_UPDATE.put("dls", "minecraft:stripe_downleft"); ++ PATTERN_UPDATE.put("ss", "minecraft:small_stripes"); ++ PATTERN_UPDATE.put("cr", "minecraft:cross"); ++ PATTERN_UPDATE.put("sc", "minecraft:straight_cross"); ++ PATTERN_UPDATE.put("bt", "minecraft:triangle_bottom"); ++ PATTERN_UPDATE.put("tt", "minecraft:triangle_top"); ++ PATTERN_UPDATE.put("bts", "minecraft:triangles_bottom"); ++ PATTERN_UPDATE.put("tts", "minecraft:triangles_top"); ++ PATTERN_UPDATE.put("ld", "minecraft:diagonal_left"); ++ PATTERN_UPDATE.put("rd", "minecraft:diagonal_up_right"); ++ PATTERN_UPDATE.put("lud", "minecraft:diagonal_up_left"); ++ PATTERN_UPDATE.put("rud", "minecraft:diagonal_right"); ++ PATTERN_UPDATE.put("mc", "minecraft:circle"); ++ PATTERN_UPDATE.put("mr", "minecraft:rhombus"); ++ PATTERN_UPDATE.put("vh", "minecraft:half_vertical"); ++ PATTERN_UPDATE.put("hh", "minecraft:half_horizontal"); ++ PATTERN_UPDATE.put("vhr", "minecraft:half_vertical_right"); ++ PATTERN_UPDATE.put("hhb", "minecraft:half_horizontal_bottom"); ++ PATTERN_UPDATE.put("bo", "minecraft:border"); ++ PATTERN_UPDATE.put("cbo", "minecraft:curly_border"); ++ PATTERN_UPDATE.put("gra", "minecraft:gradient"); ++ PATTERN_UPDATE.put("gru", "minecraft:gradient_up"); ++ PATTERN_UPDATE.put("bri", "minecraft:bricks"); ++ PATTERN_UPDATE.put("glb", "minecraft:globe"); ++ PATTERN_UPDATE.put("cre", "minecraft:creeper"); ++ PATTERN_UPDATE.put("sku", "minecraft:skull"); ++ PATTERN_UPDATE.put("flo", "minecraft:flower"); ++ PATTERN_UPDATE.put("moj", "minecraft:mojang"); ++ PATTERN_UPDATE.put("pig", "minecraft:piglin"); ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final ListType patterns = data.getList("Patterns", ObjectType.MAP); ++ if (patterns != null) { ++ for (int i = 0, len = patterns.size(); i < len; ++i) { ++ final MapType pattern = patterns.getMap(i); ++ ++ final String patternName = pattern.getString("Pattern"); ++ if (patternName != null) { ++ final String renamed = PATTERN_UPDATE.get(patternName); ++ if (renamed != null) { ++ pattern.setString("Pattern", renamed); ++ } ++ } ++ RenameHelper.renameSingle(pattern, "Pattern", "pattern"); ++ ++ final String newColour = getBannerColour(pattern.getInt("Color")); ++ pattern.setString("Color", newColour); ++ RenameHelper.renameSingle(pattern, "Color", "color"); ++ } ++ } ++ RenameHelper.renameSingle(data, "Patterns", "patterns"); ++ ++ return null; ++ } ++ }); ++ ++ // Step 2 ++ // Note: there is nothing after the previous breakpoint (1.19.4) that reads nested entity item ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:arrow", new DataConverter<>(VERSION, 2) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final Object potion = data.getGeneric("Potion"); ++ final Object customPotionEffects = data.getGeneric("custom_potion_effects"); ++ final Object color = data.getGeneric("Color"); ++ ++ if (potion == null && customPotionEffects == null && color == null) { ++ return null; ++ } ++ ++ data.remove("Potion"); ++ data.remove("custom_potion_effects"); ++ data.remove("Color"); ++ ++ final MapType item = data.getMap("item"); ++ if (item == null) { ++ return null; ++ } ++ ++ final MapType tag = item.getOrCreateMap("tag"); ++ ++ if (potion != null) { ++ tag.setGeneric("Potion", potion); ++ } ++ if (customPotionEffects != null) { ++ tag.setGeneric("custom_potion_effects", customPotionEffects); ++ } ++ if (color != null) { ++ tag.setGeneric("CustomPotionColor", color); ++ } ++ ++ return null; ++ } ++ }); ++ ++ // Step 3 ++ MCTypeRegistry.DATA_COMPONENTS.addStructureWalker(VERSION, new DataWalker<>() { ++ private static void walkBlockPredicates(final MapType root, final long fromVersion, final long toVersion) { ++ if (root.hasKey("blocks", ObjectType.STRING)) { ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, root, "blocks", fromVersion, toVersion); ++ } else if (root.hasKey("blocks", ObjectType.LIST)) { ++ WalkerUtils.convertList(MCTypeRegistry.BLOCK_NAME, root, "blocks", fromVersion, toVersion); ++ } ++ } ++ ++ @Override ++ public MapType walk(final MapType root, final long fromVersion, final long toVersion) { ++ WalkerUtils.convertListPath(MCTypeRegistry.ENTITY, root, "minecraft:bees", "entity_data", fromVersion, toVersion); ++ ++ WalkerUtils.convert(MCTypeRegistry.TILE_ENTITY, root, "minecraft:block_entity_data", fromVersion, toVersion); ++ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, root, "minecraft:bundle_contents", fromVersion, toVersion); ++ ++ final MapType canBreak = root.getMap("minecraft:can_break"); ++ if (canBreak != null) { ++ final ListType predicates = canBreak.getList("predicates", ObjectType.MAP); ++ if (predicates != null) { ++ for (int i = 0, len = predicates.size(); i < len; ++i) { ++ walkBlockPredicates(predicates.getMap(i), fromVersion, toVersion); ++ } ++ } ++ // Not handled by DFU: simple encoding does not require "predicates" ++ walkBlockPredicates(canBreak, fromVersion, toVersion); ++ } ++ ++ final MapType canPlaceOn = root.getMap("minecraft:can_break"); ++ if (canPlaceOn != null) { ++ final ListType predicates = canPlaceOn.getList("predicates", ObjectType.MAP); ++ if (predicates != null) { ++ for (int i = 0, len = predicates.size(); i < len; ++i) { ++ walkBlockPredicates(predicates.getMap(i), fromVersion, toVersion); ++ } ++ } ++ // Not handled by DFU: simple encoding does not require "predicates" ++ walkBlockPredicates(canPlaceOn, fromVersion, toVersion); ++ } ++ ++ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, root, "minecraft:charged_projectiles", fromVersion, toVersion); ++ WalkerUtils.convertListPath(MCTypeRegistry.ITEM_STACK, root, "minecraft:container", "item", fromVersion, toVersion); ++ WalkerUtils.convert(MCTypeRegistry.ENTITY, root, "minecraft:entity_data", fromVersion, toVersion); ++ WalkerUtils.convertList(MCTypeRegistry.ITEM_NAME, root, "minecraft:pot_decorations", fromVersion, toVersion); ++ ++ return null; ++ } ++ }); ++ ++ // Step 4 ++ MCTypeRegistry.PARTICLE.addStructureConverter(new DataConverter<>(VERSION, 4) { ++ @Override ++ public MapType convert(final Object input, final long sourceVersion, final long toVersion) { ++ if (!(input instanceof String flat)) { ++ return null; ++ } ++ ++ return ConverterParticleToNBT.convert(flat, Types.NBT); ++ } ++ }); ++ ++ MCTypeRegistry.PARTICLE.addStructureWalker(VERSION, 4, (final Object input, final long fromVersion, final long toVersion) -> { ++ if (!(input instanceof MapType)) { ++ return null; ++ } ++ ++ final MapType root = (MapType)input; ++ ++ WalkerUtils.convert(MCTypeRegistry.ITEM_STACK, root, "item", fromVersion, toVersion); ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_STATE, root, "block_state", fromVersion, toVersion); ++ ++ return null; ++ }); ++ ++ // Step 5 ++ // Note: needs breakpoint, reads nested tile entity data ++ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(VERSION, 5) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ return ConverterItemStackToDataComponents.convertItem(data); ++ } ++ }); ++ ++ MCTypeRegistry.ITEM_STACK.addStructureWalker(VERSION, 5, (final MapType root, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convert(MCTypeRegistry.ITEM_NAME, root, "id", fromVersion, toVersion); ++ WalkerUtils.convert(MCTypeRegistry.DATA_COMPONENTS, root, "components", fromVersion, toVersion); ++ ++ return null; ++ }); ++ ++ // Custom converter for converting commands inside signs, books, command blocks ++ V3818_Commands.register_5(); ++ ++ // Step 6 ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:area_effect_cloud", new DataConverter<>(VERSION, 6) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final Object color = data.getGeneric("Color"); ++ final Object effects = data.getGeneric("effects"); ++ final Object potion = data.getGeneric("Potion"); ++ ++ if (color == null && effects == null && potion == null) { ++ return null; ++ } ++ data.remove("Color"); ++ data.remove("effects"); ++ data.remove("Potion"); ++ ++ final MapType potionContents = data.getTypeUtil().createEmptyMap(); ++ data.setMap("potion_contents", potionContents); ++ ++ if (color != null) { ++ potionContents.setGeneric("custom_color", color); ++ } ++ ++ if (effects != null) { ++ potionContents.setGeneric("custom_effects", effects); ++ } ++ ++ if (potion != null) { ++ potionContents.setGeneric("potion", potion); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V3818() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3820.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3820.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0635b81924d4431fdfbcdd3814eaf01f85e53787 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3820.java +@@ -0,0 +1,78 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemstack.ConverterItemStackToDataComponents; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V3820 { ++ ++ private static final int VERSION = MCVersions.V24W09A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:skull", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final Object skullOwner = data.getGeneric("SkullOwner"); ++ final Object extraType = data.getGeneric("ExtraType"); ++ ++ if (skullOwner == null && extraType == null) { ++ return null; ++ } ++ ++ data.remove("SkullOwner"); ++ data.remove("ExtraType"); ++ ++ data.setMap( ++ "profile", ++ ConverterItemStackToDataComponents.convertProfile( ++ skullOwner == null ? extraType : skullOwner, data.getTypeUtil() ++ ) ++ ); ++ ++ return null; ++ } ++ }); ++ // I don't see why this converter is necessary, V3818 should have converted correctly... ++ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType components = data.getMap("components"); ++ ++ if (components == null) { ++ return null; ++ } ++ ++ final MapType oldTarget = components.getMap("minecraft:lodestone_target"); ++ if (oldTarget == null) { ++ return null; ++ } ++ ++ components.remove("minecraft:lodestone_target"); ++ components.setMap("minecraft:lodestone_tracker", oldTarget); ++ ++ final Object pos = oldTarget.getMap("pos"); ++ final Object dim = oldTarget.getMap("dimension"); ++ ++ if (pos == null && dim == null) { ++ return null; ++ } ++ ++ oldTarget.remove("pos"); ++ oldTarget.remove("dimension"); ++ ++ final MapType target = oldTarget.getTypeUtil().createEmptyMap(); ++ oldTarget.setMap("target", target); ++ ++ target.setGeneric("pos", pos); ++ target.setGeneric("dimension", dim); ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V3820() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3825.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3825.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cded86949c5af12aa96a8883b56b36b13d5dcd57 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3825.java +@@ -0,0 +1,155 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.util.ComponentUtils; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import java.util.Arrays; ++import java.util.HashSet; ++import java.util.Set; ++ ++public final class V3825 { ++ ++ private static final int VERSION = MCVersions.V24W12A + 1; ++ ++ private static final Set BANNER_NAMES = new HashSet<>( ++ Arrays.asList( ++ "block.minecraft.ominous_banner" ++ ) ++ ); ++ private static final Set MAP_NAMES = new HashSet<>( ++ Arrays.asList( ++ "filled_map.buried_treasure", ++ "filled_map.explorer_jungle", ++ "filled_map.explorer_swamp", ++ "filled_map.mansion", ++ "filled_map.monument", ++ "filled_map.trial_chambers", ++ "filled_map.village_desert", ++ "filled_map.village_plains", ++ "filled_map.village_savanna", ++ "filled_map.village_snowy", ++ "filled_map.village_taiga" ++ ) ++ ); ++ ++ public static void register() { ++ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(VERSION) { ++ private static void convertName(final MapType components, final Set standardNames) { ++ final String customName = components.getString("minecraft:custom_name"); ++ if (customName == null) { ++ return; ++ } ++ ++ final String translation = ComponentUtils.retrieveTranslationString(customName); ++ if (translation == null) { ++ return; ++ } ++ ++ if (standardNames.contains(translation)) { ++ components.remove("minecraft:custom_name"); ++ components.setString("minecraft:item_name", customName); ++ } ++ } ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType components = data.getMap("components"); ++ if (components == null) { ++ return null; ++ } ++ ++ final String id = data.getString("id"); ++ if (id == null) { ++ return null; ++ } ++ ++ switch (id) { ++ case "minecraft:white_banner": { ++ convertName(components, BANNER_NAMES); ++ break; ++ } ++ case "minecraft:filled_map": { ++ convertName(components, MAP_NAMES); ++ break; ++ } ++ } ++ ++ return null; ++ } ++ }); ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:banner", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final String customName = data.getString("CustomName"); ++ if (customName == null || !"block.minecraft.ominous_banner".equals(ComponentUtils.retrieveTranslationString(customName))) { ++ return null; ++ } ++ ++ data.remove("CustomName"); ++ ++ final MapType components = data.getOrCreateMap("components"); ++ ++ components.setString("minecraft:item_name", customName); ++ components.setMap("minecraft:hide_additional_tooltip", components.getTypeUtil().createEmptyMap()); ++ ++ return null; ++ } ++ }); ++ // DFU does not change the schema, even though it moves spawn_potentials ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:trial_spawner", (final MapType data, final long fromVersion, final long toVersion) -> { ++ final MapType normalConfig = data.getMap("normal_config"); ++ if (normalConfig != null) { ++ WalkerUtils.convertListPath(MCTypeRegistry.ENTITY, normalConfig, "spawn_potentials", "data", "entity", fromVersion, toVersion); ++ } ++ final MapType ominousConfig = data.getMap("ominous_config"); ++ if (ominousConfig != null) { ++ WalkerUtils.convertListPath(MCTypeRegistry.ENTITY, normalConfig, "spawn_potentials", "data", "entity", fromVersion, toVersion); ++ } ++ ++ WalkerUtils.convert(MCTypeRegistry.ENTITY, data.getMap("spawn_data"), "entity", fromVersion, toVersion); ++ return null; ++ }); ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:trial_spawner", new DataConverter<>(VERSION) { ++ private static final String[] NORMAL_CONFIG_KEYS = new String[] { ++ "spawn_range", ++ "total_mobs", ++ "simultaneous_mobs", ++ "total_mobs_added_per_player", ++ "simultaneous_mobs_added_per_player", ++ "ticks_between_spawn", ++ "spawn_potentials", ++ "loot_tables_to_eject", ++ "items_to_drop_when_ominous" ++ }; ++ ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType normalConfig = data.getTypeUtil().createEmptyMap(); ++ ++ for (final String normalKey : NORMAL_CONFIG_KEYS) { ++ final Object normalValue = data.getGeneric(normalKey); ++ if (normalValue != null) { ++ data.remove(normalKey); ++ normalConfig.setGeneric(normalKey, normalValue); ++ } ++ } ++ ++ if (!normalConfig.isEmpty()) { ++ data.setMap("normal_config", normalConfig); ++ } ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:ominous_item_spawner", new DataWalkerItems("item")); ++ } ++ ++ private V3825() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3828.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3828.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f752bb2fca2e4cd438c0540460912d4bc2c6f25e +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3828.java +@@ -0,0 +1,37 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.util.NamespaceUtil; ++ ++public final class V3828 { ++ ++ private static final int VERSION = MCVersions.V24W14A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.VILLAGER_TRADE.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType buyB = data.getMap("buyB"); ++ ++ if (buyB == null) { ++ return null; ++ } ++ ++ final String id = NamespaceUtil.correctNamespace(buyB.getString("id", "minecraft:air")); ++ final int count = buyB.getInt("count", 0); ++ ++ // Fix DFU: use count <= 0 instead of count == 0 ++ if ("minecraft:air".equals(id) || count <= 0) { ++ data.remove("buyB"); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V3828() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3833.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3833.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4581b9ce0734e552fd8810239e7b86f8b43513c5 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3833.java +@@ -0,0 +1,37 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.util.NamespaceUtil; ++ ++public final class V3833 { ++ ++ private static final int VERSION = MCVersions.V1_20_5_PRE4 + 1; ++ ++ public static void register() { ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:brushable_block", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType item = data.getMap("item"); ++ if (item == null) { ++ return null; ++ } ++ ++ ++ final String id = NamespaceUtil.correctNamespace(item.getString("id", "minecraft:air")); ++ final int count = item.getInt("count", 0); ++ ++ // Fix DFU: use count <= 0 instead of count == 0 ++ if ("minecraft:air".equals(id) || count <= 0) { ++ data.remove("item"); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V3833() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V501.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V501.java +new file mode 100644 +index 0000000000000000000000000000000000000000..a7a4d6446b7765ac485af82df660aafab05955bf +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V501.java +@@ -0,0 +1,18 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++ ++public final class V501 { ++ ++ private static final int VERSION = MCVersions.V16W20A; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ registerMob("PolarBear"); ++ } ++ ++ private V501() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V502.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V502.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7f88b435378305a3a66e1e54b85afd9b019513ee +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V502.java +@@ -0,0 +1,45 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import java.util.concurrent.ThreadLocalRandom; ++ ++public final class V502 { ++ ++ private static final int VERSION = MCVersions.V16W20A + 1; ++ ++ public static void register() { ++ ConverterAbstractItemRename.register(VERSION, (final String name) -> { ++ return "minecraft:cooked_fished".equals(name) ? "minecraft:cooked_fish" : null; ++ }); ++ MCTypeRegistry.ENTITY.addConverterForId("Zombie", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (!data.getBoolean("IsVillager")) { ++ return null; ++ } ++ ++ data.remove("IsVillager"); ++ ++ if (data.hasKey("ZombieType")) { ++ return null; ++ } ++ ++ int type = data.getInt("VillagerProfession", -1); ++ // Vanilla doesn't remove the profession tag, so we don't! ++ if (type < 0 || type >= 6) { ++ type = ThreadLocalRandom.current().nextInt(6); ++ } ++ ++ data.setInt("ZombieType", type); ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V502() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V505.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V505.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3faf2c3265600141003355771f38a7879e0f769a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V505.java +@@ -0,0 +1,23 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V505 { ++ ++ private static final int VERSION = MCVersions.V16W21B + 1; ++ ++ public static void register() { ++ MCTypeRegistry.OPTIONS.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ data.setString("useVbo", "true"); ++ return null; ++ } ++ }); ++ } ++ ++ private V505() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V700.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V700.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3b65108c6a1ac469bb8f81a933b6475f3ea9f63f +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V700.java +@@ -0,0 +1,32 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V700 { ++ ++ private static final int VERSION = MCVersions.V1_10_2 + 188; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("Guardian", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (data.getBoolean("Elder")) { ++ data.setString("id", "ElderGuardian"); ++ } ++ data.remove("Elder"); ++ return null; ++ } ++ }); ++ ++ registerMob("ElderGuardian"); ++ } ++ ++ private V700() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V701.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V701.java +new file mode 100644 +index 0000000000000000000000000000000000000000..55f00e218f04e1e095ccc7d62282d87d7eb8f8c7 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V701.java +@@ -0,0 +1,41 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V701 { ++ ++ private static final int VERSION = MCVersions.V1_10_2 + 189; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("Skeleton", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final int type = data.getInt("SkeletonType"); ++ data.remove("SkeletonType"); ++ ++ switch (type) { ++ case 1: ++ data.setString("id", "WitherSkeleton"); ++ break; ++ case 2: ++ data.setString("id", "Stray"); ++ break; ++ } ++ ++ return null; ++ } ++ }); ++ ++ registerMob("WitherSkeleton"); ++ registerMob("Stray"); ++ } ++ ++ private V701() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V702.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V702.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c0d74b4822be60c637f26b2ef1e172fdf9e89d01 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V702.java +@@ -0,0 +1,56 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V702 { ++ ++ private static final int VERSION = MCVersions.V1_10_2 + 190; ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("Zombie", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final int zombieType = data.getInt("ZombieType"); ++ data.remove("ZombieType"); ++ ++ switch (zombieType) { ++ case 0: ++ default: ++ break; ++ ++ case 1: ++ case 2: ++ case 3: ++ case 4: ++ case 5: ++ data.setString("id", "ZombieVillager"); ++ data.setInt("Profession", zombieType - 1); ++ break; ++ ++ case 6: ++ data.setString("id", "Husk"); ++ break; ++ } ++ ++ return null; ++ } ++ }); ++ ++ registerMob("ZombieVillager"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "ZombieVillager", (final MapType root, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convertList(MCTypeRegistry.VILLAGER_TRADE, root.getMap("Offers"), "Recipes", fromVersion, toVersion); ++ return null; ++ }); ++ registerMob( "Husk"); ++ } ++ ++ private V702() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V703.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V703.java +new file mode 100644 +index 0000000000000000000000000000000000000000..cc593df4a09d6cb93196d8cfb34ebac43e61ebbe +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V703.java +@@ -0,0 +1,67 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V703 { ++ ++ private static final int VERSION = MCVersions.V1_10_2 + 191; ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("EntityHorse", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final int type = data.getInt("Type"); ++ data.remove("Type"); ++ ++ switch (type) { ++ case 0: ++ default: ++ data.setString("id", "Horse"); ++ break; ++ ++ case 1: ++ data.setString("id", "Donkey"); ++ break; ++ ++ case 2: ++ data.setString("id", "Mule"); ++ break; ++ ++ case 3: ++ data.setString("id", "ZombieHorse"); ++ break; ++ ++ case 4: ++ data.setString("id", "SkeletonHorse"); ++ break; ++ } ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "Horse", new DataWalkerItems("ArmorItem", "SaddleItem")); ++ V100.registerEquipment(VERSION, "Horse"); ++ ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "Donkey", new DataWalkerItems("SaddleItem")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "Donkey", new DataWalkerItemLists("Items")); ++ V100.registerEquipment(VERSION, "Donkey"); ++ ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "Mule", new DataWalkerItems("SaddleItem")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "Mule", new DataWalkerItemLists("Items")); ++ V100.registerEquipment(VERSION, "Mule"); ++ ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "ZombieHorse", new DataWalkerItems("SaddleItem")); ++ V100.registerEquipment(VERSION, "ZombieHorse"); ++ ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "SkeletonHorse", new DataWalkerItems("SaddleItem")); ++ V100.registerEquipment(VERSION, "SkeletonHorse"); ++ } ++ ++ private V703() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V704.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V704.java +new file mode 100644 +index 0000000000000000000000000000000000000000..10ee10109405bd569beb23a235a7411db45d6ec0 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V704.java +@@ -0,0 +1,432 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.hooks.DataHookEnforceNamespacedID; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++import ca.spottedleaf.dataconverter.minecraft.walkers.item_name.DataWalkerItemNames; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.util.Long2ObjectArraySortedMap; ++import com.mojang.logging.LogUtils; ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.world.item.BlockItem; ++import net.minecraft.world.item.Item; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.EntityBlock; ++import net.minecraft.world.level.block.entity.BlockEntity; ++import net.minecraft.world.level.block.entity.BlockEntityType; ++import org.slf4j.Logger; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V704 { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ private static final int VERSION = MCVersions.V1_10_2 + 192; ++ ++ public static final Map ITEM_ID_TO_TILE_ENTITY_ID = new HashMap<>() { ++ @Override ++ public String put(final String key, final String value) { ++ if (this.containsKey(key)) { ++ LOGGER.error("Duplicate item id to tile key: " + key); ++ throw new RuntimeException(); // only devs should see the consequence of this... at least start up the damn thing... ++ } ++ return super.put(key, value); ++ } ++ }; ++ static { ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:furnace", "minecraft:furnace"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:lit_furnace", "minecraft:furnace"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:chest", "minecraft:chest"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:trapped_chest", "minecraft:chest"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:ender_chest", "minecraft:ender_chest"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:jukebox", "minecraft:jukebox"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:dispenser", "minecraft:dispenser"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:dropper", "minecraft:dropper"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:mob_spawner", "minecraft:mob_spawner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:spawner", "minecraft:mob_spawner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:noteblock", "minecraft:noteblock"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:brewing_stand", "minecraft:brewing_stand"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:enhanting_table", "minecraft:enchanting_table"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:command_block", "minecraft:command_block"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:beacon", "minecraft:beacon"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:skull", "minecraft:skull"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:daylight_detector", "minecraft:daylight_detector"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:hopper", "minecraft:hopper"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:banner", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:flower_pot", "minecraft:flower_pot"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:repeating_command_block", "minecraft:command_block"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:chain_command_block", "minecraft:command_block"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:shulker_box", "minecraft:shulker_box"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:white_shulker_box", "minecraft:shulker_box"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:orange_shulker_box", "minecraft:shulker_box"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:magenta_shulker_box", "minecraft:shulker_box"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:light_blue_shulker_box", "minecraft:shulker_box"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:yellow_shulker_box", "minecraft:shulker_box"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:lime_shulker_box", "minecraft:shulker_box"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:pink_shulker_box", "minecraft:shulker_box"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:gray_shulker_box", "minecraft:shulker_box"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:silver_shulker_box", "minecraft:shulker_box"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:cyan_shulker_box", "minecraft:shulker_box"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:purple_shulker_box", "minecraft:shulker_box"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:blue_shulker_box", "minecraft:shulker_box"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:brown_shulker_box", "minecraft:shulker_box"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:green_shulker_box", "minecraft:shulker_box"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:red_shulker_box", "minecraft:shulker_box"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:black_shulker_box", "minecraft:shulker_box"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:bed", "minecraft:bed"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:light_gray_shulker_box", "minecraft:shulker_box"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:white_banner", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:orange_banner", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:magenta_banner", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:light_blue_banner", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:yellow_banner", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:lime_banner", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:pink_banner", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:gray_banner", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:silver_banner", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:cyan_banner", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:purple_banner", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:blue_banner", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:brown_banner", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:green_banner", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:red_banner", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:black_banner", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:standing_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:wall_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:piston_head", "minecraft:piston"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:daylight_detector_inverted", "minecraft:daylight_detector"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:unpowered_comparator", "minecraft:comparator"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:powered_comparator", "minecraft:comparator"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:wall_banner", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:standing_banner", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:structure_block", "minecraft:structure_block"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:end_portal", "minecraft:end_portal"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:end_gateway", "minecraft:end_gateway"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:shield", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:white_bed", "minecraft:bed"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:orange_bed", "minecraft:bed"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:magenta_bed", "minecraft:bed"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:light_blue_bed", "minecraft:bed"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:yellow_bed", "minecraft:bed"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:lime_bed", "minecraft:bed"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:pink_bed", "minecraft:bed"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:gray_bed", "minecraft:bed"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:silver_bed", "minecraft:bed"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:cyan_bed", "minecraft:bed"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:purple_bed", "minecraft:bed"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:blue_bed", "minecraft:bed"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:brown_bed", "minecraft:bed"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:green_bed", "minecraft:bed"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:red_bed", "minecraft:bed"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:black_bed", "minecraft:bed"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:oak_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:spruce_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:birch_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:jungle_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:acacia_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:dark_oak_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:crimson_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:warped_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:skeleton_skull", "minecraft:skull"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:wither_skeleton_skull", "minecraft:skull"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:zombie_head", "minecraft:skull"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:player_head", "minecraft:skull"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:creeper_head", "minecraft:skull"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:dragon_head", "minecraft:skull"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:barrel", "minecraft:barrel"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:conduit", "minecraft:conduit"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:smoker", "minecraft:smoker"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:blast_furnace", "minecraft:blast_furnace"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:lectern", "minecraft:lectern"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:bell", "minecraft:bell"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:jigsaw", "minecraft:jigsaw"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:campfire", "minecraft:campfire"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:bee_nest", "minecraft:beehive"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:beehive", "minecraft:beehive"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:sculk_sensor", "minecraft:sculk_sensor"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:decorated_pot", "minecraft:decorated_pot"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:crafter", "minecraft:crafter"); ++ ++ // These are missing from Vanilla up to 1.20.5 ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:enchanting_table", "minecraft:enchanting_table"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:comparator", "minecraft:comparator"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:light_gray_bed", "minecraft:bed"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:light_gray_banner", "minecraft:banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:soul_campfire", "minecraft:campfire"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:sculk_catalyst", "minecraft:sculk_catalyst"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:mangrove_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:sculk_shrieker", "minecraft:sculk_shrieker"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:chiseled_bookshelf", "minecraft:chiseled_bookshelf"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:bamboo_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:oak_hanging_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:spruce_hanging_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:birch_hanging_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:jungle_hanging_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:acacia_hanging_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:dark_oak_hanging_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:mangrove_hanging_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:bamboo_hanging_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:crimson_hanging_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:warped_hanging_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:piglin_head", "minecraft:skull"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:suspicious_sand", "minecraft:brushable_block"); // note: this was renamed in the past, see special case in the itemstack walker ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:cherry_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:cherry_hanging_sign", "minecraft:sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:suspicious_gravel", "minecraft:brushable_block"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:calibrated_sculk_sensor", "minecraft:calibrated_sculk_sensor"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:trial_spawner", "minecraft:trial_spawner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:vault", "minecraft:vault"); ++ } ++ ++ // This class is responsible for also integrity checking the item id to tile id map here, we just use the item registry to figure it out ++ // No longer need to do this, as items now use components in 1.20.5 ++ /*static { ++ for (final Item item : BuiltInRegistries.ITEM) { ++ if (!(item instanceof BlockItem)) { ++ continue; ++ } ++ ++ if (!(((BlockItem)item).getBlock() instanceof EntityBlock entityBlock)) { ++ continue; ++ } ++ ++ String possibleId; ++ try { ++ final BlockEntity entity = entityBlock.newBlockEntity(new BlockPos(0, 0, 0), ((Block)entityBlock).defaultBlockState()); ++ if (entity != null) { ++ possibleId = BlockEntityType.getKey(entity.getType()).toString(); ++ } else { ++ possibleId = null; ++ } ++ } catch (final Throwable th) { ++ possibleId = null; ++ } ++ ++ final String itemName = BuiltInRegistries.ITEM.getKey(item).toString(); ++ final String mappedTo = ITEM_ID_TO_TILE_ENTITY_ID.get(itemName); ++ if (mappedTo == null) { ++ LOGGER.error("Item id " + itemName + " does not contain tile mapping! (V704)"); ++ } else if (possibleId != null && !mappedTo.equals(possibleId)) { ++ final boolean chestCase = mappedTo.equals("minecraft:chest") && possibleId.equals("minecraft:trapped_chest"); ++ final boolean signCase = mappedTo.equals("minecraft:sign") && possibleId.equals("minecraft:hanging_sign"); ++ // save data is identical for the chest and sign case, so we don't care ++ // it's also important to note that there is no versioning for this map, so it is possible ++ // that mapping them correctly could cause issues converting old data ++ if (!chestCase && !signCase) { ++ LOGGER.error("Item id " + itemName + " is mapped to the wrong tile entity! Mapped to: " + mappedTo + ", expected: " + possibleId); ++ } ++ } ++ } ++ }*/ ++ ++ private static Long2ObjectArraySortedMap makeSingle(final long k1, final String v1) { ++ final Long2ObjectArraySortedMap ret = new Long2ObjectArraySortedMap<>(); ++ ++ ret.put(k1, v1); ++ ++ return ret; ++ } ++ ++ private static Long2ObjectArraySortedMap makeDouble(final long k1, final String v1, ++ final long k2, final String v2) { ++ final Long2ObjectArraySortedMap ret = new Long2ObjectArraySortedMap<>(); ++ ++ ret.put(k1, v1); ++ ret.put(k2, v2); ++ ++ return ret; ++ } ++ ++ private static final Map> ITEM_ID_TO_ENTITY_ID = new HashMap<>(); ++ static { ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:armor_stand", makeDouble(V99.VERSION, "ArmorStand", V705.VERSION, "minecraft:armor_stand")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:painting", makeDouble(V99.VERSION, "Painting", V705.VERSION, "minecraft:painting")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:boat", makeDouble(V99.VERSION, "Boat", V705.VERSION, "minecraft:boat")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:oak_boat", makeSingle(V705.VERSION, "minecraft:boat")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:oak_chest_boat", makeSingle(V705.VERSION, "minecraft:chest_boat")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:spruce_boat",makeSingle(V705.VERSION, "minecraft:boat")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:spruce_chest_boat", makeSingle(V705.VERSION, "minecraft:chest_boat")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:birch_boat", makeSingle(V705.VERSION, "minecraft:boat")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:birch_chest_boat", makeSingle(V705.VERSION, "minecraft:chest_boat")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:jungle_boat", makeSingle(V705.VERSION, "minecraft:boat")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:jungle_chest_boat", makeSingle(V705.VERSION, "minecraft:chest_boat")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:acacia_boat", makeSingle(V705.VERSION, "minecraft:boat")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:acacia_chest_boat", makeSingle(V705.VERSION, "minecraft:chest_boat")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:cherry_boat", makeSingle(V705.VERSION, "minecraft:boat")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:cherry_chest_boat", makeSingle(V705.VERSION, "minecraft:chest_boat")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:dark_oak_boat", makeSingle(V705.VERSION, "minecraft:boat")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:dark_oak_chest_boat", makeSingle(V705.VERSION, "minecraft:chest_boat")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:mangrove_boat", makeSingle(V705.VERSION, "minecraft:boat")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:mangrove_chest_boat", makeSingle(V705.VERSION, "minecraft:chest_boat")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:bamboo_raft", makeSingle(V705.VERSION, "minecraft:boat")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:bamboo_chest_raft", makeSingle(V705.VERSION, "minecraft:chest_boat")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:minecart", makeDouble(V99.VERSION, "MinecartRideable", V705.VERSION, "minecraft:minecart")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:chest_minecart", makeDouble(V99.VERSION, "MinecartChest", V705.VERSION, "minecraft:chest_minecart")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:furnace_minecart", makeDouble(V99.VERSION, "MinecartFurnace", V705.VERSION, "minecraft:furnace_minecart")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:tnt_minecart", makeDouble(V99.VERSION, "MinecartTNT", V705.VERSION, "minecraft:tnt_minecart")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:hopper_minecart", makeDouble(V99.VERSION, "MinecartHopper", V705.VERSION, "minecraft:hopper_minecart")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:item_frame", makeDouble(V99.VERSION, "ItemFrame", V705.VERSION, "minecraft:item_frame")); ++ ITEM_ID_TO_ENTITY_ID.put("minecraft:glow_item_frame", makeSingle(V705.VERSION, "minecraft:glow_item_frame")); ++ } ++ ++ private static final Map TILE_ID_UPDATE = new HashMap<>(); ++ static { ++ TILE_ID_UPDATE.put("Airportal", "minecraft:end_portal"); ++ TILE_ID_UPDATE.put("Banner", "minecraft:banner"); ++ TILE_ID_UPDATE.put("Beacon", "minecraft:beacon"); ++ TILE_ID_UPDATE.put("Cauldron", "minecraft:brewing_stand"); ++ TILE_ID_UPDATE.put("Chest", "minecraft:chest"); ++ TILE_ID_UPDATE.put("Comparator", "minecraft:comparator"); ++ TILE_ID_UPDATE.put("Control", "minecraft:command_block"); ++ TILE_ID_UPDATE.put("DLDetector", "minecraft:daylight_detector"); ++ TILE_ID_UPDATE.put("Dropper", "minecraft:dropper"); ++ TILE_ID_UPDATE.put("EnchantTable", "minecraft:enchanting_table"); ++ TILE_ID_UPDATE.put("EndGateway", "minecraft:end_gateway"); ++ TILE_ID_UPDATE.put("EnderChest", "minecraft:ender_chest"); ++ TILE_ID_UPDATE.put("FlowerPot", "minecraft:flower_pot"); ++ TILE_ID_UPDATE.put("Furnace", "minecraft:furnace"); ++ TILE_ID_UPDATE.put("Hopper", "minecraft:hopper"); ++ TILE_ID_UPDATE.put("MobSpawner", "minecraft:mob_spawner"); ++ TILE_ID_UPDATE.put("Music", "minecraft:noteblock"); ++ TILE_ID_UPDATE.put("Piston", "minecraft:piston"); ++ TILE_ID_UPDATE.put("RecordPlayer", "minecraft:jukebox"); ++ TILE_ID_UPDATE.put("Sign", "minecraft:sign"); ++ TILE_ID_UPDATE.put("Skull", "minecraft:skull"); ++ TILE_ID_UPDATE.put("Structure", "minecraft:structure_block"); ++ TILE_ID_UPDATE.put("Trap", "minecraft:dispenser"); ++ } ++ ++ private static void registerInventory(final String id) { ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, id, new DataWalkerItemLists("Items")); ++ } ++ ++ public static void register() { ++ MCTypeRegistry.TILE_ENTITY.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final String id = data.getString("id"); ++ if (id == null) { ++ return null; ++ } ++ ++ data.setString("id", TILE_ID_UPDATE.getOrDefault(id, id)); ++ return null; ++ } ++ }); ++ ++ ++ MCTypeRegistry.TILE_ENTITY.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convert(MCTypeRegistry.DATA_COMPONENTS, data, "components", fromVersion, toVersion); ++ return null; ++ }); ++ registerInventory( "minecraft:furnace"); ++ registerInventory( "minecraft:chest"); ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:jukebox", new DataWalkerItems("RecordItem")); ++ registerInventory("minecraft:dispenser"); ++ registerInventory("minecraft:dropper"); ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:mob_spawner", (final MapType data, final long fromVersion, final long toVersion) -> { ++ MCTypeRegistry.UNTAGGED_SPAWNER.convert(data, fromVersion, toVersion); ++ return null; ++ }); ++ registerInventory("minecraft:brewing_stand"); ++ registerInventory("minecraft:hopper"); ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:flower_pot", new DataWalkerItemNames("Item")); ++ MCTypeRegistry.TILE_ENTITY.addWalker( ++ VERSION, "minecraft:command_block", ++ new DataWalkerTypePaths<>(MCTypeRegistry.DATACONVERTER_CUSTOM_TYPE_COMMAND, "Command") ++ ); ++ ++ MCTypeRegistry.ITEM_STACK.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convert(MCTypeRegistry.ITEM_NAME, data, "id", fromVersion, toVersion); ++ ++ final MapType tag = data.getMap("tag"); ++ if (tag == null) { ++ return null; ++ } ++ ++ // only things here are in tag, if changed update if above ++ ++ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, tag, "Items", fromVersion, toVersion); ++ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, tag, "ChargedProjectiles", fromVersion, toVersion); ++ ++ MapType entityTag = tag.getMap("EntityTag"); ++ if (entityTag != null) { ++ final String itemId = data.getString("id"); ++ final String entityId; ++ if (itemId != null && itemId.contains("_spawn_egg")) { ++ // V1451 changes spawn eggs to have the sub entity id be a part of the item id, but of course Mojang never ++ // bothered to write in logic to set the sub entity id, so we have to. ++ // format is ALWAYS :_spawn_egg post flattening ++ entityId = itemId.substring(0, itemId.indexOf("_spawn_egg")); ++ } else { ++ final Long2ObjectArraySortedMap mappingByVersion = ITEM_ID_TO_ENTITY_ID.get(itemId); ++ final String mapped = mappingByVersion == null ? null : mappingByVersion.getFloor(fromVersion); ++ entityId = mapped == null ? entityTag.getString("id") : mapped; ++ } ++ ++ if (entityId == null) { ++ if (!"minecraft:air".equals(itemId)) { ++ LOGGER.warn("Unable to resolve Entity for ItemStack (V704): " + itemId); ++ } ++ } else { ++ if (!entityTag.hasKey("id", ObjectType.STRING)) { ++ entityTag.setString("id", entityId); ++ } ++ } ++ ++ final MapType replace = MCTypeRegistry.ENTITY.convert(entityTag, fromVersion, toVersion); ++ ++ if (replace != null) { ++ entityTag = replace; ++ tag.setMap("EntityTag", entityTag); ++ } ++ } ++ ++ MapType blockEntityTag = tag.getMap("BlockEntityTag"); ++ if (blockEntityTag != null) { ++ final String itemId = data.getString("id"); ++ final String entityId; ++ if ("minecraft:suspicious_sand".equals(itemId) && fromVersion < V3438.VERSION) { ++ // renamed after this version, and since the id is a mapping to just string we need to special case this ++ entityId = "minecraft:suspicious_sand"; ++ } else { ++ entityId = ITEM_ID_TO_TILE_ENTITY_ID.get(itemId); ++ } ++ ++ if (entityId == null) { ++ if (!"minecraft:air".equals(itemId)) { ++ LOGGER.warn("Unable to resolve BlockEntity for ItemStack (V704): " + itemId); ++ } ++ } else { ++ if (!blockEntityTag.hasKey("id", ObjectType.STRING)) { ++ blockEntityTag.setString("id", entityId); ++ } ++ } ++ final MapType replace = MCTypeRegistry.TILE_ENTITY.convert(blockEntityTag, fromVersion, toVersion); ++ if (replace != null) { ++ blockEntityTag = replace; ++ tag.setMap("BlockEntityTag", blockEntityTag); ++ } ++ } ++ ++ WalkerUtils.convertList(MCTypeRegistry.BLOCK_NAME, tag, "CanDestroy", fromVersion, toVersion); ++ WalkerUtils.convertList(MCTypeRegistry.BLOCK_NAME, tag, "CanPlaceOn", fromVersion, toVersion); ++ ++ return null; ++ }); ++ ++ // Enforce namespace for ids ++ MCTypeRegistry.TILE_ENTITY.addStructureHook(VERSION, new DataHookEnforceNamespacedID()); ++ } ++ ++ private V704() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V705.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V705.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e0efac6a303d4c9623e03acdf07f89c2cacc9f04 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V705.java +@@ -0,0 +1,221 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterAbstractEntityRename; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.hooks.DataHookEnforceNamespacedID; ++import ca.spottedleaf.dataconverter.minecraft.hooks.DataHookValueTypeEnforceNamespaced; ++import ca.spottedleaf.dataconverter.minecraft.walkers.block_name.DataWalkerBlockNames; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; ++import ca.spottedleaf.dataconverter.minecraft.walkers.tile_entity.DataWalkerTileEntities; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.MapType; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V705 { ++ ++ public static final int VERSION = MCVersions.V1_10_2 + 193; ++ ++ private static final Map ENTITY_ID_UPDATE = new HashMap<>(); ++ static { ++ ENTITY_ID_UPDATE.put("AreaEffectCloud", "minecraft:area_effect_cloud"); ++ ENTITY_ID_UPDATE.put("ArmorStand", "minecraft:armor_stand"); ++ ENTITY_ID_UPDATE.put("Arrow", "minecraft:arrow"); ++ ENTITY_ID_UPDATE.put("Bat", "minecraft:bat"); ++ ENTITY_ID_UPDATE.put("Blaze", "minecraft:blaze"); ++ ENTITY_ID_UPDATE.put("Boat", "minecraft:boat"); ++ ENTITY_ID_UPDATE.put("CaveSpider", "minecraft:cave_spider"); ++ ENTITY_ID_UPDATE.put("Chicken", "minecraft:chicken"); ++ ENTITY_ID_UPDATE.put("Cow", "minecraft:cow"); ++ ENTITY_ID_UPDATE.put("Creeper", "minecraft:creeper"); ++ ENTITY_ID_UPDATE.put("Donkey", "minecraft:donkey"); ++ ENTITY_ID_UPDATE.put("DragonFireball", "minecraft:dragon_fireball"); ++ ENTITY_ID_UPDATE.put("ElderGuardian", "minecraft:elder_guardian"); ++ ENTITY_ID_UPDATE.put("EnderCrystal", "minecraft:ender_crystal"); ++ ENTITY_ID_UPDATE.put("EnderDragon", "minecraft:ender_dragon"); ++ ENTITY_ID_UPDATE.put("Enderman", "minecraft:enderman"); ++ ENTITY_ID_UPDATE.put("Endermite", "minecraft:endermite"); ++ ENTITY_ID_UPDATE.put("EyeOfEnderSignal", "minecraft:eye_of_ender_signal"); ++ ENTITY_ID_UPDATE.put("FallingSand", "minecraft:falling_block"); ++ ENTITY_ID_UPDATE.put("Fireball", "minecraft:fireball"); ++ ENTITY_ID_UPDATE.put("FireworksRocketEntity", "minecraft:fireworks_rocket"); ++ ENTITY_ID_UPDATE.put("Ghast", "minecraft:ghast"); ++ ENTITY_ID_UPDATE.put("Giant", "minecraft:giant"); ++ ENTITY_ID_UPDATE.put("Guardian", "minecraft:guardian"); ++ ENTITY_ID_UPDATE.put("Horse", "minecraft:horse"); ++ ENTITY_ID_UPDATE.put("Husk", "minecraft:husk"); ++ ENTITY_ID_UPDATE.put("Item", "minecraft:item"); ++ ENTITY_ID_UPDATE.put("ItemFrame", "minecraft:item_frame"); ++ ENTITY_ID_UPDATE.put("LavaSlime", "minecraft:magma_cube"); ++ ENTITY_ID_UPDATE.put("LeashKnot", "minecraft:leash_knot"); ++ ENTITY_ID_UPDATE.put("MinecartChest", "minecraft:chest_minecart"); ++ ENTITY_ID_UPDATE.put("MinecartCommandBlock", "minecraft:commandblock_minecart"); ++ ENTITY_ID_UPDATE.put("MinecartFurnace", "minecraft:furnace_minecart"); ++ ENTITY_ID_UPDATE.put("MinecartHopper", "minecraft:hopper_minecart"); ++ ENTITY_ID_UPDATE.put("MinecartRideable", "minecraft:minecart"); ++ ENTITY_ID_UPDATE.put("MinecartSpawner", "minecraft:spawner_minecart"); ++ ENTITY_ID_UPDATE.put("MinecartTNT", "minecraft:tnt_minecart"); ++ ENTITY_ID_UPDATE.put("Mule", "minecraft:mule"); ++ ENTITY_ID_UPDATE.put("MushroomCow", "minecraft:mooshroom"); ++ ENTITY_ID_UPDATE.put("Ozelot", "minecraft:ocelot"); ++ ENTITY_ID_UPDATE.put("Painting", "minecraft:painting"); ++ ENTITY_ID_UPDATE.put("Pig", "minecraft:pig"); ++ ENTITY_ID_UPDATE.put("PigZombie", "minecraft:zombie_pigman"); ++ ENTITY_ID_UPDATE.put("PolarBear", "minecraft:polar_bear"); ++ ENTITY_ID_UPDATE.put("PrimedTnt", "minecraft:tnt"); ++ ENTITY_ID_UPDATE.put("Rabbit", "minecraft:rabbit"); ++ ENTITY_ID_UPDATE.put("Sheep", "minecraft:sheep"); ++ ENTITY_ID_UPDATE.put("Shulker", "minecraft:shulker"); ++ ENTITY_ID_UPDATE.put("ShulkerBullet", "minecraft:shulker_bullet"); ++ ENTITY_ID_UPDATE.put("Silverfish", "minecraft:silverfish"); ++ ENTITY_ID_UPDATE.put("Skeleton", "minecraft:skeleton"); ++ ENTITY_ID_UPDATE.put("SkeletonHorse", "minecraft:skeleton_horse"); ++ ENTITY_ID_UPDATE.put("Slime", "minecraft:slime"); ++ ENTITY_ID_UPDATE.put("SmallFireball", "minecraft:small_fireball"); ++ ENTITY_ID_UPDATE.put("SnowMan", "minecraft:snowman"); ++ ENTITY_ID_UPDATE.put("Snowball", "minecraft:snowball"); ++ ENTITY_ID_UPDATE.put("SpectralArrow", "minecraft:spectral_arrow"); ++ ENTITY_ID_UPDATE.put("Spider", "minecraft:spider"); ++ ENTITY_ID_UPDATE.put("Squid", "minecraft:squid"); ++ ENTITY_ID_UPDATE.put("Stray", "minecraft:stray"); ++ ENTITY_ID_UPDATE.put("ThrownEgg", "minecraft:egg"); ++ ENTITY_ID_UPDATE.put("ThrownEnderpearl", "minecraft:ender_pearl"); ++ ENTITY_ID_UPDATE.put("ThrownExpBottle", "minecraft:xp_bottle"); ++ ENTITY_ID_UPDATE.put("ThrownPotion", "minecraft:potion"); ++ ENTITY_ID_UPDATE.put("Villager", "minecraft:villager"); ++ ENTITY_ID_UPDATE.put("VillagerGolem", "minecraft:villager_golem"); ++ ENTITY_ID_UPDATE.put("Witch", "minecraft:witch"); ++ ENTITY_ID_UPDATE.put("WitherBoss", "minecraft:wither"); ++ ENTITY_ID_UPDATE.put("WitherSkeleton", "minecraft:wither_skeleton"); ++ ENTITY_ID_UPDATE.put("WitherSkull", "minecraft:wither_skull"); ++ ENTITY_ID_UPDATE.put("Wolf", "minecraft:wolf"); ++ ENTITY_ID_UPDATE.put("XPOrb", "minecraft:xp_orb"); ++ ENTITY_ID_UPDATE.put("Zombie", "minecraft:zombie"); ++ ENTITY_ID_UPDATE.put("ZombieHorse", "minecraft:zombie_horse"); ++ ENTITY_ID_UPDATE.put("ZombieVillager", "minecraft:zombie_villager"); ++ } ++ ++ private static void registerMob(final String id) { ++ V100.registerEquipment(VERSION, id); ++ } ++ ++ private static void registerThrowableProjectile(final String id) { ++ MCTypeRegistry.ENTITY.addWalker(VERSION, id, new DataWalkerBlockNames("inTile")); ++ } ++ ++ public static void register() { ++ ConverterAbstractEntityRename.register(VERSION, ENTITY_ID_UPDATE::get); ++ ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:area_effect_cloud", new DataWalkerTypePaths<>(MCTypeRegistry.PARTICLE, "Particle")); ++ registerMob("minecraft:armor_stand"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:arrow", new DataWalkerBlockNames("inTile")); ++ registerMob("minecraft:bat"); ++ registerMob("minecraft:blaze"); ++ registerMob("minecraft:cave_spider"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:chest_minecart", new DataWalkerBlockNames("DisplayTile")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:chest_minecart", new DataWalkerItemLists("Items")); ++ registerMob("minecraft:chicken"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:commandblock_minecart", new DataWalkerBlockNames("DisplayTile")); ++ registerMob("minecraft:cow"); ++ registerMob("minecraft:creeper"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:donkey", new DataWalkerItemLists("Items")); ++ V100.registerEquipment(VERSION, "minecraft:donkey"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:donkey", new DataWalkerItems("SaddleItem")); ++ registerThrowableProjectile("minecraft:egg"); ++ registerMob("minecraft:elder_guardian"); ++ registerMob("minecraft:ender_dragon"); ++ V100.registerEquipment(VERSION, "minecraft:enderman"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:enderman", new DataWalkerBlockNames("carried")); ++ registerMob("minecraft:endermite"); ++ registerThrowableProjectile("minecraft:ender_pearl"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:falling_block", new DataWalkerBlockNames("Block")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:falling_block", new DataWalkerTileEntities("TileEntityData")); ++ registerThrowableProjectile("minecraft:fireball"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:fireworks_rocket", new DataWalkerItems("FireworksItem")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:furnace_minecart", new DataWalkerBlockNames("DisplayTile")); ++ registerMob("minecraft:ghast"); ++ registerMob("minecraft:giant"); ++ registerMob("minecraft:guardian"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:hopper_minecart", new DataWalkerBlockNames("DisplayTile")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:hopper_minecart", new DataWalkerItemLists("Items")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:horse", new DataWalkerItems("ArmorItem", "SaddleItem")); ++ V100.registerEquipment(VERSION, "minecraft:horse"); ++ registerMob("minecraft:husk"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:item", new DataWalkerItems("Item")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:item_frame", new DataWalkerItems("Item")); ++ registerMob("minecraft:magma_cube"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:minecart", new DataWalkerBlockNames("DisplayTile")); ++ registerMob("minecraft:mooshroom"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:mule", new DataWalkerItemLists("Items")); ++ V100.registerEquipment(VERSION, "minecraft:mule"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:mule", new DataWalkerItems("SaddleItem")); ++ registerMob("minecraft:ocelot"); ++ registerMob("minecraft:pig"); ++ registerMob("minecraft:polar_bear"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:potion", new DataWalkerItems("Potion")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:potion", new DataWalkerBlockNames("inTile")); ++ registerMob("minecraft:rabbit"); ++ registerMob("minecraft:sheep"); ++ registerMob("minecraft:shulker"); ++ registerMob("minecraft:silverfish"); ++ registerMob("minecraft:skeleton"); ++ V100.registerEquipment(VERSION, "minecraft:skeleton_horse"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:skeleton_horse", new DataWalkerItems("SaddleItem")); ++ registerMob("minecraft:slime"); ++ registerThrowableProjectile("minecraft:small_fireball"); ++ registerThrowableProjectile("minecraft:snowball"); ++ registerMob("minecraft:snowman"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:spawner_minecart", new DataWalkerBlockNames("DisplayTile")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:spawner_minecart", (final MapType data, final long fromVersion, final long toVersion) -> { ++ MCTypeRegistry.UNTAGGED_SPAWNER.convert(data, fromVersion, toVersion); ++ return null; ++ }); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:spectral_arrow", new DataWalkerBlockNames("inTile")); ++ registerMob("minecraft:spider"); ++ registerMob("minecraft:squid"); ++ registerMob("minecraft:stray"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:tnt_minecart", new DataWalkerBlockNames("DisplayTile")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:villager", (final MapType data, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, data, "Inventory", fromVersion, toVersion); ++ ++ WalkerUtils.convertList(MCTypeRegistry.VILLAGER_TRADE, data.getMap("Offers"), "Recipes", fromVersion, toVersion); ++ ++ return null; ++ }); ++ V100.registerEquipment(VERSION, "minecraft:villager"); ++ registerMob("minecraft:villager_golem"); ++ registerMob("minecraft:witch"); ++ registerMob("minecraft:wither"); ++ registerMob("minecraft:wither_skeleton"); ++ registerThrowableProjectile("minecraft:wither_skull"); ++ registerMob("minecraft:wolf"); ++ registerThrowableProjectile("minecraft:xp_bottle"); ++ registerMob("minecraft:zombie"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:zombie_horse", new DataWalkerItems("SaddleItem")); ++ V100.registerEquipment(VERSION, "minecraft:zombie_horse"); ++ registerMob("minecraft:zombie_pigman"); ++ registerMob("minecraft:zombie_villager"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:zombie_villager", (final MapType data, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convertList(MCTypeRegistry.VILLAGER_TRADE, data.getMap("Offers"), "Recipes", fromVersion, toVersion); ++ ++ return null; ++ }); ++ registerMob("minecraft:evocation_illager"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:llama", new DataWalkerItemLists("Items")); ++ V100.registerEquipment(VERSION, "minecraft:llama"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:llama", new DataWalkerItems("SaddleItem", "DecorItem")); ++ registerMob("minecraft:vex"); ++ registerMob("minecraft:vindication_illager"); ++ // Don't need to re-register itemstack walker, the V704 will correctly choose the right id for armorstand based on ++ // the source version ++ ++ // Enforce namespace for ids ++ MCTypeRegistry.ENTITY.addStructureHook(VERSION, new DataHookEnforceNamespacedID()); ++ MCTypeRegistry.ENTITY_NAME.addStructureHook(VERSION, new DataHookValueTypeEnforceNamespaced()); ++ } ++ ++ private V705() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V804.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V804.java +new file mode 100644 +index 0000000000000000000000000000000000000000..81a2006d5e2059df0979c6380a16255767bcd89a +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V804.java +@@ -0,0 +1,59 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V804 { ++ ++ private static final int VERSION = MCVersions.V16W35A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:banner", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType tag = data.getMap("tag"); ++ if (tag == null) { ++ return null; ++ } ++ ++ final MapType blockEntity = tag.getMap("BlockEntityTag"); ++ if (blockEntity == null) { ++ return null; ++ } ++ ++ if (!blockEntity.hasKey("Base", ObjectType.NUMBER)) { ++ return null; ++ } ++ ++ data.setShort("Damage", (short)(blockEntity.getShort("Base") & 15)); ++ ++ final MapType display = tag.getMap("display"); ++ if (display != null) { ++ final ListType lore = display.getList("Lore", ObjectType.STRING); ++ if (lore != null) { ++ if (lore.size() == 1 && "(+NBT)".equals(lore.getString(0))) { ++ return null; ++ } ++ } ++ } ++ ++ blockEntity.remove("Base"); ++ if (blockEntity.isEmpty()) { ++ tag.remove("BlockEntityTag"); ++ } ++ ++ if (tag.isEmpty()) { ++ data.remove("tag"); ++ } ++ ++ return null; ++ } ++ }); ++ } ++ ++ private V804() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V806.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V806.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f4ebe856d03d9837214e9a1c93f1b1e79aa7bb08 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V806.java +@@ -0,0 +1,39 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.Types; ++ ++public final class V806 { ++ ++ private static final int VERSION = MCVersions.V16W36A + 1; ++ ++ public static void register() { ++ final DataConverter, MapType> potionWaterUpdater = new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ MapType tag = data.getMap("tag"); ++ if (tag == null) { ++ tag = Types.NBT.createEmptyMap(); ++ data.setMap("tag", tag); ++ } ++ ++ if (!tag.hasKey("Potion", ObjectType.STRING)) { ++ tag.setString("Potion", "minecraft:water"); ++ } ++ ++ return null; ++ } ++ }; ++ ++ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:potion", potionWaterUpdater); ++ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:splash_potion", potionWaterUpdater); ++ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:lingering_potion", potionWaterUpdater); ++ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:tipped_arrow", potionWaterUpdater); ++ } ++ ++ private V806() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V808.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V808.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c6b6038255e16bd15873bb7fe596b721fcec365e +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V808.java +@@ -0,0 +1,29 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V808 { ++ ++ private static final int VERSION = MCVersions.V16W38A + 1; ++ ++ public static void register() { ++ MCTypeRegistry.ENTITY.addConverterForId("minecraft:shulker", new DataConverter<>(VERSION, 1) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ if (!data.hasKey("Color", ObjectType.NUMBER)) { ++ data.setByte("Color", (byte)10); ++ } ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:shulker_box", new DataWalkerItemLists("Items")); ++ } ++ ++ private V808() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V813.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V813.java +new file mode 100644 +index 0000000000000000000000000000000000000000..68810919e168f36de160033aa659060487d94bd8 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V813.java +@@ -0,0 +1,64 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class V813 { ++ ++ private static final int VERSION = MCVersions.V16W40A; ++ ++ private static final String[] SHULKER_ID_BY_COLOUR = new String[] { ++ "minecraft:white_shulker_box", ++ "minecraft:orange_shulker_box", ++ "minecraft:magenta_shulker_box", ++ "minecraft:light_blue_shulker_box", ++ "minecraft:yellow_shulker_box", ++ "minecraft:lime_shulker_box", ++ "minecraft:pink_shulker_box", ++ "minecraft:gray_shulker_box", ++ "minecraft:silver_shulker_box", ++ "minecraft:cyan_shulker_box", ++ "minecraft:purple_shulker_box", ++ "minecraft:blue_shulker_box", ++ "minecraft:brown_shulker_box", ++ "minecraft:green_shulker_box", ++ "minecraft:red_shulker_box", ++ "minecraft:black_shulker_box" ++ }; ++ ++ public static void register() { ++ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:shulker_box", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final MapType tag = data.getMap("tag"); ++ if (tag == null) { ++ return null; ++ } ++ ++ final MapType blockEntity = tag.getMap("BlockEntityTag"); ++ if (blockEntity == null) { ++ return null; ++ } ++ ++ final int color = blockEntity.getInt("Color"); ++ blockEntity.remove("Color"); ++ ++ data.setString("id", SHULKER_ID_BY_COLOUR[color % SHULKER_ID_BY_COLOUR.length]); ++ ++ return null; ++ } ++ }); ++ ++ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:shulker_box", new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ data.remove("Color"); ++ return null; ++ } ++ }); ++ } ++ ++ private V813() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V816.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V816.java +new file mode 100644 +index 0000000000000000000000000000000000000000..dc9fba23654262b1489e4f8056a7f4b222ab1179 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V816.java +@@ -0,0 +1,27 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.converters.DataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.types.MapType; ++import java.util.Locale; ++ ++public final class V816 { ++ ++ private static final int VERSION = MCVersions.V16W43A; ++ ++ public static void register() { ++ MCTypeRegistry.OPTIONS.addStructureConverter(new DataConverter<>(VERSION) { ++ @Override ++ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { ++ final String lang = data.getString("lang"); ++ if (lang != null) { ++ data.setString("lang", lang.toLowerCase(Locale.ROOT)); ++ } ++ return null; ++ } ++ }); ++ } ++ ++ private V816() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V820.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V820.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0d59cb380e625bb2658216d4a6cb8faebdd147c5 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V820.java +@@ -0,0 +1,21 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; ++import com.google.common.collect.ImmutableMap; ++import java.util.HashMap; ++ ++public final class V820 { ++ ++ private static final int VERSION = MCVersions.V1_11 + 1; ++ ++ public static void register() { ++ ConverterAbstractItemRename.register(VERSION, new HashMap<>( ++ ImmutableMap.of( ++ "minecraft:totem", "minecraft:totem_of_undying" ++ ) ++ )::get); ++ } ++ ++ private V820() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V99.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V99.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f0e26849e28ce7ce362927ec81b281e51bd1e591 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V99.java +@@ -0,0 +1,363 @@ ++package ca.spottedleaf.dataconverter.minecraft.versions; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.HelperItemNameV102; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.hooks.DataHookEnforceNamespacedID; ++import ca.spottedleaf.dataconverter.minecraft.hooks.DataHookValueTypeEnforceNamespaced; ++import ca.spottedleaf.dataconverter.minecraft.walkers.block_name.DataWalkerBlockNames; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; ++import ca.spottedleaf.dataconverter.minecraft.walkers.item_name.DataWalkerItemNames; ++import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; ++import ca.spottedleaf.dataconverter.minecraft.walkers.tile_entity.DataWalkerTileEntities; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import com.mojang.logging.LogUtils; ++import org.slf4j.Logger; ++import java.util.HashMap; ++import java.util.Map; ++ ++public final class V99 { ++ ++ // Structure for all data before data upgrading was added to minecraft (pre 15w32a) ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ public static final int VERSION = MCVersions.V15W32A - 1; ++ ++ private static final Map ITEM_ID_TO_TILE_ENTITY_ID = new HashMap<>(); ++ static { ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:furnace", "Furnace"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:lit_furnace", "Furnace"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:chest", "Chest"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:trapped_chest", "Chest"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:ender_chest", "EnderChest"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:jukebox", "RecordPlayer"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:dispenser", "Trap"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:dropper", "Dropper"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:sign", "Sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:mob_spawner", "MobSpawner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:noteblock", "Music"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:brewing_stand", "Cauldron"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:enhanting_table", "EnchantTable"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:command_block", "CommandBlock"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:beacon", "Beacon"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:skull", "Skull"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:daylight_detector", "DLDetector"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:hopper", "Hopper"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:banner", "Banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:flower_pot", "FlowerPot"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:repeating_command_block", "CommandBlock"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:chain_command_block", "CommandBlock"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:standing_sign", "Sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:wall_sign", "Sign"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:piston_head", "Piston"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:daylight_detector_inverted", "DLDetector"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:unpowered_comparator", "Comparator"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:powered_comparator", "Comparator"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:wall_banner", "Banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:standing_banner", "Banner"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:structure_block", "Structure"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:end_portal", "Airportal"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:end_gateway", "EndGateway"); ++ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:shield", "Banner"); ++ } ++ ++ private static void registerMob(final String id) { ++ MCTypeRegistry.ENTITY.addWalker(VERSION, id, new DataWalkerItemLists("Equipment")); ++ } ++ ++ private static void registerProjectile(final String id) { ++ MCTypeRegistry.ENTITY.addWalker(VERSION, id, new DataWalkerBlockNames("inTile")); ++ } ++ ++ private static void registerInventory(final String id) { ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, id, new DataWalkerItemLists("Items")); ++ } ++ ++ public static void register() { ++ // entities ++ MCTypeRegistry.ENTITY.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convert(MCTypeRegistry.ENTITY, data, "Riding", fromVersion, toVersion); ++ ++ return null; ++ }); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "Item", new DataWalkerItems("Item")); ++ registerProjectile("ThrownEgg"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "Arrow", new DataWalkerBlockNames("inTile")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "TippedArrow", new DataWalkerBlockNames("inTile")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "SpectralArrow", new DataWalkerBlockNames("inTile")); ++ registerProjectile("Snowball"); ++ registerProjectile("Fireball"); ++ registerProjectile("SmallFireball"); ++ registerProjectile("ThrownEnderpearl"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "ThrownPotion", new DataWalkerBlockNames("inTile")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "ThrownPotion", new DataWalkerItems("Potion")); ++ registerProjectile("ThrownExpBottle"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "ItemFrame", new DataWalkerItems("Item")); ++ registerProjectile("WitherSkull"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "FallingSand", new DataWalkerBlockNames("Block")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "FallingSand", new DataWalkerTileEntities("TileEntityData")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "FireworksRocketEntity", new DataWalkerItems("FireworksItem")); ++ // Note: Minecart is the generic entity. It can be subtyped via an int to become one of the specific minecarts ++ // (i.e rideable, chest, furnace, tnt, etc) ++ // Because of this, we add all walkers to the generic type, even though they might not be needed. ++ // Vanilla does not make the generic minecart convert spawners, but we do. ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "Minecart", new DataWalkerBlockNames("DisplayTile")); // for all minecart types ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "Minecart", new DataWalkerItemLists("Items")); // for chest types ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "Minecart", (final MapType data, final long fromVersion, final long toVersion) -> { ++ MCTypeRegistry.UNTAGGED_SPAWNER.convert(data, fromVersion, toVersion); ++ return null; ++ }); // for spawner type ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "MinecartRideable", new DataWalkerBlockNames("DisplayTile")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "MinecartChest", new DataWalkerBlockNames("DisplayTile")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "MinecartChest", new DataWalkerItemLists("Items")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "MinecartFurnace", new DataWalkerBlockNames("DisplayTile")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "MinecartTNT", new DataWalkerBlockNames("DisplayTile")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "MinecartSpawner", new DataWalkerBlockNames("DisplayTile")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "MinecartSpawner", (final MapType data, final long fromVersion, final long toVersion) -> { ++ MCTypeRegistry.UNTAGGED_SPAWNER.convert(data, fromVersion, toVersion); ++ return null; ++ }); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "MinecartHopper", new DataWalkerBlockNames("DisplayTile")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "MinecartHopper", new DataWalkerItemLists("Items")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "MinecartCommandBlock", new DataWalkerBlockNames("DisplayTile")); ++ registerMob("ArmorStand"); ++ registerMob("Creeper"); ++ registerMob("Skeleton"); ++ registerMob("Spider"); ++ registerMob("Giant"); ++ registerMob("Zombie"); ++ registerMob("Slime"); ++ registerMob("Ghast"); ++ registerMob("PigZombie"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "Enderman", new DataWalkerBlockNames("carried")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "Enderman", new DataWalkerItemLists("Equipment")); ++ registerMob("CaveSpider"); ++ registerMob("Silverfish"); ++ registerMob("Blaze"); ++ registerMob("LavaSlime"); ++ registerMob("EnderDragon"); ++ registerMob("WitherBoss"); ++ registerMob("Bat"); ++ registerMob("Witch"); ++ registerMob("Endermite"); ++ registerMob("Guardian"); ++ registerMob("Pig"); ++ registerMob("Sheep"); ++ registerMob("Cow"); ++ registerMob("Chicken"); ++ registerMob("Squid"); ++ registerMob("Wolf"); ++ registerMob("MushroomCow"); ++ registerMob("SnowMan"); ++ registerMob("Ozelot"); ++ registerMob("VillagerGolem"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "EntityHorse", new DataWalkerItemLists("Items", "Equipment")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "EntityHorse", new DataWalkerItems("ArmorItem", "SaddleItem")); ++ registerMob("Rabbit"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "Villager", new DataWalkerItemLists("Inventory", "Equipment")); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "Villager", (final MapType data, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convertList(MCTypeRegistry.VILLAGER_TRADE, data.getMap("Offers"), "Recipes", fromVersion, toVersion); ++ ++ return null; ++ }); ++ registerMob("Shulker"); ++ MCTypeRegistry.ENTITY.addWalker(VERSION, "AreaEffectCloud", new DataWalkerTypePaths<>(MCTypeRegistry.PARTICLE, "Particle")); ++ ++ // tile entities ++ MCTypeRegistry.TILE_ENTITY.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convert(MCTypeRegistry.DATA_COMPONENTS, data, "components", fromVersion, toVersion); ++ ++ return null; ++ }); ++ ++ // Inventory -> new DataWalkerItemLists("Items") ++ registerInventory("Furnace"); ++ registerInventory("Chest"); ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "RecordPlayer", new DataWalkerItems("RecordItem")); ++ registerInventory("Trap"); ++ registerInventory("Dropper"); ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "MobSpawner", (final MapType data, final long fromVersion, final long toVersion) -> { ++ MCTypeRegistry.UNTAGGED_SPAWNER.convert(data, fromVersion, toVersion); ++ return null; ++ }); ++ registerInventory("Cauldron"); ++ registerInventory("Hopper"); ++ // Note: Vanilla does not properly handle this case for FlowerPot, it will not convert int ids! ++ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "FlowerPot", new DataWalkerItemNames("Item")); ++ MCTypeRegistry.TILE_ENTITY.addWalker( ++ VERSION, "Control", ++ new DataWalkerTypePaths<>(MCTypeRegistry.DATACONVERTER_CUSTOM_TYPE_COMMAND, "Command") ++ ); ++ ++ // rest ++ ++ MCTypeRegistry.ITEM_STACK.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convert(MCTypeRegistry.ITEM_NAME, data, "id", fromVersion, toVersion); ++ ++ final MapType tag = data.getMap("tag"); ++ if (tag == null) { ++ return null; ++ } ++ ++ // only things here are in tag, if changed update if above ++ ++ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, tag, "Items", fromVersion, toVersion); ++ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, tag, "ChargedProjectiles", fromVersion, toVersion); ++ ++ MapType entityTag = tag.getMap("EntityTag"); ++ if (entityTag != null) { ++ String itemId = getStringId(data.getString("id")); ++ final String entityId; ++ if ("minecraft:armor_stand".equals(itemId)) { ++ // The check for version id is removed here. For whatever reason, the legacy ++ // data converters used entity id "minecraft:armor_stand" when version was greater-than 514, ++ // but entity ids were not namespaced until V705! So somebody fucked up the legacy converters. ++ // DFU agrees with my analysis here, it will only set the entityId here to the namespaced variant ++ // with the V705 schema. ++ entityId = "ArmorStand"; ++ } else if ("minecraft:item_frame".equals(itemId)) { ++ // add missing item_frame entity id ++ entityId = "ItemFrame"; ++ } else if ("minecraft:painting".equals(itemId)) { ++ entityId = "Painting"; ++ } else { ++ entityId = entityTag.getString("id"); ++ } ++ ++ final boolean removeId; ++ if (entityId == null) { ++ if (!"minecraft:air".equals(itemId)) { ++ LOGGER.warn("Unable to resolve Entity for ItemStack (V99): " + data.getGeneric("id")); ++ } ++ removeId = false; ++ } else { ++ removeId = !entityTag.hasKey("id", ObjectType.STRING); ++ if (removeId) { ++ entityTag.setString("id", entityId); ++ } ++ } ++ ++ final MapType replace = MCTypeRegistry.ENTITY.convert(entityTag, fromVersion, toVersion); ++ ++ if (replace != null) { ++ entityTag = replace; ++ tag.setMap("EntityTag", entityTag); ++ } ++ if (removeId) { ++ entityTag.remove("id"); ++ } ++ } ++ ++ MapType blockEntityTag = tag.getMap("BlockEntityTag"); ++ if (blockEntityTag != null) { ++ final String itemId = getStringId(data.getString("id")); ++ final String entityId = ITEM_ID_TO_TILE_ENTITY_ID.get(itemId); ++ final boolean removeId; ++ if (entityId == null) { ++ if (!"minecraft:air".equals(itemId)) { ++ LOGGER.warn("Unable to resolve BlockEntity for ItemStack (V99): " + data.getGeneric("id")); ++ } ++ removeId = false; ++ } else { ++ removeId = !blockEntityTag.hasKey("id", ObjectType.STRING); ++ blockEntityTag.setString("id", entityId); ++ } ++ final MapType replace = MCTypeRegistry.TILE_ENTITY.convert(blockEntityTag, fromVersion, toVersion); ++ if (replace != null) { ++ blockEntityTag = replace; ++ tag.setMap("BlockEntityTag", blockEntityTag); ++ } ++ if (removeId) { ++ blockEntityTag.remove("id"); ++ } ++ } ++ ++ WalkerUtils.convertList(MCTypeRegistry.BLOCK_NAME, tag, "CanDestroy", fromVersion, toVersion); ++ WalkerUtils.convertList(MCTypeRegistry.BLOCK_NAME, tag, "CanPlaceOn", fromVersion, toVersion); ++ ++ return null; ++ }); ++ ++ MCTypeRegistry.PLAYER.addStructureWalker(VERSION, new DataWalkerItemLists("Inventory", "EnderItems")); ++ ++ MCTypeRegistry.CHUNK.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ final MapType level = data.getMap("Level"); ++ if (level == null) { ++ return null; ++ } ++ ++ WalkerUtils.convertList(MCTypeRegistry.ENTITY, level, "Entities", fromVersion, toVersion); ++ WalkerUtils.convertList(MCTypeRegistry.TILE_ENTITY, level, "TileEntities", fromVersion, toVersion); ++ ++ final ListType tileTicks = level.getList("TileTicks", ObjectType.MAP); ++ if (tileTicks != null) { ++ for (int i = 0, len = tileTicks.size(); i < len; ++i) { ++ final MapType tileTick = tileTicks.getMap(i); ++ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, tileTick, "i", fromVersion, toVersion); ++ } ++ } ++ ++ return null; ++ }); ++ ++ MCTypeRegistry.ENTITY_CHUNK.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convertList(MCTypeRegistry.ENTITY, data, "Entities", fromVersion, toVersion); ++ ++ return null; ++ }); ++ ++ MCTypeRegistry.SAVED_DATA_SCOREBOARD.addStructureWalker(VERSION, (final MapType root, final long fromVersion, final long toVersion) -> { ++ final MapType data = root.getMap("data"); ++ if (data == null) { ++ return null; ++ } ++ ++ WalkerUtils.convertList(MCTypeRegistry.OBJECTIVE, data, "Objectives", fromVersion, toVersion); ++ WalkerUtils.convertList(MCTypeRegistry.TEAM, data, "Teams", fromVersion, toVersion); ++ ++ return null; ++ }); ++ MCTypeRegistry.SAVED_DATA_STRUCTURE_FEATURE_INDICES.addStructureWalker(VERSION, (final MapType root, final long fromVersion, final long toVersion) -> { ++ final MapType data = root.getMap("data"); ++ if (data == null) { ++ return null; ++ } ++ ++ WalkerUtils.convertValues(MCTypeRegistry.STRUCTURE_FEATURE, data, "Features", fromVersion, toVersion); ++ ++ return null; ++ }); ++ ++ MCTypeRegistry.VILLAGER_TRADE.addStructureWalker(VERSION, (final MapType root, final long fromVersion, final long toVersion) -> { ++ WalkerUtils.convert(MCTypeRegistry.ITEM_STACK, root, "buy", fromVersion, toVersion); ++ WalkerUtils.convert(MCTypeRegistry.ITEM_STACK, root, "buyB", fromVersion, toVersion); ++ WalkerUtils.convert(MCTypeRegistry.ITEM_STACK, root, "sell", fromVersion, toVersion); ++ ++ return null; ++ }); ++ ++ // Enforce namespacing for ids ++ MCTypeRegistry.BLOCK_NAME.addStructureHook(VERSION, new DataHookValueTypeEnforceNamespaced()); ++ MCTypeRegistry.ITEM_NAME.addStructureHook(VERSION, new DataHookValueTypeEnforceNamespaced()); ++ MCTypeRegistry.ITEM_STACK.addStructureHook(VERSION, new DataHookEnforceNamespacedID()); ++ ++ // Entity is absent; the String form is not yet namespaced, unlike the above. ++ } ++ ++ private static String getStringId(final Object id) { ++ if (id instanceof String) { ++ return (String)id; ++ } else if (id instanceof Number) { ++ return HelperItemNameV102.getNameFromId(((Number)id).intValue()); ++ } else { ++ return null; ++ } ++ } ++ ++ private V99() {} ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/block_name/DataWalkerBlockNames.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/block_name/DataWalkerBlockNames.java +new file mode 100644 +index 0000000000000000000000000000000000000000..930e014858ef635ebe25f7f92dc81ba0eaac50a8 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/block_name/DataWalkerBlockNames.java +@@ -0,0 +1,11 @@ ++package ca.spottedleaf.dataconverter.minecraft.walkers.block_name; ++ ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; ++ ++public final class DataWalkerBlockNames extends DataWalkerTypePaths { ++ ++ public DataWalkerBlockNames(final String... paths) { ++ super(MCTypeRegistry.BLOCK_NAME, paths); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/game_event/GameEventListenerWalker.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/game_event/GameEventListenerWalker.java +new file mode 100644 +index 0000000000000000000000000000000000000000..64fc063748d4839d787a773d2c7258dcffc6bc21 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/game_event/GameEventListenerWalker.java +@@ -0,0 +1,26 @@ ++package ca.spottedleaf.dataconverter.minecraft.walkers.game_event; ++ ++import ca.spottedleaf.dataconverter.converters.datatypes.DataWalker; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class GameEventListenerWalker implements DataWalker> { ++ ++ @Override ++ public MapType walk(final MapType data, final long fromVersion, final long toVersion) { ++ final MapType listener = data.getMap("listener"); ++ if (listener == null) { ++ return null; ++ } ++ ++ final MapType event = listener.getMap("event"); ++ if (event == null) { ++ return null; ++ } ++ ++ WalkerUtils.convert(MCTypeRegistry.GAME_EVENT_NAME, event, "game_event", fromVersion, toVersion); ++ ++ return null; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerListPaths.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerListPaths.java +new file mode 100644 +index 0000000000000000000000000000000000000000..20c8efdb746c9d3b9d87bf991dc44e11e1ea697c +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerListPaths.java +@@ -0,0 +1,38 @@ ++package ca.spottedleaf.dataconverter.minecraft.walkers.generic; ++ ++import ca.spottedleaf.dataconverter.converters.datatypes.DataType; ++import ca.spottedleaf.dataconverter.converters.datatypes.DataWalker; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public class DataWalkerListPaths implements DataWalker> { ++ ++ protected final DataType type; ++ protected final String[] paths; ++ ++ public DataWalkerListPaths(final DataType type, final String... paths) { ++ this.type = type; ++ this.paths = paths; ++ } ++ ++ @Override ++ public final MapType walk(final MapType data, final long fromVersion, final long toVersion) { ++ final DataType type = this.type; ++ for (final String path : this.paths) { ++ final ListType list = data.getListUnchecked(path); ++ if (list == null) { ++ continue; ++ } ++ ++ for (int i = 0, len = list.size(); i < len; ++i) { ++ final Object current = list.getGeneric(i); ++ final Object converted = type.convert((T)current, fromVersion, toVersion); ++ if (converted != null) { ++ list.setGeneric(i, converted); ++ } ++ } ++ } ++ ++ return null; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerTypePaths.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerTypePaths.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4205546d7b2c4a07d23a017004989875b7beb3c6 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerTypePaths.java +@@ -0,0 +1,34 @@ ++package ca.spottedleaf.dataconverter.minecraft.walkers.generic; ++ ++import ca.spottedleaf.dataconverter.converters.datatypes.DataType; ++import ca.spottedleaf.dataconverter.converters.datatypes.DataWalker; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public class DataWalkerTypePaths implements DataWalker> { ++ ++ protected final DataType type; ++ protected final String[] paths; ++ ++ public DataWalkerTypePaths(final DataType type, final String... paths) { ++ this.type = type; ++ this.paths = paths; ++ } ++ ++ @Override ++ public final MapType walk(final MapType data, final long fromVersion, final long toVersion) { ++ for (final String path : this.paths) { ++ final Object current = data.getGeneric(path); ++ if (current == null) { ++ continue; ++ } ++ ++ final Object converted = this.type.convert((T)current, fromVersion, toVersion); ++ ++ if (converted != null) { ++ data.setGeneric(path, converted); ++ } ++ } ++ ++ return null; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/WalkerUtils.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/WalkerUtils.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4feedd9e48c3a85bd75b9c0a3b09c91fa9532a93 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/WalkerUtils.java +@@ -0,0 +1,183 @@ ++package ca.spottedleaf.dataconverter.minecraft.walkers.generic; ++ ++import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCDataType; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCValueType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import java.util.ArrayList; ++ ++public final class WalkerUtils { ++ ++ public static void convert(final MCDataType type, final MapType data, final String path, final long fromVersion, final long toVersion) { ++ if (data == null) { ++ return; ++ } ++ ++ final MapType map = data.getMap(path); ++ if (map != null) { ++ final MapType replace = type.convert(map, fromVersion, toVersion); ++ if (replace != null) { ++ data.setMap(path, replace); ++ } ++ } ++ } ++ ++ public static void convertList(final MCDataType type, final MapType data, final String path, final long fromVersion, final long toVersion) { ++ if (data == null) { ++ return; ++ } ++ ++ final ListType list = data.getList(path, ObjectType.MAP); ++ if (list != null) { ++ for (int i = 0, len = list.size(); i < len; ++i) { ++ final MapType replace = type.convert(list.getMap(i), fromVersion, toVersion); ++ if (replace != null) { ++ list.setMap(i, replace); ++ } ++ } ++ } ++ } ++ ++ public static void convertListPath(final MCDataType type, final MapType data, final String listPath, final String elementPath, ++ final long fromVersion, final long toVersion) { ++ if (data == null) { ++ return; ++ } ++ ++ final ListType list = data.getList(listPath, ObjectType.MAP); ++ if (list != null) { ++ for (int i = 0, len = list.size(); i < len; ++i) { ++ WalkerUtils.convert(type, list.getMap(i), elementPath, fromVersion, toVersion); ++ } ++ } ++ } ++ ++ public static void convertListPath(final MCDataType type, final MapType data, final String listPath, final String elementPath1, ++ final String elementPath2, final long fromVersion, final long toVersion) { ++ if (data == null) { ++ return; ++ } ++ ++ final ListType list = data.getList(listPath, ObjectType.MAP); ++ if (list != null) { ++ for (int i = 0, len = list.size(); i < len; ++i) { ++ WalkerUtils.convert(type, list.getMap(i).getMap(elementPath1), elementPath2, fromVersion, toVersion); ++ } ++ } ++ } ++ ++ public static void convert(final MCValueType type, final MapType data, final String path, final long fromVersion, final long toVersion) { ++ if (data == null) { ++ return; ++ } ++ ++ final Object value = data.getGeneric(path); ++ if (value != null) { ++ final Object converted = type.convert(value, fromVersion, toVersion); ++ if (converted != null) { ++ data.setGeneric(path, converted); ++ } ++ } ++ } ++ ++ public static void convert(final MCValueType type, final ListType data, final long fromVersion, final long toVersion) { ++ if (data == null) { ++ return; ++ } ++ ++ for (int i = 0, len = data.size(); i < len; ++i) { ++ final Object value = data.getGeneric(i); ++ final Object converted = type.convert(value, fromVersion, toVersion); ++ if (converted != null) { ++ data.setGeneric(i, converted); ++ } ++ } ++ } ++ ++ public static void convertList(final MCValueType type, final MapType data, final String path, final long fromVersion, final long toVersion) { ++ if (data == null) { ++ return; ++ } ++ ++ final ListType list = data.getListUnchecked(path); ++ if (list != null) { ++ convert(type, list, fromVersion, toVersion); ++ } ++ } ++ ++ public static void convertListPath(final MCValueType type, final MapType data, final String listPath, final String elementPath, ++ final long fromVersion, final long toVersion) { ++ if (data == null) { ++ return; ++ } ++ ++ final ListType list = data.getList(listPath, ObjectType.MAP); ++ if (list != null) { ++ for (int i = 0, len = list.size(); i < len; ++i) { ++ WalkerUtils.convert(type, list.getMap(i), elementPath, fromVersion, toVersion); ++ } ++ } ++ } ++ ++ public static void convertListPath(final MCValueType type, final MapType data, final String listPath, final String elementPath1, ++ final String elementPath2, final long fromVersion, final long toVersion) { ++ if (data == null) { ++ return; ++ } ++ ++ final ListType list = data.getList(listPath, ObjectType.MAP); ++ if (list != null) { ++ for (int i = 0, len = list.size(); i < len; ++i) { ++ WalkerUtils.convert(type, list.getMap(i).getMap(elementPath1), elementPath2, fromVersion, toVersion); ++ } ++ } ++ } ++ ++ public static void convertKeys(final MCValueType type, final MapType data, final String path, final long fromVersion, final long toVersion) { ++ if (data == null) { ++ return; ++ } ++ ++ final MapType map = data.getMap(path); ++ if (map != null) { ++ convertKeys(type, map, fromVersion, toVersion); ++ } ++ } ++ ++ public static void convertKeys(final MCValueType type, final MapType data, final long fromVersion, final long toVersion) { ++ if (data == null) { ++ return; ++ } ++ ++ RenameHelper.renameKeys(data, (final String input) -> { ++ return (String)type.convert(input, fromVersion, toVersion); ++ }); ++ } ++ ++ public static void convertValues(final MCDataType type, final MapType data, final String path, final long fromVersion, final long toVersion) { ++ if (data == null) { ++ return; ++ } ++ ++ convertValues(type, data.getMap(path), fromVersion, toVersion); ++ } ++ ++ public static void convertValues(final MCDataType type, final MapType data, final long fromVersion, final long toVersion) { ++ if (data == null) { ++ return; ++ } ++ ++ for (final String key : data.keys()) { ++ final MapType value = data.getMap(key); ++ if (value != null) { ++ final MapType replace = type.convert(value, fromVersion, toVersion); ++ if (replace != null) { ++ // no CME, key is in map already ++ data.setMap(key, replace); ++ } ++ } ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/item_name/DataWalkerItemNames.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/item_name/DataWalkerItemNames.java +new file mode 100644 +index 0000000000000000000000000000000000000000..14e291efd864d97dcf83db01c09b9daaae1949bd +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/item_name/DataWalkerItemNames.java +@@ -0,0 +1,11 @@ ++package ca.spottedleaf.dataconverter.minecraft.walkers.item_name; ++ ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; ++ ++public final class DataWalkerItemNames extends DataWalkerTypePaths { ++ ++ public DataWalkerItemNames(final String... paths) { ++ super(MCTypeRegistry.ITEM_NAME, paths); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItemLists.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItemLists.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5b4402c3cc4e68e9c591e8bbb4a2542d8e2214d4 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItemLists.java +@@ -0,0 +1,12 @@ ++package ca.spottedleaf.dataconverter.minecraft.walkers.itemstack; ++ ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerListPaths; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public class DataWalkerItemLists extends DataWalkerListPaths, MapType> { ++ ++ public DataWalkerItemLists(final String... paths) { ++ super(MCTypeRegistry.ITEM_STACK, paths); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItems.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItems.java +new file mode 100644 +index 0000000000000000000000000000000000000000..04770e8378ac8784895cdfe400a47b0b601c2187 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItems.java +@@ -0,0 +1,12 @@ ++package ca.spottedleaf.dataconverter.minecraft.walkers.itemstack; ++ ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public class DataWalkerItems extends DataWalkerTypePaths, MapType> { ++ ++ public DataWalkerItems(final String... paths) { ++ super(MCTypeRegistry.ITEM_STACK, paths); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/tile_entity/DataWalkerTileEntities.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/tile_entity/DataWalkerTileEntities.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d9cc21bf41cb4b377752b684f8e59818cd620103 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/tile_entity/DataWalkerTileEntities.java +@@ -0,0 +1,12 @@ ++package ca.spottedleaf.dataconverter.minecraft.walkers.tile_entity; ++ ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; ++import ca.spottedleaf.dataconverter.types.MapType; ++ ++public final class DataWalkerTileEntities extends DataWalkerTypePaths, MapType> { ++ ++ public DataWalkerTileEntities(final String... paths) { ++ super(MCTypeRegistry.TILE_ENTITY, paths); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/ListType.java b/src/main/java/ca/spottedleaf/dataconverter/types/ListType.java +new file mode 100644 +index 0000000000000000000000000000000000000000..19f7e95f754e8385bbe60fd2fb7fc95b6a4ebd7c +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/types/ListType.java +@@ -0,0 +1,272 @@ ++package ca.spottedleaf.dataconverter.types; ++ ++public interface ListType { ++ ++ public TypeUtil getTypeUtil(); ++ ++ @Override ++ public int hashCode(); ++ ++ @Override ++ public boolean equals(final Object other); ++ ++ // Provides a deep copy of this list ++ public ListType copy(); ++ ++ // returns NONE if no type has been assigned. if NONE, then this list is also empty. It is not true on the other hand that an empty list has no type. ++ public ObjectType getType(); ++ ++ public int size(); ++ ++ public void remove(final int index); ++ ++ public default Object getGeneric(final int index) { ++ switch (this.getType()) { ++ case NONE: ++ throw new IllegalStateException("List is empty and has no type"); ++ case BYTE: ++ return Byte.valueOf(this.getByte(index)); ++ case SHORT: ++ return Short.valueOf(this.getShort(index)); ++ case INT: ++ return Integer.valueOf(this.getInt(index)); ++ case LONG: ++ return Long.valueOf(this.getLong(index)); ++ case FLOAT: ++ return Float.valueOf(this.getFloat(index)); ++ case DOUBLE: ++ return Double.valueOf(this.getDouble(index)); ++ case NUMBER: ++ return this.getNumber(index); ++ case BYTE_ARRAY: ++ return this.getBytes(index); ++ case SHORT_ARRAY: ++ return this.getShorts(index); ++ case INT_ARRAY: ++ return this.getInts(index); ++ case LONG_ARRAY: ++ return this.getLongs(index); ++ case LIST: ++ return this.getList(index); ++ case MAP: ++ return this.getMap(index); ++ case STRING: ++ return this.getString(index); ++ default: ++ throw new UnsupportedOperationException(this.getType().name()); ++ } ++ } ++ ++ public default void setGeneric(final int index, final Object to) { ++ if (to instanceof Number) { ++ if (to instanceof Byte) { ++ this.setByte(index, ((Byte)to).byteValue()); ++ return; ++ } else if (to instanceof Short) { ++ this.setShort(index, ((Short)to).shortValue()); ++ return; ++ } else if (to instanceof Integer) { ++ this.setInt(index, ((Integer)to).intValue()); ++ return; ++ } else if (to instanceof Long) { ++ this.setLong(index, ((Long)to).longValue()); ++ return; ++ } else if (to instanceof Float) { ++ this.setFloat(index, ((Float)to).floatValue()); ++ return; ++ } else if (to instanceof Double) { ++ this.setDouble(index, ((Double)to).doubleValue()); ++ return; ++ } // else fall through to throw ++ } else if (to instanceof MapType) { ++ this.setMap(index, (MapType)to); ++ return; ++ } else if (to instanceof ListType) { ++ this.setList(index, (ListType)to); ++ return; ++ } else if (to instanceof String) { ++ this.setString(index, (String)to); ++ return; ++ } else if (to.getClass().isArray()) { ++ if (to instanceof byte[]) { ++ this.setBytes(index, (byte[])to); ++ return; ++ } else if (to instanceof short[]) { ++ this.setShorts(index, (short[])to); ++ return; ++ } else if (to instanceof int[]) { ++ this.setInts(index, (int[])to); ++ return; ++ } else if (to instanceof long[]) { ++ this.setLongs(index, (long[])to); ++ return; ++ } // else fall through to throw ++ } ++ ++ throw new IllegalArgumentException("Object " + to + " is not a valid type!"); ++ } ++ ++ // types here are strict. if the type on get does not match the underlying type, will throw. ++ ++ public Number getNumber(final int index); ++ ++ // if the value at index is a Number but not a byte, then returns the number casted to byte. If the value at the index is not a number, then throws ++ public byte getByte(final int index); ++ ++ public void setByte(final int index, final byte to); ++ ++ // if the value at index is a Number but not a short, then returns the number casted to short. If the value at the index is not a number, then throws ++ public short getShort(final int index); ++ ++ public void setShort(final int index, final short to); ++ ++ // if the value at index is a Number but not a int, then returns the number casted to int. If the value at the index is not a number, then throws ++ public int getInt(final int index); ++ ++ public void setInt(final int index, final int to); ++ ++ // if the value at index is a Number but not a long, then returns the number casted to long. If the value at the index is not a number, then throws ++ public long getLong(final int index); ++ ++ public void setLong(final int index, final long to); ++ ++ // if the value at index is a Number but not a float, then returns the number casted to float. If the value at the index is not a number, then throws ++ public float getFloat(final int index); ++ ++ public void setFloat(final int index, final float to); ++ ++ // if the value at index is a Number but not a double, then returns the number casted to double. If the value at the index is not a number, then throws ++ public double getDouble(final int index); ++ ++ public void setDouble(final int index, final double to); ++ ++ public byte[] getBytes(final int index); ++ ++ public void setBytes(final int index, final byte[] to); ++ ++ public short[] getShorts(final int index); ++ ++ public void setShorts(final int index, final short[] to); ++ ++ public int[] getInts(final int index); ++ ++ public void setInts(final int index, final int[] to); ++ ++ public long[] getLongs(final int index); ++ ++ public void setLongs(final int index, final long[] to); ++ ++ public ListType getList(final int index); ++ ++ public void setList(final int index, final ListType list); ++ ++ public MapType getMap(final int index); ++ ++ public void setMap(final int index, final MapType to); ++ ++ public String getString(final int index); ++ ++ public void setString(final int index, final String to); ++ ++ public default void addGeneric(final Object to) { ++ if (to instanceof Number) { ++ if (to instanceof Byte) { ++ this.addByte(((Byte)to).byteValue()); ++ return; ++ } else if (to instanceof Short) { ++ this.addShort(((Short)to).shortValue()); ++ return; ++ } else if (to instanceof Integer) { ++ this.addInt(((Integer)to).intValue()); ++ return; ++ } else if (to instanceof Long) { ++ this.addLong(((Long)to).longValue()); ++ return; ++ } else if (to instanceof Float) { ++ this.addFloat(((Float)to).floatValue()); ++ return; ++ } else if (to instanceof Double) { ++ this.addDouble(((Double)to).doubleValue()); ++ return; ++ } // else fall through to throw ++ } else if (to instanceof MapType) { ++ this.addMap((MapType)to); ++ return; ++ } else if (to instanceof ListType) { ++ this.addList((ListType)to); ++ return; ++ } else if (to instanceof String) { ++ this.addString((String)to); ++ return; ++ } else if (to.getClass().isArray()) { ++ if (to instanceof byte[]) { ++ this.addByteArray((byte[])to); ++ return; ++ } else if (to instanceof short[]) { ++ this.addShortArray((short[])to); ++ return; ++ } else if (to instanceof int[]) { ++ this.addIntArray((int[])to); ++ return; ++ } else if (to instanceof long[]) { ++ this.addLongArray((long[])to); ++ return; ++ } // else fall through to throw ++ } ++ ++ throw new IllegalArgumentException("Object " + to + " is not a valid type!"); ++ } ++ ++ public void addByte(final byte b); ++ ++ public void addByte(final int index, final byte b); ++ ++ public void addShort(final short s); ++ ++ public void addShort(final int index, final short s); ++ ++ public void addInt(final int i); ++ ++ public void addInt(final int index, final int i); ++ ++ public void addLong(final long l); ++ ++ public void addLong(final int index, final long l); ++ ++ public void addFloat(final float f); ++ ++ public void addFloat(final int index, final float f); ++ ++ public void addDouble(final double d); ++ ++ public void addDouble(final int index, final double d); ++ ++ public void addByteArray(final byte[] arr); ++ ++ public void addByteArray(final int index, final byte[] arr); ++ ++ public void addShortArray(final short[] arr); ++ ++ public void addShortArray(final int index, final short[] arr); ++ ++ public void addIntArray(final int[] arr); ++ ++ public void addIntArray(final int index, final int[] arr); ++ ++ public void addLongArray(final long[] arr); ++ ++ public void addLongArray(final int index, final long[] arr); ++ ++ public void addList(final ListType list); ++ ++ public void addList(final int index, final ListType list); ++ ++ public void addMap(final MapType map); ++ ++ public void addMap(final int index, final MapType map); ++ ++ public void addString(final String string); ++ ++ public void addString(final int index, final String string); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/MapType.java b/src/main/java/ca/spottedleaf/dataconverter/types/MapType.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b8dad91ad3b8692448134c4f12cf9853dc06fccc +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/types/MapType.java +@@ -0,0 +1,223 @@ ++package ca.spottedleaf.dataconverter.types; ++ ++import java.util.Set; ++ ++public interface MapType { ++ ++ public TypeUtil getTypeUtil(); ++ ++ @Override ++ public int hashCode(); ++ ++ @Override ++ public boolean equals(final Object other); ++ ++ public int size(); ++ ++ public boolean isEmpty(); ++ ++ public void clear(); ++ ++ public Set keys(); ++ ++ // Provides a deep copy of this map ++ public MapType copy(); ++ ++ public boolean hasKey(final K key); ++ ++ public boolean hasKey(final K key, final ObjectType type); ++ ++ public void remove(final K key); ++ ++ public Object getGeneric(final K key); ++ ++ // types here are not strict. if the key maps to a different type, default is always returned ++ // if default is not a parameter, then default is always null ++ ++ public Number getNumber(final K key); ++ ++ public Number getNumber(final K key, final Number dfl); ++ ++ public boolean getBoolean(final K key); ++ ++ public boolean getBoolean(final K key, final boolean dfl); ++ ++ public void setBoolean(final K key, final boolean val); ++ ++ // if the mapped value is a Number but not a byte, then the number is casted to byte. If the mapped value does not exist or is not a number, returns 0 ++ public byte getByte(final K key); ++ ++ // if the mapped value is a Number but not a byte, then the number is casted to byte. If the mapped value does not exist or is not a number, returns dfl ++ public byte getByte(final K key, final byte dfl); ++ ++ public void setByte(final K key, final byte val); ++ ++ // if the mapped value is a Number but not a short, then the number is casted to short. If the mapped value does not exist or is not a number, returns 0 ++ public short getShort(final K key); ++ ++ // if the mapped value is a Number but not a short, then the number is casted to short. If the mapped value does not exist or is not a number, returns dfl ++ public short getShort(final K key, final short dfl); ++ ++ public void setShort(final K key, final short val); ++ ++ // if the mapped value is a Number but not a int, then the number is casted to int. If the mapped value does not exist or is not a number, returns 0 ++ public int getInt(final K key); ++ ++ // if the mapped value is a Number but not a int, then the number is casted to int. If the mapped value does not exist or is not a number, returns dfl ++ public int getInt(final K key, final int dfl); ++ ++ public void setInt(final K key, final int val); ++ ++ // if the mapped value is a Number but not a long, then the number is casted to long. If the mapped value does not exist or is not a number, returns 0 ++ public long getLong(final K key); ++ ++ // if the mapped value is a Number but not a long, then the number is casted to long. If the mapped value does not exist or is not a number, returns dfl ++ public long getLong(final K key, final long dfl); ++ ++ public void setLong(final K key, final long val); ++ ++ // if the mapped value is a Number but not a float, then the number is casted to float. If the mapped value does not exist or is not a number, returns 0 ++ public float getFloat(final K key); ++ ++ // if the mapped value is a Number but not a float, then the number is casted to float. If the mapped value does not exist or is not a number, returns dfl ++ public float getFloat(final K key, final float dfl); ++ ++ public void setFloat(final K key, final float val); ++ ++ // if the mapped value is a Number but not a double, then the number is casted to double. If the mapped value does not exist or is not a number, returns 0 ++ public double getDouble(final K key); ++ ++ // if the mapped value is a Number but not a double, then the number is casted to double. If the mapped value does not exist or is not a number, returns dfl ++ public double getDouble(final K key, final double dfl); ++ ++ public void setDouble(final K key, final double val); ++ ++ public byte[] getBytes(final K key); ++ ++ public byte[] getBytes(final K key, final byte[] dfl); ++ ++ public void setBytes(final K key, final byte[] val); ++ ++ public short[] getShorts(final K key); ++ ++ public short[] getShorts(final K key, final short[] dfl); ++ ++ public void setShorts(final K key, final short[] val); ++ ++ public int[] getInts(final K key); ++ ++ public int[] getInts(final K key, final int[] dfl); ++ ++ public void setInts(final K key, final int[] val); ++ ++ public long[] getLongs(final K key); ++ ++ public long[] getLongs(final K key, final long[] dfl); ++ ++ public void setLongs(final K key, final long[] val); ++ ++ public ListType getListUnchecked(final K key); ++ ++ public ListType getListUnchecked(final K key, final ListType dfl); ++ ++ public default ListType getList(final K key, final ObjectType type) { ++ return this.getList(key, type, null); ++ } ++ ++ public default ListType getOrCreateList(final K key, final ObjectType type) { ++ ListType ret = this.getList(key, type); ++ if (ret == null) { ++ this.setList(key, ret = this.getTypeUtil().createEmptyList()); ++ } ++ ++ return ret; ++ } ++ ++ public default ListType getList(final K key, final ObjectType type, final ListType dfl) { ++ final ListType ret = this.getListUnchecked(key, null); ++ final ObjectType retType; ++ if (ret != null && ((retType = ret.getType()) == type || retType == ObjectType.UNDEFINED)) { ++ return ret; ++ } else { ++ return dfl; ++ } ++ } ++ ++ public void setList(final K key, final ListType val); ++ ++ public MapType getMap(final K key); ++ ++ public default MapType getOrCreateMap(final K key) { ++ MapType ret = this.getMap(key); ++ if (ret == null) { ++ this.setMap(key, ret = this.getTypeUtil().createEmptyMap()); ++ } ++ ++ return ret; ++ } ++ ++ public MapType getMap(final K key, final MapType dfl); ++ ++ public void setMap(final K key, final MapType val); ++ ++ public String getString(final K key); ++ ++ public String getString(final K key, final String dfl); ++ ++ public String getForcedString(final K key); ++ ++ public String getForcedString(final K key, final String dfl); ++ ++ public void setString(final K key, final String val); ++ ++ public default void setGeneric(final K key, final Object value) { ++ if (value instanceof Boolean) { ++ this.setBoolean(key, ((Boolean)value).booleanValue()); ++ } else if (value instanceof Number) { ++ if (value instanceof Byte) { ++ this.setByte(key, ((Byte)value).byteValue()); ++ return; ++ } else if (value instanceof Short) { ++ this.setShort(key, ((Short)value).shortValue()); ++ return; ++ } else if (value instanceof Integer) { ++ this.setInt(key, ((Integer)value).intValue()); ++ return; ++ } else if (value instanceof Long) { ++ this.setLong(key, ((Long)value).longValue()); ++ return; ++ } else if (value instanceof Float) { ++ this.setFloat(key, ((Float)value).floatValue()); ++ return; ++ } else if (value instanceof Double) { ++ this.setDouble(key, ((Double)value).doubleValue()); ++ return; ++ } // else fall through to throw ++ } else if (value instanceof MapType) { ++ this.setMap(key, (MapType)value); ++ return; ++ } else if (value instanceof ListType) { ++ this.setList(key, (ListType)value); ++ return; ++ } else if (value instanceof String) { ++ this.setString(key, (String)value); ++ return; ++ } else if (value.getClass().isArray()) { ++ if (value instanceof byte[]) { ++ this.setBytes(key, (byte[])value); ++ return; ++ } else if (value instanceof short[]) { ++ this.setShorts(key, (short[])value); ++ return; ++ } else if (value instanceof int[]) { ++ this.setInts(key, (int[])value); ++ return; ++ } else if (value instanceof long[]) { ++ this.setLongs(key, (long[])value); ++ return; ++ } // else fall through to throw ++ } ++ ++ throw new IllegalArgumentException("Object " + value + " is not a valid type!"); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/ObjectType.java b/src/main/java/ca/spottedleaf/dataconverter/types/ObjectType.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1aab91233ddb98c3af5d424bac120891f1ee16c7 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/types/ObjectType.java +@@ -0,0 +1,72 @@ ++package ca.spottedleaf.dataconverter.types; ++ ++public enum ObjectType { ++ NONE(null), ++ BYTE(Byte.class), ++ SHORT(Short.class), ++ INT(Integer.class), ++ LONG(Long.class), ++ FLOAT(Float.class), ++ DOUBLE(Double.class), ++ NUMBER(Number.class), ++ BYTE_ARRAY(byte[].class), ++ SHORT_ARRAY(short[].class), ++ INT_ARRAY(int[].class), ++ LONG_ARRAY(long[].class), ++ LIST(ListType.class), ++ MAP(MapType.class), ++ STRING(String.class), ++ UNDEFINED(null); ++ ++ private final Class clazz; ++ private final boolean isNumber; ++ ++ private ObjectType(final Class clazz) { ++ this.clazz = clazz; ++ this.isNumber = clazz != null && Number.class.isAssignableFrom(clazz); ++ } ++ ++ public boolean isNumber() { ++ return this.isNumber; ++ } ++ ++ public Class getObjectClass() { ++ return this.clazz; ++ } ++ ++ public static ObjectType getType(final Object object) { ++ if (object instanceof Number) { ++ if (object instanceof Byte) { ++ return BYTE; ++ } else if (object instanceof Short) { ++ return SHORT; ++ } else if (object instanceof Integer) { ++ return INT; ++ } else if (object instanceof Long) { ++ return LONG; ++ } else if (object instanceof Float) { ++ return FLOAT; ++ } else if (object instanceof Double) { ++ return DOUBLE; ++ } // else return null ++ } else if (object instanceof MapType) { ++ return MAP; ++ } else if (object instanceof ListType) { ++ return LIST; ++ } else if (object instanceof String) { ++ return STRING; ++ } else if (object.getClass().isArray()) { ++ if (object instanceof byte[]) { ++ return BYTE_ARRAY; ++ } else if (object instanceof short[]) { ++ return SHORT_ARRAY; ++ } else if (object instanceof int[]) { ++ return INT_ARRAY; ++ } else if (object instanceof long[]) { ++ return LONG_ARRAY; ++ } // else return null ++ } ++ ++ return null; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/TypeUtil.java b/src/main/java/ca/spottedleaf/dataconverter/types/TypeUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..156a2ea46f8f88a02e88b50d7bb7be82ecd41919 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/types/TypeUtil.java +@@ -0,0 +1,9 @@ ++package ca.spottedleaf.dataconverter.types; ++ ++public interface TypeUtil { ++ ++ public ListType createEmptyList(); ++ ++ public MapType createEmptyMap(); ++ ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/Types.java b/src/main/java/ca/spottedleaf/dataconverter/types/Types.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2ab9e3b579f20c9a189518496c522155630a36c4 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/types/Types.java +@@ -0,0 +1,15 @@ ++package ca.spottedleaf.dataconverter.types; ++ ++import ca.spottedleaf.dataconverter.types.json.JsonTypeCompressedUtil; ++import ca.spottedleaf.dataconverter.types.json.JsonTypeUtil; ++import ca.spottedleaf.dataconverter.types.nbt.NBTTypeUtil; ++ ++public interface Types { ++ ++ public static final TypeUtil NBT = new NBTTypeUtil(); ++ ++ public static final TypeUtil JSON = new JsonTypeUtil(); ++ ++ // why does this exist ++ public static final TypeUtil JSON_COMPRESSED = new JsonTypeCompressedUtil(); ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonListType.java b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonListType.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f6f57cb3a215876976b5eecae810b8b20925f2e2 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonListType.java +@@ -0,0 +1,415 @@ ++package ca.spottedleaf.dataconverter.types.json; ++ ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.TypeUtil; ++import ca.spottedleaf.dataconverter.types.Types; ++import com.google.gson.JsonArray; ++import com.google.gson.JsonElement; ++import com.google.gson.JsonObject; ++import com.google.gson.JsonPrimitive; ++ ++public final class JsonListType implements ListType { ++ ++ protected final JsonArray array; ++ protected final boolean compressed; ++ ++ public JsonListType(final boolean compressed) { ++ this.array = new JsonArray(); ++ this.compressed = compressed; ++ } ++ ++ public JsonListType(final JsonArray array, final boolean compressed) { ++ this.array = array; ++ this.compressed = compressed; ++ } ++ ++ @Override ++ public TypeUtil getTypeUtil() { ++ return this.compressed ? Types.JSON_COMPRESSED : Types.JSON; ++ } ++ ++ @Override ++ public boolean equals(final Object obj) { ++ if (this == obj) { ++ return true; ++ } ++ ++ if (obj == null || obj.getClass() != JsonListType.class) { ++ return false; ++ } ++ ++ return this.array.equals(((JsonListType)obj).array); ++ } ++ ++ @Override ++ public int hashCode() { ++ return this.array.hashCode(); ++ } ++ ++ @Override ++ public String toString() { ++ return "JsonListType{" + ++ "array=" + this.array + ++ ", compressed=" + this.compressed + ++ '}'; ++ } ++ ++ public JsonArray getJson() { ++ return this.array; ++ } ++ ++ @Override ++ public ListType copy() { ++ return new JsonListType(JsonTypeUtil.copyJson(this.array), this.compressed); ++ } ++ ++ @Override ++ public ObjectType getType() { ++ return ObjectType.UNDEFINED; ++ } ++ ++ @Override ++ public int size() { ++ return this.array.size(); ++ } ++ ++ @Override ++ public void remove(final int index) { ++ this.array.remove(index); ++ } ++ ++ @Override ++ public Number getNumber(final int index) { ++ final JsonElement element = this.array.get(index); ++ if (element instanceof JsonPrimitive) { ++ final JsonPrimitive primitive = (JsonPrimitive)element; ++ if (primitive.isNumber()) { ++ return primitive.getAsNumber(); ++ } else if (primitive.isBoolean()) { ++ return primitive.getAsBoolean() ? Byte.valueOf((byte)1) : Byte.valueOf((byte)0); ++ } else if (this.compressed && primitive.isString()) { ++ try { ++ return Integer.valueOf(Integer.parseInt(primitive.getAsString())); ++ } catch (final NumberFormatException ex) { ++ return null; ++ } ++ } ++ } ++ ++ return null; ++ } ++ ++ @Override ++ public byte getByte(final int index) { ++ final Number number = this.getNumber(index); ++ ++ return number == null ? 0 : number.byteValue(); ++ } ++ ++ @Override ++ public void setByte(final int index, final byte to) { ++ this.array.set(index, new JsonPrimitive(Byte.valueOf(to))); ++ } ++ ++ @Override ++ public short getShort(final int index) { ++ final Number number = this.getNumber(index); ++ ++ return number == null ? 0 : number.shortValue(); ++ } ++ ++ @Override ++ public void setShort(final int index, final short to) { ++ this.array.set(index, new JsonPrimitive(Short.valueOf(to))); ++ } ++ ++ @Override ++ public int getInt(final int index) { ++ final Number number = this.getNumber(index); ++ ++ return number == null ? 0 : number.intValue(); ++ } ++ ++ @Override ++ public void setInt(final int index, final int to) { ++ this.array.set(index, new JsonPrimitive(Integer.valueOf(to))); ++ } ++ ++ @Override ++ public long getLong(final int index) { ++ final Number number = this.getNumber(index); ++ ++ return number == null ? 0 : number.longValue(); ++ } ++ ++ @Override ++ public void setLong(final int index, final long to) { ++ this.array.set(index, new JsonPrimitive(Long.valueOf(to))); ++ } ++ ++ @Override ++ public float getFloat(final int index) { ++ final Number number = this.getNumber(index); ++ ++ return number == null ? 0 : number.floatValue(); ++ } ++ ++ @Override ++ public void setFloat(final int index, final float to) { ++ this.array.set(index, new JsonPrimitive(Float.valueOf(to))); ++ } ++ ++ @Override ++ public double getDouble(final int index) { ++ final Number number = this.getNumber(index); ++ ++ return number == null ? 0 : number.doubleValue(); ++ } ++ ++ @Override ++ public void setDouble(final int index, final double to) { ++ this.array.set(index, new JsonPrimitive(Double.valueOf(to))); ++ } ++ ++ @Override ++ public byte[] getBytes(final int index) { ++ // JSON does not support raw primitive arrays ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void setBytes(final int index, final byte[] to) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public short[] getShorts(final int index) { ++ // JSON does not support raw primitive arrays ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void setShorts(final int index, final short[] to) { ++ // JSON does not support raw primitive arrays ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public int[] getInts(final int index) { ++ // JSON does not support raw primitive arrays ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void setInts(final int index, final int[] to) { ++ // JSON does not support raw primitive arrays ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public long[] getLongs(final int index) { ++ // JSON does not support raw primitive arrays ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void setLongs(final int index, final long[] to) { ++ // JSON does not support raw primitive arrays ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public ListType getList(final int index) { ++ final JsonElement element = this.array.get(index); ++ if (element instanceof JsonArray) { ++ return new JsonListType((JsonArray)element, this.compressed); ++ } ++ return null; ++ } ++ ++ @Override ++ public void setList(final int index, final ListType list) { ++ this.array.set(index, ((JsonListType)list).array); ++ } ++ ++ @Override ++ public MapType getMap(final int index) { ++ final JsonElement element = this.array.get(index); ++ if (element instanceof JsonObject) { ++ return new JsonMapType((JsonObject)element, this.compressed); ++ } ++ return null; ++ } ++ ++ @Override ++ public void setMap(final int index, final MapType to) { ++ this.array.set(index, ((JsonMapType)to).map); ++ } ++ ++ @Override ++ public String getString(final int index) { ++ final JsonElement element = this.array.get(index); ++ if (element instanceof JsonPrimitive) { ++ final JsonPrimitive primitive = (JsonPrimitive)element; ++ if (primitive.isString() || (this.compressed && primitive.isNumber())) { ++ return primitive.getAsString(); ++ } ++ } ++ ++ return null; ++ } ++ ++ @Override ++ public void setString(final int index, final String to) { ++ this.array.set(index, new JsonPrimitive(to)); ++ } ++ ++ @Override ++ public void addByte(final byte b) { ++ this.array.add(Byte.valueOf(b)); ++ } ++ ++ @Override ++ public void addByte(final int index, final byte b) { ++ // doesn't implement any methods for adding at index... yee haw... ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void addShort(final short s) { ++ this.array.add(Short.valueOf(s)); ++ } ++ ++ @Override ++ public void addShort(final int index, final short s) { ++ // doesn't implement any methods for adding at index... yee haw... ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void addInt(final int i) { ++ this.array.add(Integer.valueOf(i)); ++ } ++ ++ @Override ++ public void addInt(final int index, final int i) { ++ // doesn't implement any methods for adding at index... yee haw... ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void addLong(final long l) { ++ this.array.add(Long.valueOf(l)); ++ } ++ ++ @Override ++ public void addLong(final int index, final long l) { ++ // doesn't implement any methods for adding at index... yee haw... ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void addFloat(final float f) { ++ this.array.add(Float.valueOf(f)); ++ } ++ ++ @Override ++ public void addFloat(final int index, final float f) { ++ // doesn't implement any methods for adding at index... yee haw... ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void addDouble(final double d) { ++ this.array.add(Double.valueOf(d)); ++ } ++ ++ @Override ++ public void addDouble(final int index, final double d) { ++ // doesn't implement any methods for adding at index... yee haw... ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void addByteArray(final byte[] arr) { ++ // JSON does not support raw primitive arrays ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void addByteArray(final int index, final byte[] arr) { ++ // JSON does not support raw primitive arrays ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void addShortArray(final short[] arr) { ++ // JSON does not support raw primitive arrays ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void addShortArray(final int index, final short[] arr) { ++ // JSON does not support raw primitive arrays ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void addIntArray(final int[] arr) { ++ // JSON does not support raw primitive arrays ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void addIntArray(final int index, final int[] arr) { ++ // JSON does not support raw primitive arrays ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void addLongArray(final long[] arr) { ++ // JSON does not support raw primitive arrays ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void addLongArray(final int index, final long[] arr) { ++ // JSON does not support raw primitive arrays ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void addList(final ListType list) { ++ this.array.add(((JsonListType)list).array); ++ } ++ ++ @Override ++ public void addList(final int index, final ListType list) { ++ // doesn't implement any methods for adding at index... yee haw... ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void addMap(final MapType map) { ++ this.array.add(((JsonMapType)map).map); ++ } ++ ++ @Override ++ public void addMap(final int index, final MapType map) { ++ // doesn't implement any methods for adding at index... yee haw... ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void addString(final String string) { ++ this.array.add(string); ++ } ++ ++ @Override ++ public void addString(final int index, final String string) { ++ // doesn't implement any methods for adding at index... yee haw... ++ throw new UnsupportedOperationException(); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonMapType.java b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonMapType.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b6ad4623894454675f4be52ecdb4655d6623b385 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonMapType.java +@@ -0,0 +1,474 @@ ++package ca.spottedleaf.dataconverter.types.json; ++ ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.TypeUtil; ++import ca.spottedleaf.dataconverter.types.Types; ++import com.google.gson.JsonArray; ++import com.google.gson.JsonElement; ++import com.google.gson.JsonObject; ++import com.google.gson.JsonPrimitive; ++import java.util.LinkedHashSet; ++import java.util.Map; ++import java.util.Set; ++ ++public final class JsonMapType implements MapType { ++ ++ protected final JsonObject map; ++ protected final boolean compressed; ++ ++ public JsonMapType(final boolean compressed) { ++ this.map = new JsonObject(); ++ this.compressed = compressed; ++ } ++ ++ public JsonMapType(final JsonObject map, final boolean compressed) { ++ this.map = map; ++ this.compressed = compressed; ++ } ++ ++ @Override ++ public TypeUtil getTypeUtil() { ++ return this.compressed ? Types.JSON_COMPRESSED : Types.JSON; ++ } ++ ++ @Override ++ public boolean equals(final Object obj) { ++ if (this == obj) { ++ return true; ++ } ++ ++ if (obj == null || obj.getClass() != JsonMapType.class) { ++ return false; ++ } ++ ++ return this.map.equals(((JsonMapType)obj).map); ++ } ++ ++ @Override ++ public int hashCode() { ++ return this.map.hashCode(); ++ } ++ ++ @Override ++ public String toString() { ++ return "JsonMapType{" + ++ "map=" + this.map + ++ ", compressed=" + this.compressed + ++ '}'; ++ } ++ ++ public JsonObject getJson() { ++ return this.map; ++ } ++ ++ @Override ++ public int size() { ++ return this.map.entrySet().size(); ++ } ++ ++ @Override ++ public boolean isEmpty() { ++ return this.map.entrySet().isEmpty(); ++ } ++ ++ @Override ++ public void clear() { ++ this.map.entrySet().clear(); ++ } ++ ++ @Override ++ public Set keys() { ++ // ah shit. no keyset method ++ final Set keys = new LinkedHashSet<>(); ++ ++ for (final Map.Entry entry : this.map.entrySet()) { ++ keys.add(entry.getKey()); ++ } ++ ++ return keys; ++ } ++ ++ @Override ++ public MapType copy() { ++ return new JsonMapType(JsonTypeUtil.copyJson(this.map), this.compressed); ++ } ++ ++ @Override ++ public boolean hasKey(final String key) { ++ return this.map.has(key); ++ } ++ ++ @Override ++ public boolean hasKey(final String key, final ObjectType type) { ++ final JsonElement element = this.map.get(key); ++ if (element == null) { ++ return false; ++ } ++ ++ if (type == ObjectType.UNDEFINED) { ++ return true; ++ } ++ ++ if (element.isJsonArray()) { ++ return type == ObjectType.LIST; ++ } else if (element.isJsonObject()) { ++ return type == ObjectType.MAP; ++ } else if (element.isJsonNull()) { ++ return false; ++ } ++ ++ final JsonPrimitive primitive = (JsonPrimitive)element; ++ if (primitive.isString()) { ++ return type == ObjectType.STRING || (this.compressed && type == ObjectType.NUMBER); ++ } else if (primitive.isBoolean()) { ++ return type.isNumber(); ++ } else { ++ // is number ++ final Number number = primitive.getAsNumber(); ++ if (number instanceof Byte) { ++ return type == ObjectType.BYTE || (this.compressed && type == ObjectType.STRING); ++ } else if (number instanceof Short) { ++ return type == ObjectType.SHORT || (this.compressed && type == ObjectType.STRING); ++ } else if (number instanceof Integer) { ++ return type == ObjectType.INT || (this.compressed && type == ObjectType.STRING); ++ } else if (number instanceof Long) { ++ return type == ObjectType.LONG || (this.compressed && type == ObjectType.STRING); ++ } else if (number instanceof Float) { ++ return type == ObjectType.FLOAT || (this.compressed && type == ObjectType.STRING); ++ } else { ++ return type == ObjectType.DOUBLE || (this.compressed && type == ObjectType.STRING); ++ } ++ } ++ } ++ ++ @Override ++ public void remove(final String key) { ++ this.map.remove(key); ++ } ++ ++ @Override ++ public Object getGeneric(final String key) { ++ final JsonElement element = this.map.get(key); ++ if (element == null || element.isJsonNull()) { ++ return null; ++ } else if (element.isJsonObject()) { ++ return new JsonMapType((JsonObject)element, this.compressed); ++ } else if (element.isJsonArray()) { ++ return new JsonListType((JsonArray)element, this.compressed); ++ } else { ++ // primitive ++ final JsonPrimitive primitive = (JsonPrimitive)element; ++ if (primitive.isNumber()) { ++ return primitive.getAsNumber(); ++ } else if (primitive.isString()) { ++ return primitive.getAsString(); ++ } else if (primitive.isBoolean()) { ++ return Boolean.valueOf(primitive.getAsBoolean()); ++ } else { ++ throw new IllegalStateException("Unknown json object " + element); ++ } ++ } ++ } ++ ++ @Override ++ public Number getNumber(final String key) { ++ return this.getNumber(key, null); ++ } ++ ++ @Override ++ public Number getNumber(final String key, final Number dfl) { ++ final JsonElement element = this.map.get(key); ++ if (element instanceof JsonPrimitive) { ++ final JsonPrimitive primitive = (JsonPrimitive)element; ++ if (primitive.isNumber()) { ++ return primitive.getAsNumber(); ++ } else if (primitive.isBoolean()) { ++ return primitive.getAsBoolean() ? Byte.valueOf((byte)1) : Byte.valueOf((byte)0); ++ } else if (this.compressed && primitive.isString()) { ++ try { ++ return Integer.valueOf(Integer.parseInt(primitive.getAsString())); ++ } catch (final NumberFormatException ex) { ++ return null; ++ } ++ } ++ } ++ ++ return dfl; ++ } ++ ++ @Override ++ public boolean getBoolean(final String key) { ++ return this.getBoolean(key, false); ++ } ++ ++ @Override ++ public boolean getBoolean(final String key, final boolean dfl) { ++ final JsonElement element = this.map.get(key); ++ if (element instanceof JsonPrimitive) { ++ final JsonPrimitive primitive = (JsonPrimitive)element; ++ if (primitive.isNumber()) { ++ return primitive.getAsNumber().byteValue() != 0; ++ } else if (primitive.isBoolean()) { ++ return primitive.getAsBoolean(); ++ } ++ } ++ ++ return dfl; ++ } ++ ++ @Override ++ public void setBoolean(final String key, final boolean val) { ++ this.map.addProperty(key, Boolean.valueOf(val)); ++ } ++ ++ @Override ++ public byte getByte(final String key) { ++ return this.getByte(key, (byte)0); ++ } ++ ++ @Override ++ public byte getByte(final String key, final byte dfl) { ++ final Number ret = this.getNumber(key, null); ++ return ret == null ? dfl : ret.byteValue(); ++ } ++ ++ @Override ++ public void setByte(final String key, final byte val) { ++ this.map.addProperty(key, Byte.valueOf(val)); ++ } ++ ++ @Override ++ public short getShort(final String key) { ++ return this.getShort(key, (short)0); ++ } ++ ++ @Override ++ public short getShort(final String key, final short dfl) { ++ final Number ret = this.getNumber(key, null); ++ return ret == null ? dfl : ret.shortValue(); ++ } ++ ++ @Override ++ public void setShort(final String key, final short val) { ++ this.map.addProperty(key, Short.valueOf(val)); ++ } ++ ++ @Override ++ public int getInt(final String key) { ++ return this.getInt(key, 0); ++ } ++ ++ @Override ++ public int getInt(final String key, final int dfl) { ++ final Number ret = this.getNumber(key, null); ++ return ret == null ? dfl : ret.intValue(); ++ } ++ ++ @Override ++ public void setInt(final String key, final int val) { ++ this.map.addProperty(key, Integer.valueOf(val)); ++ } ++ ++ @Override ++ public long getLong(final String key) { ++ return this.getLong(key, 0L); ++ } ++ ++ @Override ++ public long getLong(final String key, final long dfl) { ++ final Number ret = this.getNumber(key, null); ++ return ret == null ? dfl : ret.longValue(); ++ } ++ ++ @Override ++ public void setLong(final String key, final long val) { ++ this.map.addProperty(key, Long.valueOf(val)); ++ } ++ ++ @Override ++ public float getFloat(final String key) { ++ return this.getFloat(key, 0.0F); ++ } ++ ++ @Override ++ public float getFloat(final String key, final float dfl) { ++ final Number ret = this.getNumber(key, null); ++ return ret == null ? dfl : ret.floatValue(); ++ } ++ ++ @Override ++ public void setFloat(final String key, final float val) { ++ this.map.addProperty(key, Float.valueOf(val)); ++ } ++ ++ @Override ++ public double getDouble(final String key) { ++ return this.getDouble(key, 0.0D); ++ } ++ ++ @Override ++ public double getDouble(final String key, final double dfl) { ++ final Number ret = this.getNumber(key, null); ++ return ret == null ? dfl : ret.doubleValue(); ++ } ++ ++ @Override ++ public void setDouble(final String key, final double val) { ++ this.map.addProperty(key, Double.valueOf(val)); ++ } ++ ++ @Override ++ public byte[] getBytes(final String key) { ++ return this.getBytes(key, null); ++ } ++ ++ @Override ++ public byte[] getBytes(final String key, final byte[] dfl) { ++ return dfl; ++ } ++ ++ @Override ++ public void setBytes(final String key, final byte[] val) { ++ // JSON does not support raw primitive arrays ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public short[] getShorts(final String key) { ++ return this.getShorts(key, null); ++ } ++ ++ @Override ++ public short[] getShorts(final String key, final short[] dfl) { ++ return dfl; ++ } ++ ++ @Override ++ public void setShorts(final String key, final short[] val) { ++ // JSON does not support raw primitive arrays ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public int[] getInts(final String key) { ++ return this.getInts(key, null); ++ } ++ ++ @Override ++ public int[] getInts(final String key, final int[] dfl) { ++ return dfl; ++ } ++ ++ @Override ++ public void setInts(final String key, final int[] val) { ++ // JSON does not support raw primitive arrays ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public long[] getLongs(final String key) { ++ return this.getLongs(key, null); ++ } ++ ++ @Override ++ public long[] getLongs(final String key, final long[] dfl) { ++ return dfl; ++ } ++ ++ @Override ++ public void setLongs(final String key, final long[] val) { ++ // JSON does not support raw primitive arrays ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public ListType getListUnchecked(final String key) { ++ return this.getListUnchecked(key, null); ++ } ++ ++ @Override ++ public ListType getListUnchecked(final String key, final ListType dfl) { ++ final JsonElement element = this.map.get(key); ++ if (element instanceof JsonArray) { ++ return new JsonListType((JsonArray)element, this.compressed); ++ } ++ ++ return dfl; ++ } ++ ++ @Override ++ public void setList(final String key, final ListType val) { ++ this.map.add(key, ((JsonListType)val).getJson()); ++ } ++ ++ @Override ++ public MapType getMap(final String key) { ++ return this.getMap(key, null); ++ } ++ ++ @Override ++ public MapType getMap(final String key, final MapType dfl) { ++ final JsonElement element = this.map.get(key); ++ if (element instanceof JsonObject) { ++ return new JsonMapType((JsonObject)element, this.compressed); ++ } ++ ++ return dfl; ++ } ++ ++ @Override ++ public void setMap(final String key, final MapType val) { ++ this.map.add(key, ((JsonMapType)val).map); ++ } ++ ++ @Override ++ public String getString(final String key) { ++ return this.getString(key, null); ++ } ++ ++ @Override ++ public String getString(final String key, final String dfl) { ++ final JsonElement element = this.map.get(key); ++ if (element instanceof JsonPrimitive) { ++ final JsonPrimitive primitive = (JsonPrimitive)element; ++ if (primitive.isString()) { ++ return primitive.getAsString(); ++ } else if (this.compressed && primitive.isNumber()) { ++ return primitive.getAsString(); ++ } ++ } ++ ++ return dfl; ++ } ++ ++ @Override ++ public String getForcedString(final String key) { ++ return this.getForcedString(key, null); ++ } ++ ++ @Override ++ public String getForcedString(final String key, final String dfl) { ++ final JsonElement element = this.map.get(key); ++ if (element instanceof JsonPrimitive) { ++ final JsonPrimitive primitive = (JsonPrimitive)element; ++ if (primitive.isString()) { ++ return primitive.getAsString(); ++ } else if (primitive.isNumber()) { ++ return primitive.getAsString(); ++ } ++ ++ return primitive.toString(); ++ } else if (element != null) { ++ return element.toString(); ++ } ++ ++ return dfl; ++ } ++ ++ @Override ++ public void setString(final String key, final String val) { ++ this.map.addProperty(key, val); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonTypeCompressedUtil.java b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonTypeCompressedUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9c3093b66b847b5248bde923243fce78842bf67f +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonTypeCompressedUtil.java +@@ -0,0 +1,18 @@ ++package ca.spottedleaf.dataconverter.types.json; ++ ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.TypeUtil; ++ ++public final class JsonTypeCompressedUtil implements TypeUtil { ++ ++ @Override ++ public ListType createEmptyList() { ++ return new JsonListType(true); ++ } ++ ++ @Override ++ public MapType createEmptyMap() { ++ return new JsonMapType(true); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonTypeUtil.java b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonTypeUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9410ae68395a09c7710bdbb2ccc6acf6633cad23 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonTypeUtil.java +@@ -0,0 +1,81 @@ ++package ca.spottedleaf.dataconverter.types.json; ++ ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.TypeUtil; ++import ca.spottedleaf.dataconverter.types.nbt.NBTListType; ++import ca.spottedleaf.dataconverter.types.nbt.NBTMapType; ++import com.google.gson.JsonArray; ++import com.google.gson.JsonElement; ++import com.google.gson.JsonNull; ++import com.google.gson.JsonObject; ++import com.google.gson.JsonPrimitive; ++import com.google.gson.internal.Streams; ++import com.google.gson.stream.JsonReader; ++import java.io.StringReader; ++import java.util.Map; ++ ++public final class JsonTypeUtil implements TypeUtil { ++ ++ @Override ++ public ListType createEmptyList() { ++ return new JsonListType(false); ++ } ++ ++ @Override ++ public MapType createEmptyMap() { ++ return new JsonMapType(false); ++ } ++ ++ public static T copyJson(final T from) { ++ // This is stupidly inefficient. However, deepCopy() is not exposed in this gson version. ++ final String out = from.toString(); ++ ++ return (T)Streams.parse(new JsonReader(new StringReader(out))); ++ } ++ ++ private static Object convertToGenericNBT(final JsonElement element, final boolean compressed) { ++ if (element instanceof JsonObject) { ++ return convertJsonToNBT(new JsonMapType((JsonObject)element, compressed)); ++ } else if (element instanceof JsonArray) { ++ return convertJsonToNBT(new JsonListType((JsonArray)element, compressed)); ++ } else if (element instanceof JsonNull) { ++ return null; ++ } else { ++ final JsonPrimitive primitive = (JsonPrimitive)element; ++ if (primitive.isBoolean()) { ++ return primitive.getAsBoolean() ? Byte.valueOf((byte)1) : Byte.valueOf((byte)0); ++ } else if (primitive.isNumber()) { ++ return primitive.getAsNumber(); ++ } else if (primitive.isString()) { ++ return primitive.getAsString(); ++ } ++ } ++ ++ throw new IllegalStateException("Unrecognized type " + element); ++ } ++ ++ public static NBTMapType convertJsonToNBT(final JsonMapType json) { ++ final NBTMapType ret = new NBTMapType(); ++ for (final Map.Entry entry : json.map.entrySet()) { ++ final Object obj = convertToGenericNBT(entry.getValue(), json.compressed); ++ if (obj == null) { ++ continue; ++ } ++ ++ ret.setGeneric(entry.getKey(), obj); ++ } ++ ++ return ret; ++ } ++ ++ public static NBTListType convertJsonToNBT(final JsonListType json) { ++ final NBTListType ret = new NBTListType(); ++ ++ for (int i = 0, len = json.size(); i < len; ++i) { ++ ret.addGeneric(convertToGenericNBT(json.array.get(i), json.compressed)); ++ } ++ ++ return ret; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTListType.java b/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTListType.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bf4e9ea17222cfa8f7cee9e46775302c9c2e6328 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTListType.java +@@ -0,0 +1,440 @@ ++package ca.spottedleaf.dataconverter.types.nbt; ++ ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.TypeUtil; ++import ca.spottedleaf.dataconverter.types.Types; ++import net.minecraft.nbt.ByteArrayTag; ++import net.minecraft.nbt.ByteTag; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.DoubleTag; ++import net.minecraft.nbt.FloatTag; ++import net.minecraft.nbt.IntArrayTag; ++import net.minecraft.nbt.IntTag; ++import net.minecraft.nbt.ListTag; ++import net.minecraft.nbt.LongArrayTag; ++import net.minecraft.nbt.LongTag; ++import net.minecraft.nbt.NumericTag; ++import net.minecraft.nbt.ShortTag; ++import net.minecraft.nbt.StringTag; ++import net.minecraft.nbt.Tag; ++ ++public final class NBTListType implements ListType { ++ ++ private final ListTag list; ++ ++ public NBTListType() { ++ this.list = new ListTag(); ++ } ++ ++ public NBTListType(final ListTag tag) { ++ this.list = tag; ++ } ++ ++ @Override ++ public TypeUtil getTypeUtil() { ++ return Types.NBT; ++ } ++ ++ @Override ++ public boolean equals(final Object obj) { ++ if (this == obj) { ++ return true; ++ } ++ if (obj == null || obj.getClass() != NBTListType.class) { ++ return false; ++ } ++ ++ return this.list.equals(((NBTListType)obj).list); ++ } ++ ++ @Override ++ public int hashCode() { ++ return this.list.hashCode(); ++ } ++ ++ @Override ++ public String toString() { ++ return "NBTListType{" + ++ "list=" + this.list + ++ '}'; ++ } ++ ++ public ListTag getTag() { ++ return this.list; ++ } ++ ++ @Override ++ public ListType copy() { ++ return new NBTListType(this.list.copy()); ++ } ++ ++ protected static ObjectType getType(final byte id) { ++ switch (id) { ++ case 0: // END ++ return ObjectType.NONE; ++ case 1: // BYTE ++ return ObjectType.BYTE; ++ case 2: // SHORT ++ return ObjectType.SHORT; ++ case 3: // INT ++ return ObjectType.INT; ++ case 4: // LONG ++ return ObjectType.LONG; ++ case 5: // FLOAT ++ return ObjectType.FLOAT; ++ case 6: // DOUBLE ++ return ObjectType.DOUBLE; ++ case 7: // BYTE_ARRAY ++ return ObjectType.BYTE_ARRAY; ++ case 8: // STRING ++ return ObjectType.STRING; ++ case 9: // LIST ++ return ObjectType.LIST; ++ case 10: // COMPOUND ++ return ObjectType.MAP; ++ case 11: // INT_ARRAY ++ return ObjectType.INT_ARRAY; ++ case 12: // LONG_ARRAY ++ return ObjectType.LONG_ARRAY; ++ default: ++ throw new IllegalStateException("Unknown type: " + id); ++ } ++ } ++ ++ @Override ++ public ObjectType getType() { ++ return getType(this.list.getElementType()); ++ } ++ ++ @Override ++ public int size() { ++ return this.list.size(); ++ } ++ ++ @Override ++ public void remove(final int index) { ++ this.list.remove(index); ++ } ++ ++ @Override ++ public Number getNumber(final int index) { ++ final Tag tag = this.list.get(index); // does bound checking for us ++ if (!(tag instanceof NumericTag)) { ++ throw new IllegalStateException(); ++ } ++ return ((NumericTag)tag).getAsNumber(); ++ } ++ ++ @Override ++ public byte getByte(final int index) { ++ final Tag tag = this.list.get(index); // does bound checking for us ++ if (!(tag instanceof NumericTag)) { ++ throw new IllegalStateException(); ++ } ++ return ((NumericTag)tag).getAsByte(); ++ } ++ ++ @Override ++ public void setByte(final int index, final byte to) { ++ this.list.set(index, ByteTag.valueOf(to)); ++ } ++ ++ @Override ++ public short getShort(final int index) { ++ final Tag tag = this.list.get(index); // does bound checking for us ++ if (!(tag instanceof NumericTag)) { ++ throw new IllegalStateException(); ++ } ++ return ((NumericTag)tag).getAsShort(); ++ } ++ ++ @Override ++ public void setShort(final int index, final short to) { ++ this.list.set(index, ShortTag.valueOf(to)); ++ } ++ ++ @Override ++ public int getInt(final int index) { ++ final Tag tag = this.list.get(index); // does bound checking for us ++ if (!(tag instanceof NumericTag)) { ++ throw new IllegalStateException(); ++ } ++ return ((NumericTag)tag).getAsInt(); ++ } ++ ++ @Override ++ public void setInt(final int index, final int to) { ++ this.list.set(index, IntTag.valueOf(to)); ++ } ++ ++ @Override ++ public long getLong(final int index) { ++ final Tag tag = this.list.get(index); // does bound checking for us ++ if (!(tag instanceof NumericTag)) { ++ throw new IllegalStateException(); ++ } ++ return ((NumericTag)tag).getAsLong(); ++ } ++ ++ @Override ++ public void setLong(final int index, final long to) { ++ this.list.set(index, LongTag.valueOf(to)); ++ } ++ ++ @Override ++ public float getFloat(final int index) { ++ final Tag tag = this.list.get(index); // does bound checking for us ++ if (!(tag instanceof NumericTag)) { ++ throw new IllegalStateException(); ++ } ++ return ((NumericTag)tag).getAsFloat(); ++ } ++ ++ @Override ++ public void setFloat(final int index, final float to) { ++ this.list.set(index, FloatTag.valueOf(to)); ++ } ++ ++ @Override ++ public double getDouble(final int index) { ++ final Tag tag = this.list.get(index); // does bound checking for us ++ if (!(tag instanceof NumericTag)) { ++ throw new IllegalStateException(); ++ } ++ return ((NumericTag)tag).getAsDouble(); ++ } ++ ++ @Override ++ public void setDouble(final int index, final double to) { ++ this.list.set(index, DoubleTag.valueOf(to)); ++ } ++ ++ @Override ++ public byte[] getBytes(final int index) { ++ final Tag tag = this.list.get(index); // does bound checking for us ++ if (!(tag instanceof ByteArrayTag)) { ++ throw new IllegalStateException(); ++ } ++ return ((ByteArrayTag)tag).getAsByteArray(); ++ } ++ ++ @Override ++ public void setBytes(final int index, final byte[] to) { ++ this.list.set(index, new ByteArrayTag(to)); ++ } ++ ++ @Override ++ public short[] getShorts(final int index) { ++ // NBT does not support shorts ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void setShorts(final int index, final short[] to) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public int[] getInts(final int index) { ++ final Tag tag = this.list.get(index); // does bound checking for us ++ if (!(tag instanceof IntArrayTag)) { ++ throw new IllegalStateException(); ++ } ++ return ((IntArrayTag)tag).getAsIntArray(); ++ } ++ ++ @Override ++ public void setInts(final int index, final int[] to) { ++ this.list.set(index, new IntArrayTag(to)); ++ } ++ ++ @Override ++ public long[] getLongs(final int index) { ++ final Tag tag = this.list.get(index); // does bound checking for us ++ if (!(tag instanceof LongArrayTag)) { ++ throw new IllegalStateException(); ++ } ++ return ((LongArrayTag)tag).getAsLongArray(); ++ } ++ ++ @Override ++ public void setLongs(final int index, final long[] to) { ++ this.list.set(index, new LongArrayTag(to)); ++ } ++ ++ @Override ++ public ListType getList(final int index) { ++ final Tag tag = this.list.get(index); // does bound checking for us ++ if (!(tag instanceof ListTag)) { ++ throw new IllegalStateException(); ++ } ++ return new NBTListType((ListTag)tag); ++ } ++ ++ @Override ++ public void setList(final int index, final ListType list) { ++ this.list.set(index, ((NBTListType)list).getTag()); ++ } ++ ++ @Override ++ public MapType getMap(final int index) { ++ final Tag tag = this.list.get(index); // does bound checking for us ++ if (!(tag instanceof CompoundTag)) { ++ throw new IllegalStateException(); ++ } ++ return new NBTMapType((CompoundTag)tag); ++ } ++ ++ @Override ++ public void setMap(final int index, final MapType to) { ++ this.list.set(index, ((NBTMapType)to).getTag()); ++ } ++ ++ @Override ++ public String getString(final int index) { ++ final Tag tag = this.list.get(index); // does bound checking for us ++ if (!(tag instanceof StringTag)) { ++ throw new IllegalStateException(); ++ } ++ return ((StringTag)tag).getAsString(); ++ } ++ ++ @Override ++ public void setString(final int index, final String to) { ++ this.list.set(index, StringTag.valueOf(to)); ++ } ++ ++ @Override ++ public void addByte(final byte b) { ++ this.list.add(ByteTag.valueOf(b)); ++ } ++ ++ @Override ++ public void addByte(final int index, final byte b) { ++ this.list.add(index, ByteTag.valueOf(b)); ++ } ++ ++ @Override ++ public void addShort(final short s) { ++ this.list.add(ShortTag.valueOf(s)); ++ } ++ ++ @Override ++ public void addShort(final int index, final short s) { ++ this.list.add(index, ShortTag.valueOf(s)); ++ } ++ ++ @Override ++ public void addInt(final int i) { ++ this.list.add(IntTag.valueOf(i)); ++ } ++ ++ @Override ++ public void addInt(final int index, final int i) { ++ this.list.add(index, IntTag.valueOf(i)); ++ } ++ ++ @Override ++ public void addLong(final long l) { ++ this.list.add(LongTag.valueOf(l)); ++ } ++ ++ @Override ++ public void addLong(final int index, final long l) { ++ this.list.add(index, LongTag.valueOf(l)); ++ } ++ ++ @Override ++ public void addFloat(final float f) { ++ this.list.add(FloatTag.valueOf(f)); ++ } ++ ++ @Override ++ public void addFloat(final int index, final float f) { ++ this.list.add(index, FloatTag.valueOf(f)); ++ } ++ ++ @Override ++ public void addDouble(final double d) { ++ this.list.add(DoubleTag.valueOf(d)); ++ } ++ ++ @Override ++ public void addDouble(final int index, final double d) { ++ this.list.add(index, DoubleTag.valueOf(d)); ++ } ++ ++ @Override ++ public void addByteArray(final byte[] arr) { ++ this.list.add(new ByteArrayTag(arr)); ++ } ++ ++ @Override ++ public void addByteArray(final int index, final byte[] arr) { ++ this.list.add(index, new ByteArrayTag(arr)); ++ } ++ ++ @Override ++ public void addShortArray(final short[] arr) { ++ // NBT does not support short[] ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void addShortArray(final int index, final short[] arr) { ++ // NBT does not support short[] ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void addIntArray(final int[] arr) { ++ this.list.add(new IntArrayTag(arr)); ++ } ++ ++ @Override ++ public void addIntArray(final int index, final int[] arr) { ++ this.list.add(index, new IntArrayTag(arr)); ++ } ++ ++ @Override ++ public void addLongArray(final long[] arr) { ++ this.list.add(new LongArrayTag(arr)); ++ } ++ ++ @Override ++ public void addLongArray(final int index, final long[] arr) { ++ this.list.add(index, new LongArrayTag(arr)); ++ } ++ ++ @Override ++ public void addList(final ListType list) { ++ this.list.add(((NBTListType)list).getTag()); ++ } ++ ++ @Override ++ public void addList(final int index, final ListType list) { ++ this.list.add(index, ((NBTListType)list).getTag()); ++ } ++ ++ @Override ++ public void addMap(final MapType map) { ++ this.list.add(((NBTMapType)map).getTag()); ++ } ++ ++ @Override ++ public void addMap(final int index, final MapType map) { ++ this.list.add(index, ((NBTMapType)map).getTag()); ++ } ++ ++ @Override ++ public void addString(final String string) { ++ this.list.add(StringTag.valueOf(string)); ++ } ++ ++ @Override ++ public void addString(final int index, final String string) { ++ this.list.add(index, StringTag.valueOf(string)); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTMapType.java b/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTMapType.java +new file mode 100644 +index 0000000000000000000000000000000000000000..01b6796c6ac168a82f41cf4fddbd32a1c8a86484 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTMapType.java +@@ -0,0 +1,454 @@ ++package ca.spottedleaf.dataconverter.types.nbt; ++ ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.ObjectType; ++import ca.spottedleaf.dataconverter.types.TypeUtil; ++import ca.spottedleaf.dataconverter.types.Types; ++import net.minecraft.nbt.ByteArrayTag; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.IntArrayTag; ++import net.minecraft.nbt.ListTag; ++import net.minecraft.nbt.LongArrayTag; ++import net.minecraft.nbt.NumericTag; ++import net.minecraft.nbt.StringTag; ++import net.minecraft.nbt.Tag; ++ ++import java.util.Set; ++ ++public final class NBTMapType implements MapType { ++ ++ private final CompoundTag map; ++ ++ public NBTMapType() { ++ this.map = new CompoundTag(); ++ } ++ ++ public NBTMapType(final CompoundTag tag) { ++ this.map = tag; ++ } ++ ++ @Override ++ public boolean equals(final Object obj) { ++ if (this == obj) { ++ return true; ++ } ++ if (obj == null || obj.getClass() != NBTMapType.class) { ++ return false; ++ } ++ ++ return this.map.equals(((NBTMapType)obj).map); ++ } ++ ++ @Override ++ public TypeUtil getTypeUtil() { ++ return Types.NBT; ++ } ++ ++ @Override ++ public int hashCode() { ++ return this.map.hashCode(); ++ } ++ ++ @Override ++ public String toString() { ++ return "NBTMapType{" + ++ "map=" + this.map + ++ '}'; ++ } ++ ++ @Override ++ public int size() { ++ return this.map.size(); ++ } ++ ++ @Override ++ public boolean isEmpty() { ++ return this.map.isEmpty(); ++ } ++ ++ @Override ++ public void clear() { ++ this.map.getAllKeys().clear(); ++ } ++ ++ @Override ++ public Set keys() { ++ return this.map.getAllKeys(); ++ } ++ ++ public CompoundTag getTag() { ++ return this.map; ++ } ++ ++ @Override ++ public MapType copy() { ++ return new NBTMapType(this.map.copy()); ++ } ++ ++ @Override ++ public boolean hasKey(final String key) { ++ return this.map.get(key) != null; ++ } ++ ++ @Override ++ public boolean hasKey(final String key, final ObjectType type) { ++ final Tag tag = this.map.get(key); ++ if (tag == null) { ++ return false; ++ } ++ ++ final ObjectType valueType = NBTListType.getType(tag.getId()); ++ ++ return valueType == type || (type == ObjectType.NUMBER && valueType.isNumber()); ++ } ++ ++ @Override ++ public void remove(final String key) { ++ this.map.remove(key); ++ } ++ ++ @Override ++ public Object getGeneric(final String key) { ++ final Tag tag = this.map.get(key); ++ if (tag == null) { ++ return null; ++ } ++ ++ switch (NBTListType.getType(tag.getId())) { ++ case BYTE: ++ case SHORT: ++ case INT: ++ case LONG: ++ case FLOAT: ++ case DOUBLE: ++ return ((NumericTag)tag).getAsNumber(); ++ case MAP: ++ return new NBTMapType((CompoundTag)tag); ++ case LIST: ++ return new NBTListType((ListTag)tag); ++ case STRING: ++ return ((StringTag)tag).getAsString(); ++ case BYTE_ARRAY: ++ return ((ByteArrayTag)tag).getAsByteArray(); ++ // Note: No short array tag! ++ case INT_ARRAY: ++ return ((IntArrayTag)tag).getAsIntArray(); ++ case LONG_ARRAY: ++ return ((LongArrayTag)tag).getAsLongArray(); ++ } ++ ++ throw new IllegalStateException("Unrecognized type " + tag); ++ } ++ ++ @Override ++ public Number getNumber(final String key) { ++ return this.getNumber(key, null); ++ } ++ ++ @Override ++ public Number getNumber(final String key, final Number dfl) { ++ final Tag tag = this.map.get(key); ++ if (tag instanceof NumericTag) { ++ return ((NumericTag)tag).getAsNumber(); ++ } ++ return dfl; ++ } ++ ++ @Override ++ public boolean getBoolean(final String key) { ++ return this.getByte(key) != 0; ++ } ++ ++ @Override ++ public boolean getBoolean(final String key, final boolean dfl) { ++ return this.getByte(key, dfl ? (byte)1 : (byte)0) != 0; ++ } ++ ++ @Override ++ public void setBoolean(final String key, final boolean val) { ++ this.setByte(key, val ? (byte)1 : (byte)0); ++ } ++ ++ @Override ++ public byte getByte(final String key) { ++ final Tag tag = this.map.get(key); ++ if (tag instanceof NumericTag) { ++ return ((NumericTag)tag).getAsByte(); ++ } ++ return 0; ++ } ++ ++ @Override ++ public byte getByte(final String key, final byte dfl) { ++ final Tag tag = this.map.get(key); ++ if (tag instanceof NumericTag) { ++ return ((NumericTag)tag).getAsByte(); ++ } ++ return dfl; ++ } ++ ++ @Override ++ public void setByte(final String key, final byte val) { ++ this.map.putByte(key, val); ++ } ++ ++ @Override ++ public short getShort(final String key) { ++ final Tag tag = this.map.get(key); ++ if (tag instanceof NumericTag) { ++ return ((NumericTag)tag).getAsShort(); ++ } ++ return 0; ++ } ++ ++ @Override ++ public short getShort(final String key, final short dfl) { ++ final Tag tag = this.map.get(key); ++ if (tag instanceof NumericTag) { ++ return ((NumericTag)tag).getAsShort(); ++ } ++ return dfl; ++ } ++ ++ @Override ++ public void setShort(final String key, final short val) { ++ this.map.putShort(key, val); ++ } ++ ++ @Override ++ public int getInt(final String key) { ++ final Tag tag = this.map.get(key); ++ if (tag instanceof NumericTag) { ++ return ((NumericTag)tag).getAsInt(); ++ } ++ return 0; ++ } ++ ++ @Override ++ public int getInt(final String key, final int dfl) { ++ final Tag tag = this.map.get(key); ++ if (tag instanceof NumericTag) { ++ return ((NumericTag)tag).getAsInt(); ++ } ++ return dfl; ++ } ++ ++ @Override ++ public void setInt(final String key, final int val) { ++ this.map.putInt(key, val); ++ } ++ ++ @Override ++ public long getLong(final String key) { ++ final Tag tag = this.map.get(key); ++ if (tag instanceof NumericTag) { ++ return ((NumericTag)tag).getAsLong(); ++ } ++ return 0; ++ } ++ ++ @Override ++ public long getLong(final String key, final long dfl) { ++ final Tag tag = this.map.get(key); ++ if (tag instanceof NumericTag) { ++ return ((NumericTag)tag).getAsLong(); ++ } ++ return dfl; ++ } ++ ++ @Override ++ public void setLong(final String key, final long val) { ++ this.map.putLong(key, val); ++ } ++ ++ @Override ++ public float getFloat(final String key) { ++ final Tag tag = this.map.get(key); ++ if (tag instanceof NumericTag) { ++ return ((NumericTag)tag).getAsFloat(); ++ } ++ return 0; ++ } ++ ++ @Override ++ public float getFloat(final String key, final float dfl) { ++ final Tag tag = this.map.get(key); ++ if (tag instanceof NumericTag) { ++ return ((NumericTag)tag).getAsFloat(); ++ } ++ return dfl; ++ } ++ ++ @Override ++ public void setFloat(final String key, final float val) { ++ this.map.putFloat(key, val); ++ } ++ ++ @Override ++ public double getDouble(final String key) { ++ final Tag tag = this.map.get(key); ++ if (tag instanceof NumericTag) { ++ return ((NumericTag)tag).getAsDouble(); ++ } ++ return 0; ++ } ++ ++ @Override ++ public double getDouble(final String key, final double dfl) { ++ final Tag tag = this.map.get(key); ++ if (tag instanceof NumericTag) { ++ return ((NumericTag)tag).getAsDouble(); ++ } ++ return dfl; ++ } ++ ++ @Override ++ public void setDouble(final String key, final double val) { ++ this.map.putDouble(key, val); ++ } ++ ++ @Override ++ public byte[] getBytes(final String key) { ++ return this.getBytes(key, null); ++ } ++ ++ @Override ++ public byte[] getBytes(final String key, final byte[] dfl) { ++ final Tag tag = this.map.get(key); ++ if (tag instanceof ByteArrayTag) { ++ return ((ByteArrayTag)tag).getAsByteArray(); ++ } ++ return dfl; ++ } ++ ++ @Override ++ public void setBytes(final String key, final byte[] val) { ++ this.map.putByteArray(key, val); ++ } ++ ++ @Override ++ public short[] getShorts(final String key) { ++ return this.getShorts(key, null); ++ } ++ ++ @Override ++ public short[] getShorts(final String key, final short[] dfl) { ++ // NBT does not support short array ++ return dfl; ++ } ++ ++ @Override ++ public void setShorts(final String key, final short[] val) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public int[] getInts(final String key) { ++ return this.getInts(key, null); ++ } ++ ++ @Override ++ public int[] getInts(final String key, final int[] dfl) { ++ final Tag tag = this.map.get(key); ++ if (tag instanceof IntArrayTag) { ++ return ((IntArrayTag)tag).getAsIntArray(); ++ } ++ return dfl; ++ } ++ ++ @Override ++ public void setInts(final String key, final int[] val) { ++ this.map.putIntArray(key, val); ++ } ++ ++ @Override ++ public long[] getLongs(final String key) { ++ return this.getLongs(key, null); ++ } ++ ++ @Override ++ public long[] getLongs(final String key, final long[] dfl) { ++ final Tag tag = this.map.get(key); ++ if (tag instanceof LongArrayTag) { ++ return ((LongArrayTag)tag).getAsLongArray(); ++ } ++ return dfl; ++ } ++ ++ @Override ++ public void setLongs(final String key, final long[] val) { ++ this.map.putLongArray(key, val); ++ } ++ ++ @Override ++ public ListType getListUnchecked(final String key) { ++ return this.getListUnchecked(key, null); ++ } ++ ++ @Override ++ public ListType getListUnchecked(final String key, final ListType dfl) { ++ final Tag tag = this.map.get(key); ++ if (tag instanceof ListTag) { ++ return new NBTListType((ListTag)tag); ++ } ++ return dfl; ++ } ++ ++ @Override ++ public void setList(final String key, final ListType val) { ++ this.map.put(key, ((NBTListType)val).getTag()); ++ } ++ ++ @Override ++ public MapType getMap(final String key) { ++ return this.getMap(key, null); ++ } ++ ++ @Override ++ public MapType getMap(final String key, final MapType dfl) { ++ final Tag tag = this.map.get(key); ++ if (tag instanceof CompoundTag) { ++ return new NBTMapType((CompoundTag)tag); ++ } ++ return dfl; ++ } ++ ++ @Override ++ public void setMap(final String key, final MapType val) { ++ this.map.put(key, ((NBTMapType)val).getTag()); ++ } ++ ++ @Override ++ public String getString(final String key) { ++ return this.getString(key, null); ++ } ++ ++ @Override ++ public String getString(final String key, final String dfl) { ++ final Tag tag = this.map.get(key); ++ if (tag instanceof StringTag) { ++ return ((StringTag)tag).getAsString(); ++ } ++ return dfl; ++ } ++ ++ @Override ++ public String getForcedString(final String key) { ++ return this.getForcedString(key, null); ++ } ++ ++ @Override ++ public String getForcedString(final String key, final String dfl) { ++ final Tag tag = this.map.get(key); ++ if (tag != null) { ++ return tag.getAsString(); ++ } ++ return dfl; ++ } ++ ++ @Override ++ public void setString(final String key, final String val) { ++ this.map.putString(key, val); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTTypeUtil.java b/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTTypeUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..62c0f4073aff301bf5b3187e0d4446fd8d0ac475 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTTypeUtil.java +@@ -0,0 +1,18 @@ ++package ca.spottedleaf.dataconverter.types.nbt; ++ ++import ca.spottedleaf.dataconverter.types.ListType; ++import ca.spottedleaf.dataconverter.types.MapType; ++import ca.spottedleaf.dataconverter.types.TypeUtil; ++ ++public final class NBTTypeUtil implements TypeUtil { ++ ++ @Override ++ public ListType createEmptyList() { ++ return new NBTListType(); ++ } ++ ++ @Override ++ public MapType createEmptyMap() { ++ return new NBTMapType(); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/util/CommandArgumentUpgrader.java b/src/main/java/ca/spottedleaf/dataconverter/util/CommandArgumentUpgrader.java +new file mode 100644 +index 0000000000000000000000000000000000000000..24db54e29281076f87ef333e23da9365ad62e589 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/util/CommandArgumentUpgrader.java +@@ -0,0 +1,435 @@ ++package ca.spottedleaf.dataconverter.util; ++ ++import ca.spottedleaf.dataconverter.minecraft.MCDataConverter; ++import ca.spottedleaf.dataconverter.minecraft.MCVersions; ++import ca.spottedleaf.dataconverter.minecraft.converters.custom.V3818_Commands; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import com.google.gson.JsonElement; ++import com.google.gson.JsonParseException; ++import com.google.gson.internal.Streams; ++import com.google.gson.stream.JsonReader; ++import com.mojang.brigadier.CommandDispatcher; ++import com.mojang.brigadier.LiteralMessage; ++import com.mojang.brigadier.ParseResults; ++import com.mojang.brigadier.StringReader; ++import com.mojang.brigadier.arguments.ArgumentType; ++import com.mojang.brigadier.context.CommandContextBuilder; ++import com.mojang.brigadier.context.ParsedArgument; ++import com.mojang.brigadier.context.StringRange; ++import com.mojang.brigadier.exceptions.CommandSyntaxException; ++import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; ++import com.mojang.brigadier.tree.ArgumentCommandNode; ++import com.mojang.brigadier.tree.CommandNode; ++import com.mojang.brigadier.tree.LiteralCommandNode; ++import com.mojang.serialization.Lifecycle; ++import it.unimi.dsi.fastutil.Pair; ++import java.lang.reflect.Field; ++import java.util.ArrayList; ++import java.util.HashMap; ++import java.util.LinkedHashMap; ++import java.util.List; ++import java.util.Map; ++import java.util.Optional; ++import java.util.function.BiFunction; ++import java.util.function.Consumer; ++import java.util.function.Function; ++import java.util.stream.Stream; ++import net.minecraft.SharedConstants; ++import net.minecraft.Util; ++import net.minecraft.commands.CommandBuildContext; ++import net.minecraft.commands.CommandSource; ++import net.minecraft.commands.CommandSourceStack; ++import net.minecraft.commands.Commands; ++import net.minecraft.commands.arguments.ComponentArgument; ++import net.minecraft.commands.arguments.item.ItemArgument; ++import net.minecraft.core.Holder; ++import net.minecraft.core.HolderLookup; ++import net.minecraft.core.HolderSet; ++import net.minecraft.core.Registry; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.Tag; ++import net.minecraft.nbt.TagParser; ++import net.minecraft.network.chat.CommonComponents; ++import net.minecraft.resources.ResourceKey; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.server.commands.ExecuteCommand; ++import net.minecraft.server.commands.ReturnCommand; ++import net.minecraft.tags.TagKey; ++import net.minecraft.util.GsonHelper; ++import net.minecraft.world.phys.Vec2; ++import net.minecraft.world.phys.Vec3; ++import org.jetbrains.annotations.Nullable; ++ ++public final class CommandArgumentUpgrader { ++ private final CommandDispatcher dispatcher; ++ private final CommandBuildContext context; ++ private final CommandSourceStack source; ++ private final Map, BiFunction, CommandBuildContext, ArgumentType>> replacements; ++ ++ public static CommandArgumentUpgrader upgrader_1_20_4_to_1_20_5(final int functionPermissionLevel) { ++ return new CommandArgumentUpgrader(functionPermissionLevel, builder -> { ++ builder.registerReplacement(ItemArgument.class, (argument, ctx) -> new ItemParser_1_20_4()); ++ builder.registerReplacement(ComponentArgument.class, (argument, ctx) -> new ComponentParser_1_20_4()); ++ }); ++ } ++ ++ public CommandArgumentUpgrader( ++ final int functionPermissionLevel, ++ final Consumer consumer ++ ) { ++ this( ++ new Commands(Commands.CommandSelection.DEDICATED, makeDummyCommandBuildContext()).getDispatcher(), ++ functionPermissionLevel, ++ consumer ++ ); ++ } ++ ++ private CommandArgumentUpgrader( ++ final CommandDispatcher dispatcher, ++ final int functionPermissionLevel, ++ final Consumer consumer ++ ) { ++ final ReplacementsBuilder builder = new ReplacementsBuilder(); ++ consumer.accept(builder); ++ this.replacements = Map.copyOf(builder.replacements); ++ ++ final CommandBuildContext context = makeDummyCommandBuildContext(); ++ this.dispatcher = new CommandDispatcher<>(); ++ this.context = context; ++ final List> aliases = new ArrayList<>(); ++ for (final CommandNode child : dispatcher.getRoot().getChildren()) { ++ final CopyResult result = this.copyCommand(this.dispatcher.getRoot(), child, null); ++ if (result.replaced()) { ++ this.dispatcher.getRoot().addChild(result.root); ++ } ++ aliases.addAll(result.aliases); ++ } ++ aliases.forEach(redirectNode -> { ++ final CommandNode toNode = this.dispatcher.getRoot() ++ .getChild(redirectNode.getRedirect().getName()); ++ if (toNode != null) { ++ this.dispatcher.getRoot().addChild( ++ new LiteralCommandNode<>( ++ redirectNode.getName(), ++ null, ++ null, ++ toNode, ++ redirectNode.getRedirectModifier(), ++ redirectNode.isFork() ++ ) ++ ); ++ } ++ }); ++ ExecuteCommand.register(this.dispatcher, context); ++ ReturnCommand.register(this.dispatcher); ++ // This looks weird, but it's what vanilla does when loading functions for datapacks ++ this.source = new CommandSourceStack( ++ CommandSource.NULL, ++ Vec3.ZERO, ++ Vec2.ZERO, ++ null, ++ functionPermissionLevel, ++ "", ++ CommonComponents.EMPTY, ++ null, ++ null ++ ); ++ } ++ ++ public static final class ReplacementsBuilder { ++ private final Map, BiFunction, CommandBuildContext, ArgumentType>> replacements = ++ new HashMap<>(); ++ ++ private ReplacementsBuilder() { ++ } ++ ++ @SuppressWarnings({"unchecked", "rawtypes"}) ++ public > void registerReplacement( ++ final Class type, ++ final BiFunction> upgrader ++ ) { ++ this.replacements.put(type, (BiFunction) upgrader); ++ } ++ } ++ ++ public record UpgradedArgument(String upgraded) {} ++ ++ private static final class ItemParser_1_20_4 implements ArgumentType { ++ @Override ++ public UpgradedArgument parse(final StringReader reader) throws CommandSyntaxException { ++ final ResourceLocation id = ResourceLocation.read(reader); ++ ++ final CompoundTag itemNBT = new CompoundTag(); ++ itemNBT.putString("id", id.toString()); ++ itemNBT.putInt("Count", 1); ++ ++ if (reader.canRead() && reader.peek() == '{') { ++ itemNBT.put("tag", new TagParser(reader).readStruct()); ++ } ++ ++ final CompoundTag converted = MCDataConverter.convertTag( ++ MCTypeRegistry.ITEM_STACK, itemNBT, MCVersions.V1_20_4, SharedConstants.getCurrentVersion().getDataVersion().getVersion() ++ ); ++ ++ final String newId = converted.getString("id"); ++ ++ if (converted.contains("components", Tag.TAG_COMPOUND)) { ++ return new UpgradedArgument(newId + V3818_Commands.toCommandFormat(converted.getCompound("components"))); ++ } else { ++ return new UpgradedArgument(newId); ++ } ++ } ++ } ++ ++ private static final class ComponentParser_1_20_4 implements ArgumentType { ++ private static final Field JSON_READER_POS = Util.make(() -> { ++ try { ++ final Field field = JsonReader.class.getDeclaredField("pos"); ++ field.setAccessible(true); ++ return field; ++ } catch (final NoSuchFieldException var1) { ++ throw new IllegalStateException("Couldn't get field 'pos' for JsonReader", var1); ++ } ++ }); ++ ++ private static final Field JSON_READER_LINESTART = Util.make(() -> { ++ try { ++ final Field field = JsonReader.class.getDeclaredField("lineStart"); ++ field.setAccessible(true); ++ return field; ++ } catch (final NoSuchFieldException var1) { ++ throw new IllegalStateException("Couldn't get field 'lineStart' for JsonReader", var1); ++ } ++ }); ++ ++ @Override ++ public UpgradedArgument parse(final StringReader reader) throws CommandSyntaxException { ++ final JsonElement element; ++ try { ++ element = parseJson(reader); ++ } catch (final Exception e) { ++ throw new SimpleCommandExceptionType(new LiteralMessage(e.getMessage())).createWithContext(reader); ++ } ++ V3818_Commands.walkComponent(element); ++ return new UpgradedArgument(GsonHelper.toStableString(element)); ++ } ++ ++ public static JsonElement parseJson(final StringReader stringReader) { ++ final JsonReader jsonReader = new JsonReader(new java.io.StringReader(stringReader.getRemaining())); ++ jsonReader.setLenient(false); ++ ++ final JsonElement jsonElement; ++ try { ++ jsonElement = Streams.parse(jsonReader); ++ } catch (final StackOverflowError var9) { ++ throw new JsonParseException(var9); ++ } finally { ++ stringReader.setCursor(stringReader.getCursor() + getPos(jsonReader)); ++ } ++ return jsonElement; ++ } ++ ++ private static int getPos(final JsonReader jsonReader) { ++ try { ++ return JSON_READER_POS.getInt(jsonReader) - JSON_READER_LINESTART.getInt(jsonReader); ++ } catch (IllegalAccessException var2) { ++ throw new IllegalStateException("Couldn't read position of JsonReader", var2); ++ } ++ } ++ } ++ ++ // important: leadingSlash should not just be the result of a startsWith on command, ++ // it should reflect whether the command use is in a place that will skip a leading slash when parsing ++ public String upgradeCommandArguments(final String command, final boolean leadingSlash) { ++ final StringReader reader = new StringReader(command); ++ if (leadingSlash && reader.peek() == '/') { ++ reader.skip(); ++ } ++ final ParseResults parseResult = this.dispatcher.parse(reader, this.source); ++ if (!parseResult.getExceptions().isEmpty()) { ++ return command; ++ } ++ final Map replacements = new LinkedHashMap<>(); ++ final List>> mergedArguments = new ArrayList<>(); ++ addArguments(mergedArguments, parseResult.getContext()); ++ mergedArguments.forEach(pair -> { ++ if (pair.value().getResult() instanceof UpgradedArgument upgraded) { ++ replacements.put(pair.value().getRange(), upgraded.upgraded()); ++ } ++ }); ++ String upgradedCommand = command; ++ while (!replacements.isEmpty()) { ++ final Map.Entry next = replacements.entrySet().iterator().next(); ++ replacements.remove(next.getKey()); ++ upgradedCommand = upgradedCommand.substring(0, next.getKey().getStart()) + next.getValue() + upgradedCommand.substring(next.getKey().getEnd()); ++ // Update the offsets for the remaining replacements ++ final int diff = next.getValue().length() - next.getKey().getLength(); ++ final Map replacementsCopy = new LinkedHashMap<>(replacements); ++ replacements.clear(); ++ replacementsCopy.forEach((range, value) -> { ++ replacements.put(new StringRange(range.getStart() + diff, range.getEnd() + diff), value); ++ }); ++ } ++ return upgradedCommand; ++ } ++ ++ public String upgradeSingleArgument( ++ final Function> argumentFactory, ++ final String input ++ ) { ++ final ArgumentType argument = argumentFactory.apply(this.context); ++ final ArgumentType replaced = this.replaceArgumentType(this.context, argument); ++ if (argument == replaced) { ++ return input; ++ } ++ try { ++ final UpgradedArgument parsed = (UpgradedArgument) replaced.parse(new StringReader(input)); ++ return parsed.upgraded(); ++ } catch (final CommandSyntaxException e) { ++ return input; ++ } ++ } ++ ++ private static void addArguments( ++ final List>> mergedArguments, ++ final @Nullable CommandContextBuilder context ++ ) { ++ if (context == null) { ++ return; ++ } ++ context.getArguments().forEach((name, argument) -> mergedArguments.add(Pair.of(name, argument))); ++ addArguments(mergedArguments, context.getChild()); ++ } ++ ++ private ArgumentType replaceArgumentType(final CommandBuildContext ctx, final ArgumentType type) { ++ final BiFunction, CommandBuildContext, ArgumentType> upgrader = ++ this.replacements.get(type.getClass()); ++ if (upgrader != null) { ++ return upgrader.apply(type, ctx); ++ } ++ return type; ++ } ++ ++ record CopyResult( ++ CommandNode root, ++ boolean replaced, ++ List> aliases ++ ) { ++ CopyResult replacedResult() { ++ if (this.replaced) { ++ return this; ++ } ++ return new CopyResult(this.root, true, new ArrayList<>(this.aliases)); ++ } ++ } ++ ++ private CopyResult copyCommand( ++ final CommandNode parent, ++ final CommandNode node, ++ @Nullable CopyResult result ++ ) { ++ final CommandNode copy; ++ final boolean replaced; ++ if (node instanceof LiteralCommandNode literal) { ++ if (node.getName().equals("execute") || node.getName().equals("return")) { ++ return new CopyResult(parent, false, new ArrayList<>()); ++ } ++ if (node.getRedirect() != null) { ++ if (result != null) { ++ throw new IllegalStateException("Cannot handle non-root redirects"); ++ } ++ final List> aliases = new ArrayList<>(); ++ aliases.add(node); ++ return new CopyResult(parent, false, aliases); ++ } ++ copy = new LiteralCommandNode<>( ++ literal.getLiteral(), ++ node.getCommand(), ++ node.getRequirement(), ++ null, ++ node.getRedirectModifier(), ++ node.isFork() ++ ); ++ replaced = false; ++ } else if (node instanceof ArgumentCommandNode) { ++ final ArgumentCommandNode argument = ++ (ArgumentCommandNode) node; ++ final ArgumentType replacedType = this.replaceArgumentType(this.context, argument.getType()); ++ replaced = replacedType != argument.getType(); ++ copy = new ArgumentCommandNode<>( ++ node.getName(), ++ replacedType, ++ node.getCommand(), ++ node.getRequirement(), ++ null, ++ node.getRedirectModifier(), ++ node.isFork(), ++ argument.getCustomSuggestions() ++ ); ++ } else { ++ throw new UnsupportedOperationException(); ++ } ++ if (result == null) { ++ result = new CopyResult(copy, false, new ArrayList<>()); ++ } ++ if (replaced) { ++ result = result.replacedResult(); ++ } ++ for (final CommandNode child : node.getChildren()) { ++ result = this.copyCommand(copy, child, result); ++ } ++ if (parent != this.dispatcher.getRoot()) { ++ parent.addChild(copy); ++ } ++ return result; ++ } ++ ++ private static CommandBuildContext makeDummyCommandBuildContext() { ++ return Commands.createValidationContext( ++ new HolderLookup.Provider() { ++ ++ @Override ++ public Stream>> listRegistries() { ++ return Stream.of(); ++ } ++ ++ @Override ++ public Optional> lookup( ++ final ResourceKey> registryRef ++ ) { ++ return Optional.of(new HolderLookup.RegistryLookup() { ++ @Override ++ public ResourceKey> key() { ++ return registryRef; ++ } ++ ++ @Override ++ public Lifecycle registryLifecycle() { ++ return Lifecycle.stable(); ++ } ++ ++ @Override ++ public Stream> listElements() { ++ return Stream.of(); ++ } ++ ++ @Override ++ public Stream> listTags() { ++ return Stream.of(); ++ } ++ ++ @Override ++ public Optional> get(final ResourceKey key) { ++ return Optional.of(Holder.Reference.createStandAlone(this, key)); ++ } ++ ++ @Override ++ public Optional> get(final TagKey tag) { ++ return Optional.of(HolderSet.emptyNamed(this, tag)); ++ } ++ }); ++ } ++ } ++ ); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/util/Int2IntArraySortedMap.java b/src/main/java/ca/spottedleaf/dataconverter/util/Int2IntArraySortedMap.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6596de3d9ebae583c252aa061f0cfdf8778ea1a5 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/util/Int2IntArraySortedMap.java +@@ -0,0 +1,77 @@ ++package ca.spottedleaf.dataconverter.util; ++ ++import it.unimi.dsi.fastutil.ints.Int2IntFunction; ++ ++import java.util.Arrays; ++ ++public class Int2IntArraySortedMap { ++ ++ protected int[] key; ++ protected int[] val; ++ protected int size; ++ ++ public Int2IntArraySortedMap() { ++ this.key = new int[8]; ++ this.val = new int[8]; ++ } ++ ++ public int put(final int key, final int value) { ++ final int index = Arrays.binarySearch(this.key, 0, this.size, key); ++ if (index >= 0) { ++ final int current = this.val[index]; ++ this.val[index] = value; ++ return current; ++ } ++ final int insert = -(index + 1); ++ // shift entries down ++ if (this.size >= this.val.length) { ++ this.key = Arrays.copyOf(this.key, this.key.length * 2); ++ this.val = Arrays.copyOf(this.val, this.val.length * 2); ++ } ++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); ++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); ++ ++this.size; ++ ++ this.key[insert] = key; ++ this.val[insert] = value; ++ ++ return 0; ++ } ++ ++ public int computeIfAbsent(final int key, final Int2IntFunction producer) { ++ final int index = Arrays.binarySearch(this.key, 0, this.size, key); ++ if (index >= 0) { ++ return this.val[index]; ++ } ++ final int insert = -(index + 1); ++ // shift entries down ++ if (this.size >= this.val.length) { ++ this.key = Arrays.copyOf(this.key, this.key.length * 2); ++ this.val = Arrays.copyOf(this.val, this.val.length * 2); ++ } ++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); ++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); ++ ++this.size; ++ ++ this.key[insert] = key; ++ ++ return this.val[insert] = producer.apply(key); ++ } ++ ++ public int get(final int key) { ++ final int index = Arrays.binarySearch(this.key, 0, this.size, key); ++ if (index < 0) { ++ return 0; ++ } ++ return this.val[index]; ++ } ++ ++ public int getFloor(final int key) { ++ final int index = Arrays.binarySearch(this.key, 0, this.size, key); ++ if (index < 0) { ++ final int insert = -(index + 1) - 1; ++ return insert < 0 ? 0 : this.val[insert]; ++ } ++ return this.val[index]; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/util/Int2ObjectArraySortedMap.java b/src/main/java/ca/spottedleaf/dataconverter/util/Int2ObjectArraySortedMap.java +new file mode 100644 +index 0000000000000000000000000000000000000000..de9d632489609136c712a9adaee941fd38fad440 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/util/Int2ObjectArraySortedMap.java +@@ -0,0 +1,74 @@ ++package ca.spottedleaf.dataconverter.util; ++ ++import java.util.Arrays; ++import java.util.function.IntFunction; ++ ++public class Int2ObjectArraySortedMap { ++ ++ protected int[] key; ++ protected V[] val; ++ protected int size; ++ ++ public Int2ObjectArraySortedMap() { ++ this.key = new int[8]; ++ this.val = (V[])new Object[8]; ++ } ++ ++ public V put(final int key, final V value) { ++ final int index = Arrays.binarySearch(this.key, 0, this.size, key); ++ if (index >= 0) { ++ final V current = this.val[index]; ++ this.val[index] = value; ++ return current; ++ } ++ final int insert = -(index + 1); ++ // shift entries down ++ if (this.size >= this.val.length) { ++ this.key = Arrays.copyOf(this.key, this.key.length * 2); ++ this.val = Arrays.copyOf(this.val, this.val.length * 2); ++ } ++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); ++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); ++ ++ this.key[insert] = key; ++ this.val[insert] = value; ++ ++ return null; ++ } ++ ++ public V computeIfAbsent(final int key, final IntFunction producer) { ++ final int index = Arrays.binarySearch(this.key, 0, this.size, key); ++ if (index >= 0) { ++ return this.val[index]; ++ } ++ final int insert = -(index + 1); ++ // shift entries down ++ if (this.size >= this.val.length) { ++ this.key = Arrays.copyOf(this.key, this.key.length * 2); ++ this.val = Arrays.copyOf(this.val, this.val.length * 2); ++ } ++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); ++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); ++ ++ this.key[insert] = key; ++ ++ return this.val[insert] = producer.apply(key); ++ } ++ ++ public V get(final int key) { ++ final int index = Arrays.binarySearch(this.key, 0, this.size, key); ++ if (index < 0) { ++ return null; ++ } ++ return this.val[index]; ++ } ++ ++ public V getFloor(final int key) { ++ final int index = Arrays.binarySearch(this.key, 0, this.size, key); ++ if (index < 0) { ++ final int insert = -(index + 1); ++ return this.val[insert]; ++ } ++ return this.val[index]; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/util/IntegerUtil.java b/src/main/java/ca/spottedleaf/dataconverter/util/IntegerUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4bbf38c812feeb30d2aa5f3fcf482bfcbed79d05 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/util/IntegerUtil.java +@@ -0,0 +1,239 @@ ++package ca.spottedleaf.dataconverter.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; ++ } ++ ++ public static long getDivisorNumbers(final int d) { ++ final int ad = 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 ++ ++ /* ++ Signed usage: ++ int number; ++ long magic = getDivisorNumbers(div); ++ long mul = magic >>> 32; ++ int sign = number >> 31; ++ int result = (int)(((long)number * mul) >>> magic) - sign; ++ */ ++ /* ++ Unsigned usage: ++ int number; ++ long magic = getDivisorNumbers(div); ++ long mul = magic >>> 32; ++ int result = (int)(((long)number * mul) >>> magic); ++ */ ++ ++ int p = 31; ++ ++ // all these variables are UNSIGNED! ++ int t = two31 + (d >>> 31); ++ int anc = t - 1 - (int)((t & mask)%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; ++ 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/ca/spottedleaf/dataconverter/util/Long2IntArraySortedMap.java b/src/main/java/ca/spottedleaf/dataconverter/util/Long2IntArraySortedMap.java +new file mode 100644 +index 0000000000000000000000000000000000000000..94705bb141b550589faa9a0408402d8636c61907 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/util/Long2IntArraySortedMap.java +@@ -0,0 +1,76 @@ ++package ca.spottedleaf.dataconverter.util; ++ ++import it.unimi.dsi.fastutil.longs.Long2IntFunction; ++import java.util.Arrays; ++ ++public class Long2IntArraySortedMap { ++ ++ protected long[] key; ++ protected int[] val; ++ protected int size; ++ ++ public Long2IntArraySortedMap() { ++ this.key = new long[8]; ++ this.val = new int[8]; ++ } ++ ++ public int put(final long key, final int value) { ++ final int index = Arrays.binarySearch(this.key, 0, this.size, key); ++ if (index >= 0) { ++ final int current = this.val[index]; ++ this.val[index] = value; ++ return current; ++ } ++ final int insert = -(index + 1); ++ // shift entries down ++ if (this.size >= this.val.length) { ++ this.key = Arrays.copyOf(this.key, this.key.length * 2); ++ this.val = Arrays.copyOf(this.val, this.val.length * 2); ++ } ++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); ++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); ++ ++this.size; ++ ++ this.key[insert] = key; ++ this.val[insert] = value; ++ ++ return 0; ++ } ++ ++ public int computeIfAbsent(final long key, final Long2IntFunction producer) { ++ final int index = Arrays.binarySearch(this.key, 0, this.size, key); ++ if (index >= 0) { ++ return this.val[index]; ++ } ++ final int insert = -(index + 1); ++ // shift entries down ++ if (this.size >= this.val.length) { ++ this.key = Arrays.copyOf(this.key, this.key.length * 2); ++ this.val = Arrays.copyOf(this.val, this.val.length * 2); ++ } ++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); ++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); ++ ++this.size; ++ ++ this.key[insert] = key; ++ ++ return this.val[insert] = producer.apply(key); ++ } ++ ++ public int get(final long key) { ++ final int index = Arrays.binarySearch(this.key, 0, this.size, key); ++ if (index < 0) { ++ return 0; ++ } ++ return this.val[index]; ++ } ++ ++ public int getFloor(final long key) { ++ final int index = Arrays.binarySearch(this.key, 0, this.size, key); ++ if (index < 0) { ++ final int insert = -(index + 1) - 1; ++ return insert < 0 ? 0 : this.val[insert]; ++ } ++ return this.val[index]; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/util/Long2ObjectArraySortedMap.java b/src/main/java/ca/spottedleaf/dataconverter/util/Long2ObjectArraySortedMap.java +new file mode 100644 +index 0000000000000000000000000000000000000000..6f634c8825589a23f46ad7b54354475c9a95bd1b +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/util/Long2ObjectArraySortedMap.java +@@ -0,0 +1,76 @@ ++package ca.spottedleaf.dataconverter.util; ++ ++import java.util.Arrays; ++import java.util.function.LongFunction; ++ ++public class Long2ObjectArraySortedMap { ++ ++ protected long[] key; ++ protected V[] val; ++ protected int size; ++ ++ public Long2ObjectArraySortedMap() { ++ this.key = new long[8]; ++ this.val = (V[])new Object[8]; ++ } ++ ++ public V put(final long key, final V value) { ++ final int index = Arrays.binarySearch(this.key, 0, this.size, key); ++ if (index >= 0) { ++ final V current = this.val[index]; ++ this.val[index] = value; ++ return current; ++ } ++ final int insert = -(index + 1); ++ // shift entries down ++ if (this.size >= this.val.length) { ++ this.key = Arrays.copyOf(this.key, this.key.length * 2); ++ this.val = Arrays.copyOf(this.val, this.val.length * 2); ++ } ++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); ++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); ++ ++this.size; ++ ++ this.key[insert] = key; ++ this.val[insert] = value; ++ ++ return null; ++ } ++ ++ public V computeIfAbsent(final long key, final LongFunction producer) { ++ final int index = Arrays.binarySearch(this.key, 0, this.size, key); ++ if (index >= 0) { ++ return this.val[index]; ++ } ++ final int insert = -(index + 1); ++ // shift entries down ++ if (this.size >= this.val.length) { ++ this.key = Arrays.copyOf(this.key, this.key.length * 2); ++ this.val = Arrays.copyOf(this.val, this.val.length * 2); ++ } ++ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); ++ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); ++ ++this.size; ++ ++ this.key[insert] = key; ++ ++ return this.val[insert] = producer.apply(key); ++ } ++ ++ public V get(final long key) { ++ final int index = Arrays.binarySearch(this.key, 0, this.size, key); ++ if (index < 0) { ++ return null; ++ } ++ return this.val[index]; ++ } ++ ++ public V getFloor(final long key) { ++ final int index = Arrays.binarySearch(this.key, 0, this.size, key); ++ if (index < 0) { ++ final int insert = -(index + 1) - 1; ++ return insert < 0 ? null : this.val[insert]; ++ } ++ return this.val[index]; ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/dataconverter/util/NamespaceUtil.java b/src/main/java/ca/spottedleaf/dataconverter/util/NamespaceUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5a6536377c9c1e1753e930ff2a6bb98ea57055c7 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/dataconverter/util/NamespaceUtil.java +@@ -0,0 +1,39 @@ ++package ca.spottedleaf.dataconverter.util; ++ ++import ca.spottedleaf.dataconverter.types.MapType; ++import net.minecraft.resources.ResourceLocation; ++ ++public final class NamespaceUtil { ++ ++ private NamespaceUtil() {} ++ ++ public static void enforceForPath(final MapType data, final String path) { ++ if (data == null) { ++ return; ++ } ++ ++ final String id = data.getString(path); ++ if (id != null) { ++ final String replace = NamespaceUtil.correctNamespaceOrNull(id); ++ if (replace != null) { ++ data.setString(path, replace); ++ } ++ } ++ } ++ ++ public static String correctNamespace(final String value) { ++ if (value == null) { ++ return null; ++ } ++ final ResourceLocation resourceLocation = ResourceLocation.tryParse(value); ++ return resourceLocation != null ? resourceLocation.toString() : value; ++ } ++ ++ public static String correctNamespaceOrNull(final String value) { ++ if (value == null) { ++ return null; ++ } ++ final String correct = correctNamespace(value); ++ return correct.equals(value) ? null : correct; ++ } ++} +diff --git a/src/main/java/net/minecraft/data/structures/StructureUpdater.java b/src/main/java/net/minecraft/data/structures/StructureUpdater.java +index c0296e62fe46302e887b0c4528471e3bccde1c67..1327a33c594b56a2142d35fff8e6ef07f688797d 100644 +--- a/src/main/java/net/minecraft/data/structures/StructureUpdater.java ++++ b/src/main/java/net/minecraft/data/structures/StructureUpdater.java +@@ -25,7 +25,7 @@ public class StructureUpdater implements SnbtToNbt.Filter { + LOGGER.warn("SNBT Too old, do not forget to update: {} < {}: {}", i, 3798, name); + } + +- CompoundTag compoundTag = DataFixTypes.STRUCTURE.updateToCurrentVersion(DataFixers.getDataFixer(), nbt, i); ++ CompoundTag compoundTag = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.STRUCTURE, nbt, i, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()); // Paper + structureTemplate.load(BuiltInRegistries.BLOCK.asLookup(), compoundTag); + return structureTemplate.save(new CompoundTag()); + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java +index e605dbdb821b2d13217ac88426e50480a4e4741d..a62c90e10c0dfa4c6211a05c4071932756d7b218 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java +@@ -83,7 +83,7 @@ public class ChunkStorage implements AutoCloseable { + + try { + // CraftBukkit start +- if (i < 1466) { ++ if (false && i < 1466) { // Paper - no longer needed, data converter system / DFU handles it now + CompoundTag level = nbttagcompound.getCompound("Level"); + if (level.getBoolean("TerrainPopulated") && !level.getBoolean("LightPopulated")) { + ServerChunkCache cps = (generatoraccess == null) ? null : ((ServerLevel) generatoraccess).getChunkSource(); +@@ -95,7 +95,7 @@ public class ChunkStorage implements AutoCloseable { + // CraftBukkit end + + if (i < 1493) { +- nbttagcompound = DataFixTypes.CHUNK.update(this.fixerUpper, nbttagcompound, i, 1493); ++ ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.CHUNK, nbttagcompound, i, 1493); // Paper - replace chunk converter + if (nbttagcompound.getCompound("Level").getBoolean("hasLegacyStructureData")) { + LegacyStructureDataHandler persistentstructurelegacy = this.getLegacyStructureHandler(resourcekey, supplier); + +@@ -113,7 +113,7 @@ public class ChunkStorage implements AutoCloseable { + // Spigot end + + ChunkStorage.injectDatafixingContext(nbttagcompound, resourcekey, optional); +- nbttagcompound = DataFixTypes.CHUNK.updateToCurrentVersion(this.fixerUpper, nbttagcompound, Math.max(1493, i)); ++ nbttagcompound = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.CHUNK, nbttagcompound, Math.max(1493, i), SharedConstants.getCurrentVersion().getDataVersion().getVersion()); // Paper - replace chunk converter + if (i < SharedConstants.getCurrentVersion().getDataVersion().getVersion()) { + NbtUtils.addCurrentDataVersion(nbttagcompound); + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java +index cf44d7b48c774d4c2431d42e352b42d200ee1b71..fa4f9afb421c7924557372cbb2f20caf9e13c81c 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java +@@ -32,13 +32,30 @@ public class SimpleRegionStorage implements AutoCloseable { + return this.worker.store(pos, nbt); + } + ++ // Paper start - rewrite data conversion system ++ private ca.spottedleaf.dataconverter.minecraft.datatypes.MCDataType getDataConverterType() { ++ if (this.dataFixType == DataFixTypes.ENTITY_CHUNK) { ++ return ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.ENTITY_CHUNK; ++ } else if (this.dataFixType == DataFixTypes.POI_CHUNK) { ++ return ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.POI_CHUNK; ++ } else { ++ throw new UnsupportedOperationException("For " + this.dataFixType.name()); ++ } ++ } ++ // Paper end - rewrite data conversion system ++ + public CompoundTag upgradeChunkTag(CompoundTag nbt, int oldVersion) { +- int i = NbtUtils.getDataVersion(nbt, oldVersion); +- return this.dataFixType.updateToCurrentVersion(this.fixerUpper, nbt, i); ++ // Paper start - rewrite data conversion system ++ final int dataVer = NbtUtils.getDataVersion(nbt, oldVersion); ++ return ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(this.getDataConverterType(), nbt, dataVer, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()); ++ // Paper end - rewrite data conversion system + } + + public Dynamic upgradeChunkTag(Dynamic nbt, int oldVersion) { +- return this.dataFixType.updateToCurrentVersion(this.fixerUpper, nbt, oldVersion); ++ // Paper start - rewrite data conversion system ++ final CompoundTag converted = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(this.getDataConverterType(), (CompoundTag)nbt.getValue(), oldVersion, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()); ++ return new Dynamic<>(net.minecraft.nbt.NbtOps.INSTANCE, converted); ++ // Paper end - rewrite data conversion system + } + + public CompletableFuture synchronize(boolean sync) { +diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java +index c6181e14d85d454506534f9bbe856156c0d4a062..609100ed7aa0b23aa5a9c6fbf6878ea320bd3a93 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java +@@ -151,7 +151,7 @@ public class StructureCheck { + + CompoundTag compoundTag2; + try { +- compoundTag2 = DataFixTypes.CHUNK.updateToCurrentVersion(this.fixerUpper, compoundTag, i); ++ compoundTag2 = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.CHUNK, compoundTag, i, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()); // Paper - replace chunk converter + } catch (Exception var12) { + LOGGER.warn("Failed to partially datafix chunk {}", pos, var12); + return StructureCheckResult.CHUNK_LOAD_NEEDED; +diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager.java b/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager.java +index 39b4f29fb97464c2021b857bccb2fa933d4003d7..4d9dbf27c7c7d57a06e5f12b7fe30723d2cb69ef 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager.java +@@ -233,7 +233,7 @@ public class StructureTemplateManager { + public StructureTemplate readStructure(CompoundTag nbt) { + StructureTemplate structureTemplate = new StructureTemplate(); + int i = NbtUtils.getDataVersion(nbt, 500); +- structureTemplate.load(this.blockLookup, DataFixTypes.STRUCTURE.updateToCurrentVersion(this.fixerUpper, nbt, i)); ++ structureTemplate.load(this.blockLookup, ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.STRUCTURE, nbt, i, SharedConstants.getCurrentVersion().getDataVersion().getVersion())); // Paper + return structureTemplate; + } + +diff --git a/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java b/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java +index e69d805701def93466ad7fa94f019ec4610efb47..427ee4d6f12a7abd8da0c65e0b9081b25824df40 100644 +--- a/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java ++++ b/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java +@@ -277,12 +277,21 @@ public class LevelStorageSource { + static Dynamic readLevelDataTagFixed(Path path, DataFixer dataFixer) throws IOException { + CompoundTag nbttagcompound = LevelStorageSource.readLevelDataTagRaw(path); + CompoundTag nbttagcompound1 = nbttagcompound.getCompound("Data"); +- int i = NbtUtils.getDataVersion(nbttagcompound1, -1); ++ int i = NbtUtils.getDataVersion(nbttagcompound1, -1); final int version = i; // Paper - obfuscation helpers + Dynamic dynamic = DataFixTypes.LEVEL.updateToCurrentVersion(dataFixer, new Dynamic(NbtOps.INSTANCE, nbttagcompound1), i); + ++ // Paper start - replace data conversion system + dynamic = dynamic.update("Player", (dynamic1) -> { +- return DataFixTypes.PLAYER.updateToCurrentVersion(dataFixer, dynamic1, i); ++ return new Dynamic<>( ++ NbtOps.INSTANCE, ++ ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag( ++ ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.PLAYER, ++ (net.minecraft.nbt.CompoundTag)dynamic1.getValue(), ++ version, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion() ++ ) ++ ); + }); ++ // Paper end - replace data conversion system + dynamic = dynamic.update("WorldGenSettings", (dynamic1) -> { + return DataFixTypes.WORLD_GEN_SETTINGS.updateToCurrentVersion(dataFixer, dynamic1, i); + }); +diff --git a/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java b/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java +index 1d287dd7379e56f7fd4b425880b850cd843f5789..8ab7ca373a885fbe658013c9c6a2e38d32d77bb2 100644 +--- a/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java ++++ b/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java +@@ -137,7 +137,7 @@ public class PlayerDataStorage { + }).map((nbttagcompound) -> { + int i = NbtUtils.getDataVersion(nbttagcompound, -1); + +- nbttagcompound = DataFixTypes.PLAYER.updateToCurrentVersion(this.fixerUpper, nbttagcompound, i); ++ nbttagcompound = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.PLAYER, nbttagcompound, i, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()); // Paper - rewrite data conversion system + // entityhuman.load(nbttagcompound); // CraftBukkit - handled above + return nbttagcompound; + }); +diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +index 2f44452631256e3d6abce02f861cc5bfa5208b53..ca1240c2534f1e6c7182996fde23e58f9c252960 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +@@ -526,7 +526,7 @@ public final class CraftMagicNumbers implements UnsafeValues { + + net.minecraft.nbt.CompoundTag compound = deserializeNbtFromBytes(data); + final int dataVersion = compound.getInt("DataVersion"); +- compound = (net.minecraft.nbt.CompoundTag) MinecraftServer.getServer().fixerUpper.update(References.ITEM_STACK, new Dynamic<>(NbtOps.INSTANCE, compound), dataVersion, this.getDataVersion()).getValue(); ++ compound = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.ITEM_STACK, compound, dataVersion, this.getDataVersion()); // Paper - replace data conversion system + return CraftItemStack.asCraftMirror(net.minecraft.world.item.ItemStack.parse(MinecraftServer.getServer().registryAccess(), compound).orElseThrow()); + } + +@@ -547,7 +547,7 @@ public final class CraftMagicNumbers implements UnsafeValues { + + net.minecraft.nbt.CompoundTag compound = deserializeNbtFromBytes(data); + int dataVersion = compound.getInt("DataVersion"); +- compound = (net.minecraft.nbt.CompoundTag) MinecraftServer.getServer().fixerUpper.update(References.ENTITY, new Dynamic<>(NbtOps.INSTANCE, compound), dataVersion, this.getDataVersion()).getValue(); ++ compound = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.ENTITY, compound, dataVersion, this.getDataVersion()); + if (!preserveUUID) { + // Generate a new UUID so we don't have to worry about deserializing the same entity twice + compound.remove("UUID"); diff --git a/patches/server/0981-Rewrite-dataconverter-system.patch b/patches/server/0981-Rewrite-dataconverter-system.patch deleted file mode 100644 index bbb4bda157..0000000000 --- a/patches/server/0981-Rewrite-dataconverter-system.patch +++ /dev/null @@ -1,28922 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sat, 19 Jun 2021 10:43:01 -0700 -Subject: [PATCH] Rewrite dataconverter system - -Please see https://github.com/PaperMC/DataConverter -for details. - -diff --git a/src/main/java/ca/spottedleaf/dataconverter/converters/DataConverter.java b/src/main/java/ca/spottedleaf/dataconverter/converters/DataConverter.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1863c606be715683d53863a0c9293525d199c9cf ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/converters/DataConverter.java -@@ -0,0 +1,54 @@ -+package ca.spottedleaf.dataconverter.converters; -+ -+import java.util.Comparator; -+ -+public abstract class DataConverter { -+ -+ public static final Comparator> LOWEST_VERSION_COMPARATOR = (x, y) -> { -+ return Long.compare(x.getEncodedVersion(), y.getEncodedVersion()); -+ }; -+ -+ protected final int toVersion; -+ protected final int versionStep; -+ -+ public DataConverter(final int toVersion) { -+ this.toVersion = toVersion; -+ this.versionStep = 0; -+ } -+ -+ public DataConverter(final int toVersion, final int versionStep) { -+ this.toVersion = toVersion; -+ this.versionStep = versionStep; -+ } -+ -+ public final int getToVersion() { -+ return this.toVersion; -+ } -+ -+ public final int getVersionStep() { -+ return this.versionStep; -+ } -+ -+ public final long getEncodedVersion() { -+ return encodeVersions(this.toVersion, this.versionStep); -+ } -+ -+ public abstract R convert(final T data, final long sourceVersion, final long toVersion); -+ -+ // step must be in the lower bits, so that encodeVersions(version, step) < encodeVersions(version, step + 1) -+ public static long encodeVersions(final int version, final int step) { -+ return ((long)version << 32) | (step & 0xFFFFFFFFL); -+ } -+ -+ public static int getVersion(final long encoded) { -+ return (int)(encoded >>> 32); -+ } -+ -+ public static int getStep(final long encoded) { -+ return (int)encoded; -+ } -+ -+ public static String encodedToString(final long encoded) { -+ return getVersion(encoded) + "." + getStep(encoded); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataHook.java b/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataHook.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0b92c5c66ad3a5198873f98287a5ced71c231d09 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataHook.java -@@ -0,0 +1,9 @@ -+package ca.spottedleaf.dataconverter.converters.datatypes; -+ -+public interface DataHook { -+ -+ public R preHook(final T data, final long fromVersion, final long toVersion); -+ -+ public R postHook(final T data, final long fromVersion, final long toVersion); -+ -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataType.java b/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataType.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b56a7f9ace3b947fed49101b6e9936721fb99ea5 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataType.java -@@ -0,0 +1,7 @@ -+package ca.spottedleaf.dataconverter.converters.datatypes; -+ -+public abstract class DataType { -+ -+ public abstract R convert(final T data, final long fromVersion, final long toVersion); -+ -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataWalker.java b/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataWalker.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ca55b3f7e7208e629e88d4c7bfa9517384a26fef ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/converters/datatypes/DataWalker.java -@@ -0,0 +1,9 @@ -+package ca.spottedleaf.dataconverter.converters.datatypes; -+ -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public interface DataWalker { -+ -+ public T walk(final T data, final long fromVersion, final long toVersion); -+ -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCDataConverter.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCDataConverter.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a27d3d41109271834b6c37fa22d4b80d9e4b88c8 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCDataConverter.java -@@ -0,0 +1,79 @@ -+package ca.spottedleaf.dataconverter.minecraft; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.converters.datatypes.DataType; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCDataType; -+import ca.spottedleaf.dataconverter.minecraft.versions.V99; -+import ca.spottedleaf.dataconverter.types.json.JsonMapType; -+import ca.spottedleaf.dataconverter.types.nbt.NBTMapType; -+import com.google.gson.JsonObject; -+import it.unimi.dsi.fastutil.longs.LongArrayList; -+import net.minecraft.nbt.CompoundTag; -+ -+public final class MCDataConverter { -+ -+ private static final LongArrayList BREAKPOINTS = MCVersionRegistry.getBreakpoints(); -+ -+ public static T copy(final T type) { -+ if (type instanceof CompoundTag) { -+ return (T)((CompoundTag)type).copy(); -+ } else if (type instanceof JsonObject) { -+ return (T)((JsonObject)type).deepCopy(); -+ } -+ -+ return type; -+ } -+ -+ public static CompoundTag convertTag(final MCDataType type, final CompoundTag data, final int fromVersion, final int toVersion) { -+ final NBTMapType wrapped = new NBTMapType(data); -+ -+ final NBTMapType replaced = (NBTMapType)convert(type, wrapped, fromVersion, toVersion); -+ -+ return replaced == null ? wrapped.getTag() : replaced.getTag(); -+ } -+ -+ public static JsonObject convertJson(final MCDataType type, final JsonObject data, final boolean compressed, final int fromVersion, final int toVersion) { -+ final JsonMapType wrapped = new JsonMapType(data, compressed); -+ -+ final JsonMapType replaced = (JsonMapType)convert(type, wrapped, fromVersion, toVersion); -+ -+ return replaced == null ? wrapped.getJson() : replaced.getJson(); -+ } -+ -+ public static R convert(final DataType type, final T data, int fromVersion, final int toVersion) { -+ Object ret = data; -+ -+ long currentVersion = DataConverter.encodeVersions(fromVersion < V99.VERSION ? V99.VERSION : fromVersion, Integer.MAX_VALUE); -+ final long nextVersion = DataConverter.encodeVersions(toVersion, Integer.MAX_VALUE); -+ -+ for (int i = 0, len = BREAKPOINTS.size(); i < len; ++i) { -+ final long breakpoint = BREAKPOINTS.getLong(i); -+ -+ if (currentVersion >= breakpoint) { -+ continue; -+ } -+ -+ final Object converted = type.convert((T)ret, currentVersion, Math.min(nextVersion, breakpoint - 1)); -+ if (converted != null) { -+ ret = converted; -+ } -+ -+ currentVersion = Math.min(nextVersion, breakpoint - 1); -+ -+ if (currentVersion == nextVersion) { -+ break; -+ } -+ } -+ -+ if (currentVersion != nextVersion) { -+ final Object converted = type.convert((T)ret, currentVersion, nextVersion); -+ if (converted != null) { -+ ret = converted; -+ } -+ } -+ -+ return (R)ret; -+ } -+ -+ private MCDataConverter() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCVersionRegistry.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCVersionRegistry.java -new file mode 100644 -index 0000000000000000000000000000000000000000..057a2e1e160282c53336802580410ca23ea3222a ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCVersionRegistry.java -@@ -0,0 +1,424 @@ -+package ca.spottedleaf.dataconverter.minecraft; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import com.mojang.logging.LogUtils; -+import it.unimi.dsi.fastutil.ints.Int2ObjectLinkedOpenHashMap; -+import it.unimi.dsi.fastutil.ints.IntArrayList; -+import it.unimi.dsi.fastutil.ints.IntLinkedOpenHashSet; -+import it.unimi.dsi.fastutil.ints.IntRBTreeSet; -+import it.unimi.dsi.fastutil.longs.LongArrayList; -+import it.unimi.dsi.fastutil.longs.LongComparator; -+import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; -+import org.slf4j.Logger; -+import java.lang.reflect.Field; -+import java.util.Arrays; -+import java.util.Locale; -+ -+public final class MCVersionRegistry { -+ -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ -+ private static final Int2ObjectLinkedOpenHashMap VERSION_NAMES = new Int2ObjectLinkedOpenHashMap<>(); -+ private static final IntArrayList VERSION_LIST; -+ private static final LongArrayList DATA_VERSION_LIST; -+ -+ private static final IntArrayList DATACONVERTER_VERSIONS_LIST; -+ private static final IntLinkedOpenHashSet DATACONVERTER_VERSIONS_MAJOR = new IntLinkedOpenHashSet(); -+ private static final LongLinkedOpenHashSet DATACONVERTER_VERSIONS = new LongLinkedOpenHashSet(); -+ private static final Int2ObjectLinkedOpenHashMap SUBVERSIONS = new Int2ObjectLinkedOpenHashMap<>(); -+ private static final LongArrayList BREAKPOINTS = new LongArrayList(); -+ static { -+ // Note: Some of these are nameless. -+ // Unless a data version is specified here, it will NOT have converters ran for it. Please add them on update! -+ final int[] converterVersions = new int[] { -+ 99, -+ 100, -+ 101, -+ 102, -+ 105, -+ 106, -+ 107, -+ 108, -+ 109, -+ 110, -+ 111, -+ 113, -+ 135, -+ 143, -+ 147, -+ 165, -+ 501, -+ 502, -+ 505, -+ 700, -+ 701, -+ 702, -+ 703, -+ 704, -+ 705, -+ 804, -+ 806, -+ 808, -+ 808, -+ 813, -+ 816, -+ 820, -+ 1022, -+ 1125, -+ 1344, -+ 1446, -+ 1450, -+ 1451, -+ 1451, -+ 1451, -+ 1451, -+ 1451, -+ 1451, -+ 1451, -+ 1451, -+ 1451, -+ 1456, -+ 1458, -+ 1460, -+ 1466, -+ 1470, -+ 1474, -+ 1475, -+ 1480, -+ 1481, -+ 1483, -+ 1484, -+ 1486, -+ 1487, -+ 1488, -+ 1490, -+ 1492, -+ 1494, -+ 1496, -+ 1500, -+ 1501, -+ 1502, -+ 1506, -+ 1510, -+ 1514, -+ 1515, -+ 1624, -+ 1800, -+ 1801, -+ 1802, -+ 1803, -+ 1904, -+ 1905, -+ 1906, -+ 1909, -+ 1911, -+ 1914, -+ 1917, -+ 1918, -+ 1920, -+ 1925, -+ 1928, -+ 1929, -+ 1931, -+ 1936, -+ 1946, -+ 1948, -+ 1953, -+ 1955, -+ 1961, -+ 1963, -+ 2100, -+ 2202, -+ 2209, -+ 2211, -+ 2218, -+ 2501, -+ 2502, -+ 2503, -+ 2505, -+ 2508, -+ 2509, -+ 2511, -+ 2514, -+ 2516, -+ 2518, -+ 2519, -+ 2522, -+ 2523, -+ 2527, -+ 2528, -+ 2529, -+ 2531, -+ 2533, -+ 2535, -+ 2538, -+ 2550, -+ 2551, -+ 2552, -+ 2553, -+ 2558, -+ 2568, -+ 2671, -+ 2679, -+ 2680, -+ 2684, -+ 2686, -+ 2688, -+ 2690, -+ 2691, -+ 2693, -+ 2696, -+ 2700, -+ 2701, -+ 2702, -+ 2704, -+ 2707, -+ 2710, -+ 2717, -+ 2825, -+ 2831, -+ 2832, -+ 2833, -+ 2838, -+ 2841, -+ 2842, -+ 2843, -+ 2846, -+ 2852, -+ 2967, -+ 2970, -+ 3077, -+ 3078, -+ 3081, -+ 3082, -+ 3083, -+ 3084, -+ 3086, -+ 3087, -+ 3088, -+ 3090, -+ 3093, -+ 3094, -+ 3097, -+ 3108, -+ 3201, -+ 3203, -+ 3204, -+ 3209, -+ 3214, -+ 3319, -+ 3322, -+ 3438, -+ 3439, -+ 3440, -+ 3441, -+ 3447, -+ 3448, -+ 3450, -+ 3451, -+ 3459, -+ 3564, -+ 3565, -+ 3566, -+ 3568, -+ 3683, -+ 3685, -+ 3692, -+ 3800, -+ 3803, -+ 3807, -+ 3808, -+ 3809, -+ 3812, -+ 3813, -+ 3814, -+ 3818, -+ 3820, -+ 3825, -+ 3828, -+ 3833 -+ // All up to 1.20.5 -+ }; -+ Arrays.sort(converterVersions); -+ -+ DATACONVERTER_VERSIONS_MAJOR.addAll(DATACONVERTER_VERSIONS_LIST = new IntArrayList(converterVersions)); -+ -+ // add sub versions -+ registerSubVersion(MCVersions.V16W38A + 1, 1); -+ -+ registerSubVersion(MCVersions.V17W47A, 1); -+ registerSubVersion(MCVersions.V17W47A, 2); -+ registerSubVersion(MCVersions.V17W47A, 3); -+ registerSubVersion(MCVersions.V17W47A, 4); -+ registerSubVersion(MCVersions.V17W47A, 5); -+ registerSubVersion(MCVersions.V17W47A, 6); -+ registerSubVersion(MCVersions.V17W47A, 7); -+ -+ registerSubVersion(MCVersions.V24W04A + 1, 1); -+ registerSubVersion(MCVersions.V24W04A + 2, 1); -+ -+ registerSubVersion(MCVersions.V24W07A + 1, 1); -+ registerSubVersion(MCVersions.V24W07A + 1, 2); -+ registerSubVersion(MCVersions.V24W07A + 1, 4); -+ registerSubVersion(MCVersions.V24W07A + 1, 5); -+ registerSubVersion(MCVersions.V24W07A + 1, 6); -+ -+ // register breakpoints here -+ // for all major releases after 1.16, add them. this reduces the work required to determine if a breakpoint -+ // is needed for new converters -+ -+ // Too much changed in this version. -+ registerBreakpoint(MCVersions.V17W47A); -+ registerBreakpointAfter(MCVersions.V17W47A, Integer.MAX_VALUE); -+ -+ // final release of major version -+ registerBreakpointAfter(MCVersions.V1_17_1, Integer.MAX_VALUE); -+ -+ // final release of major version -+ registerBreakpointAfter(MCVersions.V1_18_2, Integer.MAX_VALUE); -+ -+ // final release of major version -+ registerBreakpointAfter(MCVersions.V1_19_4, Integer.MAX_VALUE); -+ -+ // Too much changed in this version. -+ registerBreakpoint(MCVersions.V24W07A + 1, 5); -+ registerBreakpointAfter(MCVersions.V24W07A + 1, Integer.MAX_VALUE); -+ } -+ -+ static { -+ final Field[] fields = MCVersions.class.getDeclaredFields(); -+ for (final Field field : fields) { -+ final String name = field.getName(); -+ final int value; -+ try { -+ value = field.getInt(null); -+ } catch (final Exception ex) { -+ throw new RuntimeException(ex); -+ } -+ -+ if (VERSION_NAMES.containsKey(value) && value != MCVersions.V15W33B) { // Mojang registered 15w33a and 15w33b under the same id. -+ LOGGER.warn("Error registering version \"" + name + "\", version number '" + value + "' is already associated with \"" + VERSION_NAMES.get(value) + "\""); -+ } -+ -+ VERSION_NAMES.put(value, name.substring(1).replace("_PRE", "-PRE").replace("_RC", "-RC").replace('_', '.').toLowerCase(Locale.ROOT)); -+ } -+ -+ for (final int version : DATACONVERTER_VERSIONS_MAJOR) { -+ if (VERSION_NAMES.containsKey(version)) { -+ continue; -+ } -+ -+ // find closest greatest version above this one -+ int closest = Integer.MAX_VALUE; -+ String closestName = null; -+ for (final int v : VERSION_NAMES.keySet()) { -+ if (v > version && v < closest) { -+ closest = v; -+ closestName = VERSION_NAMES.get(v); -+ } -+ } -+ -+ if (closestName == null) { -+ VERSION_NAMES.put(version, "unregistered_v" + version); -+ } else { -+ VERSION_NAMES.put(version, closestName + "-dev" + (closest - version)); -+ } -+ } -+ -+ // Explicit override for V99, as 99 is very special. -+ VERSION_NAMES.put(99, "pre_converter"); -+ -+ VERSION_LIST = new IntArrayList(new IntRBTreeSet(VERSION_NAMES.keySet())); -+ -+ DATA_VERSION_LIST = new LongArrayList(); -+ for (final int version : VERSION_LIST) { -+ DATA_VERSION_LIST.add(DataConverter.encodeVersions(version, 0)); -+ -+ final IntArrayList subVersions = SUBVERSIONS.get(version); -+ if (subVersions == null) { -+ continue; -+ } -+ -+ for (final int step : subVersions) { -+ DATA_VERSION_LIST.add(DataConverter.encodeVersions(version, step)); -+ } -+ } -+ -+ DATA_VERSION_LIST.sort((LongComparator)null); -+ -+ for (final int version : DATACONVERTER_VERSIONS_MAJOR) { -+ DATACONVERTER_VERSIONS.add(DataConverter.encodeVersions(version, 0)); -+ -+ final IntArrayList subVersions = SUBVERSIONS.get(version); -+ if (subVersions == null) { -+ continue; -+ } -+ -+ for (final int step : subVersions) { -+ DATACONVERTER_VERSIONS.add(DataConverter.encodeVersions(version, step)); -+ } -+ } -+ } -+ -+ private static void registerSubVersion(final int version, final int step) { -+ if (DATA_VERSION_LIST != null) { -+ throw new IllegalStateException("Added too late!"); -+ } -+ SUBVERSIONS.computeIfAbsent(version, (final int keyInMap) -> { -+ return new IntArrayList(); -+ }).add(step); -+ } -+ -+ private static void registerBreakpoint(final int version) { -+ registerBreakpoint(version, 0); -+ } -+ -+ private static void registerBreakpoint(final int version, final int step) { -+ BREAKPOINTS.add(DataConverter.encodeVersions(version, step)); -+ } -+ -+ private static void registerBreakpointAfter(final int version) { -+ registerBreakpointAfter(version, 0); -+ } -+ -+ private static void registerBreakpointAfter(final int version, final int step) { -+ BREAKPOINTS.add(DataConverter.encodeVersions(version, step) + 1L); -+ } -+ -+ // returns only versions that have dataconverters -+ public static boolean hasDataConverters(final int version) { -+ return DATACONVERTER_VERSIONS_MAJOR.contains(version); -+ } -+ -+ public String getVersionName(final int version) { -+ return VERSION_NAMES.get(version); -+ } -+ -+ public boolean isRegisteredVersion(final int version) { -+ return VERSION_NAMES.containsKey(version); -+ } -+ -+ public static IntArrayList getVersionList() { -+ return VERSION_LIST; -+ } -+ -+ public static LongArrayList getDataVersionList() { -+ return DATA_VERSION_LIST; -+ } -+ -+ public static int getMaxVersion() { -+ return VERSION_LIST.getInt(VERSION_LIST.size() - 1); -+ } -+ -+ public static LongArrayList getBreakpoints() { -+ return BREAKPOINTS; -+ } -+ -+ public static void checkVersion(final long version) { -+ if (!DATACONVERTER_VERSIONS.contains(version)) { -+ throw new IllegalStateException("Version " + DataConverter.encodedToString(version) + " is not registered to have dataconverters, yet has a dataconverter"); -+ } -+ } -+ -+ private MCVersionRegistry() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCVersions.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCVersions.java -new file mode 100644 -index 0000000000000000000000000000000000000000..dbd232869253c4b06c09a52c2d7ad64fbbc3b71c ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/MCVersions.java -@@ -0,0 +1,525 @@ -+package ca.spottedleaf.dataconverter.minecraft; -+ -+@SuppressWarnings("unused") -+public final class MCVersions { -+ -+ /* https://minecraft.wiki/wiki/Data_version */ -+ -+ public static final int V15W32A = 100; -+ public static final int V15W32B = 103; -+ public static final int V15W32C = 104; -+ public static final int V15W33A = 111; -+ public static final int V15W33B = 111; -+ public static final int V15W33C = 112; -+ public static final int V15W34A = 114; -+ public static final int V15W34B = 115; -+ public static final int V15W34C = 116; -+ public static final int V15W34D = 117; -+ public static final int V15W35A = 118; -+ public static final int V15W35B = 119; -+ public static final int V15W35C = 120; -+ public static final int V15W35D = 121; -+ public static final int V15W35E = 122; -+ public static final int V15W36A = 123; -+ public static final int V15W36B = 124; -+ public static final int V15W36C = 125; -+ public static final int V15W36D = 126; -+ public static final int V15W37A = 127; -+ public static final int V15W38A = 128; -+ public static final int V15W38B = 129; -+ public static final int V15W39A = 130; -+ public static final int V15W39B = 131; -+ public static final int V15W39C = 132; -+ public static final int V15W40A = 133; -+ public static final int V15W40B = 134; -+ public static final int V15W41A = 136; -+ public static final int V15W41B = 137; -+ public static final int V15W42A = 138; -+ public static final int V15W43A = 139; -+ public static final int V15W43B = 140; -+ public static final int V15W43C = 141; -+ public static final int V15W44A = 142; -+ public static final int V15W44B = 143; -+ public static final int V15W45A = 145; -+ public static final int V15W46A = 146; -+ public static final int V15W47A = 148; -+ public static final int V15W47B = 149; -+ public static final int V15W47C = 150; -+ public static final int V15W49A = 151; -+ public static final int V15W49B = 152; -+ public static final int V15W50A = 153; -+ public static final int V15W51A = 154; -+ public static final int V15W51B = 155; -+ public static final int V16W02A = 156; -+ public static final int V16W03A = 157; -+ public static final int V16W04A = 158; -+ public static final int V16W05A = 159; -+ public static final int V16W05B = 160; -+ public static final int V16W06A = 161; -+ public static final int V16W07A = 162; -+ public static final int V16W07B = 163; -+ public static final int V1_9_PRE1 = 164; -+ public static final int V1_9_PRE2 = 165; -+ public static final int V1_9_PRE3 = 167; -+ public static final int V1_9_PRE4 = 168; -+ public static final int V1_9 = 169; -+ public static final int V1_9_1_PRE1 = 170; -+ public static final int V1_9_1_PRE2 = 171; -+ public static final int V1_9_1_PRE3 = 172; -+ public static final int V1_9_1 = 175; -+ public static final int V1_9_2 = 176; -+ public static final int V16W14A = 177; -+ public static final int V16W15A = 178; -+ public static final int V16W15B = 179; -+ public static final int V1_9_3_PRE1 = 180; -+ public static final int V1_9_3_PRE2 = 181; -+ public static final int V1_9_3_PRE3 = 182; -+ public static final int V1_9_3 = 183; -+ public static final int V1_9_4 = 184; -+ public static final int V16W20A = 501; -+ public static final int V16W21A = 503; -+ public static final int V16W21B = 504; -+ public static final int V1_10_PRE1 = 506; -+ public static final int V1_10_PRE2 = 507; -+ public static final int V1_10 = 510; -+ public static final int V1_10_1 = 511; -+ public static final int V1_10_2 = 512; -+ public static final int V16W32A = 800; -+ public static final int V16W32B = 801; -+ public static final int V16W33A = 802; -+ public static final int V16W35A = 803; -+ public static final int V16W36A = 805; -+ public static final int V16W38A = 807; -+ public static final int V16W39A = 809; -+ public static final int V16W39B = 811; -+ public static final int V16W39C = 812; -+ public static final int V16W40A = 813; -+ public static final int V16W41A = 814; -+ public static final int V16W42A = 815; -+ public static final int V16W43A = 816; -+ public static final int V16W44A = 817; -+ public static final int V1_11_PRE1 = 818; -+ public static final int V1_11 = 819; -+ public static final int V16W50A = 920; -+ public static final int V1_11_1 = 921; -+ public static final int V1_11_2 = 922; -+ public static final int V17W06A = 1022; -+ public static final int V17W13A = 1122; -+ public static final int V17W13B = 1123; -+ public static final int V17W14A = 1124; -+ public static final int V17W15A = 1125; -+ public static final int V17W16A = 1126; -+ public static final int V17W16B = 1127; -+ public static final int V17W17A = 1128; -+ public static final int V17W17B = 1129; -+ public static final int V17W18A = 1130; -+ public static final int V17W18B = 1131; -+ public static final int V1_12_PRE1 = 1132; -+ public static final int V1_12_PRE2 = 1133; -+ public static final int V1_12_PRE3 = 1134; -+ public static final int V1_12_PRE4 = 1135; -+ public static final int V1_12_PRE5 = 1136; -+ public static final int V1_12_PRE6 = 1137; -+ public static final int V1_12_PRE7 = 1138; -+ public static final int V1_12 = 1139; -+ public static final int V17W31A = 1239; -+ public static final int V1_12_1_PRE1 = 1240; -+ public static final int V1_12_1 = 1241; -+ public static final int V1_12_2_PRE1 = 1341; -+ public static final int V1_12_2_PRE2 = 1342; -+ public static final int V1_12_2 = 1343; -+ public static final int V17W43A = 1444; -+ public static final int V17W43B = 1445; -+ public static final int V17W45A = 1447; -+ public static final int V17W45B = 1448; -+ public static final int V17W46A = 1449; -+ public static final int V17W47A = 1451; -+ public static final int V17W47B = 1452; -+ public static final int V17W48A = 1453; -+ public static final int V17W49A = 1454; -+ public static final int V17W49B = 1455; -+ public static final int V17W50A = 1457; -+ public static final int V18W01A = 1459; -+ public static final int V18W02A = 1461; -+ public static final int V18W03A = 1462; -+ public static final int V18W03B = 1463; -+ public static final int V18W05A = 1464; -+ public static final int V18W06A = 1466; -+ public static final int V18W07A = 1467; -+ public static final int V18W07B = 1468; -+ public static final int V18W07C = 1469; -+ public static final int V18W08A = 1470; -+ public static final int V18W08B = 1471; -+ public static final int V18W09A = 1472; -+ public static final int V18W10A = 1473; -+ public static final int V18W10B = 1474; -+ public static final int V18W10C = 1476; -+ public static final int V18W10D = 1477; -+ public static final int V18W11A = 1478; -+ public static final int V18W14A = 1479; -+ public static final int V18W14B = 1481; -+ public static final int V18W15A = 1482; -+ public static final int V18W16A = 1483; -+ public static final int V18W19A = 1484; -+ public static final int V18W19B = 1485; -+ public static final int V18W20A = 1489; -+ public static final int V18W20B = 1491; -+ public static final int V18W20C = 1493; -+ public static final int V18W21A = 1495; -+ public static final int V18W21B = 1496; -+ public static final int V18W22A = 1497; -+ public static final int V18W22B = 1498; -+ public static final int V18W22C = 1499; -+ public static final int V1_13_PRE1 = 1501; -+ public static final int V1_13_PRE2 = 1502; -+ public static final int V1_13_PRE3 = 1503; -+ public static final int V1_13_PRE4 = 1504; -+ public static final int V1_13_PRE5 = 1511; -+ public static final int V1_13_PRE6 = 1512; -+ public static final int V1_13_PRE7 = 1513; -+ public static final int V1_13_PRE8 = 1516; -+ public static final int V1_13_PRE9 = 1517; -+ public static final int V1_13_PRE10 = 1518; -+ public static final int V1_13 = 1519; -+ public static final int V18W30A = 1620; -+ public static final int V18W30B = 1621; -+ public static final int V18W31A = 1622; -+ public static final int V18W32A = 1623; -+ public static final int V18W33A = 1625; -+ public static final int V1_13_1_PRE1 = 1626; -+ public static final int V1_13_1_PRE2 = 1627; -+ public static final int V1_13_1 = 1628; -+ public static final int V1_13_2_PRE1 = 1629; -+ public static final int V1_13_2_PRE2 = 1630; -+ public static final int V1_13_2 = 1631; -+ public static final int V18W43A = 1901; -+ public static final int V18W43B = 1902; -+ public static final int V18W43C = 1903; -+ public static final int V18W44A = 1907; -+ public static final int V18W45A = 1908; -+ public static final int V18W46A = 1910; -+ public static final int V18W47A = 1912; -+ public static final int V18W47B = 1913; -+ public static final int V18W48A = 1914; -+ public static final int V18W48B = 1915; -+ public static final int V18W49A = 1916; -+ public static final int V18W50A = 1919; -+ public static final int V19W02A = 1921; -+ public static final int V19W03A = 1922; -+ public static final int V19W03B = 1923; -+ public static final int V19W03C = 1924; -+ public static final int V19W04A = 1926; -+ public static final int V19W04B = 1927; -+ public static final int V19W05A = 1930; -+ public static final int V19W06A = 1931; -+ public static final int V19W07A = 1932; -+ public static final int V19W08A = 1933; -+ public static final int V19W08B = 1934; -+ public static final int V19W09A = 1935; -+ public static final int V19W11A = 1937; -+ public static final int V19W11B = 1938; -+ public static final int V19W12A = 1940; -+ public static final int V19W12B = 1941; -+ public static final int V19W13A = 1942; -+ public static final int V19W13B = 1943; -+ public static final int V19W14A = 1944; -+ public static final int V19W14B = 1945; -+ public static final int V1_14_PRE1 = 1947; -+ public static final int V1_14_PRE2 = 1948; -+ public static final int V1_14_PRE3 = 1949; -+ public static final int V1_14_PRE4 = 1950; -+ public static final int V1_14_PRE5 = 1951; -+ public static final int V1_14 = 1952; -+ public static final int V1_14_1_PRE1 = 1955; -+ public static final int V1_14_1_PRE2 = 1956; -+ public static final int V1_14_1 = 1957; -+ public static final int V1_14_2_PRE1 = 1958; -+ public static final int V1_14_2_PRE2 = 1959; -+ public static final int V1_14_2_PRE3 = 1960; -+ public static final int V1_14_2_PRE4 = 1962; -+ public static final int V1_14_2 = 1963; -+ public static final int V1_14_3_PRE1 = 1964; -+ public static final int V1_14_3_PRE2 = 1965; -+ public static final int V1_14_3_PRE3 = 1966; -+ public static final int V1_14_3_PRE4 = 1967; -+ public static final int V1_14_3 = 1968; -+ public static final int V1_14_4_PRE1 = 1969; -+ public static final int V1_14_4_PRE2 = 1970; -+ public static final int V1_14_4_PRE3 = 1971; -+ public static final int V1_14_4_PRE4 = 1972; -+ public static final int V1_14_4_PRE5 = 1973; -+ public static final int V1_14_4_PRE6 = 1974; -+ public static final int V1_14_4_PRE7 = 1975; -+ public static final int V1_14_4 = 1976; -+ public static final int V19W34A = 2200; -+ public static final int V19W35A = 2201; -+ public static final int V19W36A = 2203; -+ public static final int V19W37A = 2204; -+ public static final int V19W38A = 2205; -+ public static final int V19W38B = 2206; -+ public static final int V19W39A = 2207; -+ public static final int V19W40A = 2208; -+ public static final int V19W41A = 2210; -+ public static final int V19W42A = 2212; -+ public static final int V19W44A = 2213; -+ public static final int V19W45A = 2214; -+ public static final int V19W45B = 2215; -+ public static final int V19W46A = 2216; -+ public static final int V19W46B = 2217; -+ public static final int V1_15_PRE1 = 2218; -+ public static final int V1_15_PRE2 = 2219; -+ public static final int V1_15_PRE3 = 2220; -+ public static final int V1_15_PRE4 = 2221; -+ public static final int V1_15_PRE5 = 2222; -+ public static final int V1_15_PRE6 = 2223; -+ public static final int V1_15_PRE7 = 2224; -+ public static final int V1_15 = 2225; -+ public static final int V1_15_1_PRE1 = 2226; -+ public static final int V1_15_1 = 2227; -+ public static final int V1_15_2_PRE1 = 2228; -+ public static final int V1_15_2_PRE2 = 2229; -+ public static final int V1_15_2 = 2230; -+ public static final int V20W06A = 2504; -+ public static final int V20W07A = 2506; -+ public static final int V20W08A = 2507; -+ public static final int V20W09A = 2510; -+ public static final int V20W10A = 2512; -+ public static final int V20W11A = 2513; -+ public static final int V20W12A = 2515; -+ public static final int V20W13A = 2520; -+ public static final int V20W13B = 2521; -+ public static final int V20W14A = 2524; -+ public static final int V20W15A = 2525; -+ public static final int V20W16A = 2526; -+ public static final int V20W17A = 2529; -+ public static final int V20W18A = 2532; -+ public static final int V20W19A = 2534; -+ public static final int V20W20A = 2536; -+ public static final int V20W20B = 2537; -+ public static final int V20W21A = 2554; -+ public static final int V20W22A = 2555; -+ public static final int V1_16_PRE1 = 2556; -+ public static final int V1_16_PRE2 = 2557; -+ public static final int V1_16_PRE3 = 2559; -+ public static final int V1_16_PRE4 = 2560; -+ public static final int V1_16_PRE5 = 2561; -+ public static final int V1_16_PRE6 = 2562; -+ public static final int V1_16_PRE7 = 2563; -+ public static final int V1_16_PRE8 = 2564; -+ public static final int V1_16_RC1 = 2565; -+ public static final int V1_16 = 2566; -+ public static final int V1_16_1 = 2567; -+ public static final int V20W27A = 2569; -+ public static final int V20W28A = 2570; -+ public static final int V20W29A = 2571; -+ public static final int V20W30A = 2572; -+ public static final int V1_16_2_PRE1 = 2573; -+ public static final int V1_16_2_PRE2 = 2574; -+ public static final int V1_16_2_PRE3 = 2575; -+ public static final int V1_16_2_RC1 = 2576; -+ public static final int V1_16_2_RC2 = 2577; -+ public static final int V1_16_2 = 2578; -+ public static final int V1_16_3_RC1 = 2579; -+ public static final int V1_16_3 = 2580; -+ public static final int V1_16_4_PRE1 = 2581; -+ public static final int V1_16_4_PRE2 = 2582; -+ public static final int V1_16_4_RC1 = 2583; -+ public static final int V1_16_4 = 2584; -+ public static final int V1_16_5_RC1 = 2585; -+ public static final int V1_16_5 = 2586; -+ public static final int V20W45A = 2681; -+ public static final int V20W46A = 2682; -+ public static final int V20W48A = 2683; -+ public static final int V20W49A = 2685; -+ public static final int V20W51A = 2687; -+ public static final int V21W03A = 2689; -+ public static final int V21W05A = 2690; -+ public static final int V21W05B = 2692; -+ public static final int V21W06A = 2694; -+ public static final int V21W07A = 2695; -+ public static final int V21W08A = 2697; -+ public static final int V21W08B = 2698; -+ public static final int V21W10A = 2699; -+ public static final int V21W11A = 2703; -+ public static final int V21W13A = 2705; -+ public static final int V21W14A = 2706; -+ public static final int V21W15A = 2709; -+ public static final int V21W16A = 2711; -+ public static final int V21W17A = 2712; -+ public static final int V21W18A = 2713; -+ public static final int V21W19A = 2714; -+ public static final int V21W20A = 2715; -+ public static final int V1_17_PRE1 = 2716; -+ public static final int V1_17_PRE2 = 2718; -+ public static final int V1_17_PRE3 = 2719; -+ public static final int V1_17_PRE4 = 2720; -+ public static final int V1_17_PRE5 = 2721; -+ public static final int V1_17_RC1 = 2722; -+ public static final int V1_17_RC2 = 2723; -+ public static final int V1_17 = 2724; -+ public static final int V1_17_1_PRE1 = 2725; -+ public static final int V1_17_1_PRE2 = 2726; -+ public static final int V1_17_1_PRE3 = 2727; -+ public static final int V1_17_1_RC1 = 2728; -+ public static final int V1_17_1_RC2 = 2729; -+ public static final int V1_17_1 = 2730; -+ public static final int V21W37A = 2834; -+ public static final int V21W38A = 2835; -+ public static final int V21W39A = 2836; -+ public static final int V21W40A = 2838; -+ public static final int V21W41A = 2839; -+ public static final int V21W42A = 2840; -+ public static final int V21W43A = 2844; -+ public static final int V21W44A = 2845; -+ public static final int V1_18_PRE1 = 2847; -+ public static final int V1_18_PRE2 = 2848; -+ public static final int V1_18_PRE3 = 2849; -+ public static final int V1_18_PRE4 = 2850; -+ public static final int V1_18_PRE5 = 2851; -+ public static final int V1_18_PRE6 = 2853; -+ public static final int V1_18_PRE7 = 2854; -+ public static final int V1_18_PRE8 = 2855; -+ public static final int V1_18_RC1 = 2856; -+ public static final int V1_18_RC2 = 2857; -+ public static final int V1_18_RC3 = 2858; -+ public static final int V1_18_RC4 = 2859; -+ public static final int V1_18 = 2860; -+ public static final int V1_18_1_PRE1 = 2861; -+ public static final int V1_18_1_RC1 = 2862; -+ public static final int V1_18_1_RC2 = 2863; -+ public static final int V1_18_1_RC3 = 2864; -+ public static final int V1_18_1 = 2865; -+ public static final int V22W03A = 2966; -+ public static final int V22W05A = 2967; -+ public static final int V22W06A = 2968; -+ public static final int V22W07A = 2969; -+ public static final int V1_18_2_PRE1 = 2971; -+ public static final int V1_18_2_PRE2 = 2972; -+ public static final int V1_18_2_PRE3 = 2973; -+ public static final int V1_18_2_RC1 = 2974; -+ public static final int V1_18_2 = 2975; -+ public static final int V22W11A = 3080; -+ public static final int V22W12A = 3082; -+ public static final int V22W13A = 3085; -+ public static final int V22W14A = 3088; -+ public static final int V22W15A = 3089; -+ public static final int V22W16A = 3091; -+ public static final int V22W16B = 3092; -+ public static final int V22W17A = 3093; -+ public static final int V22W18A = 3095; -+ public static final int V22W19A = 3096; -+ public static final int V1_19_PRE1 = 3098; -+ public static final int V1_19_PRE2 = 3099; -+ public static final int V1_19_PRE3 = 3100; -+ public static final int V1_19_PRE4 = 3101; -+ public static final int V1_19_PRE5 = 3102; -+ public static final int V1_19_RC1 = 3103; -+ public static final int V1_19_RC2 = 3104; -+ public static final int V1_19 = 3105; -+ public static final int V22W24A = 3106; -+ public static final int V1_19_1_PRE1 = 3107; -+ public static final int V1_19_1_RC1 = 3109; -+ public static final int V1_19_1_PRE2 = 3110; -+ public static final int V1_19_1_PRE3 = 3111; -+ public static final int V1_19_1_PRE4 = 3112; -+ public static final int V1_19_1_PRE5 = 3113; -+ public static final int V1_19_1_PRE6 = 3114; -+ public static final int V1_19_1_RC2 = 3115; -+ public static final int V1_19_1_RC3 = 3116; -+ public static final int V1_19_1 = 3117; -+ public static final int V1_19_2_RC1 = 3118; -+ public static final int V1_19_2_RC2 = 3119; -+ public static final int V1_19_2 = 3120; -+ public static final int V22W42A = 3205; -+ public static final int V22W43A = 3206; -+ public static final int V22W44A = 3207; -+ public static final int V22W45A = 3208; -+ public static final int V22W46A = 3210; -+ public static final int V1_19_3_PRE1 = 3211; -+ public static final int V1_19_3_PRE2 = 3212; -+ public static final int V1_19_3_PRE3 = 3213; -+ public static final int V1_19_3_RC1 = 3215; -+ public static final int V1_19_3 = 3218; -+ public static final int V23W03A = 3320; -+ public static final int V23W04A = 3321; -+ public static final int V23W05A = 3323; -+ public static final int V23W06A = 3326; -+ public static final int V23W07A = 3329; -+ public static final int V1_19_4_PRE1 = 3330; -+ public static final int V1_19_4_PRE2 = 3331; -+ public static final int V1_19_4_PRE3 = 3332; -+ public static final int V1_19_4_PRE4 = 3333; -+ public static final int V1_19_4_RC1 = 3334; -+ public static final int V1_19_4_RC2 = 3335; -+ public static final int V1_19_4_RC3 = 3336; -+ public static final int V1_19_4 = 3337; -+ public static final int V23W12A = 3442; -+ public static final int V23W13A = 3443; -+ public static final int V23W14A = 3445; -+ public static final int V23W16A = 3449; -+ public static final int V23W17A = 3452; -+ public static final int V23W19A = 3453; -+ public static final int V1_20_PRE1 = 3454; -+ public static final int V1_20_PRE2 = 3455; -+ public static final int V1_20_PRE3 = 3456; -+ public static final int V1_20_PRE4 = 3457; -+ public static final int V1_20_PRE5 = 3458; -+ public static final int V1_20_PRE6 = 3460; -+ public static final int V1_20_PRE7 = 3461; -+ public static final int V1_20_RC1 = 3462; -+ public static final int V1_20 = 3463; -+ public static final int V1_20_1_RC1 = 3464; -+ public static final int V1_20_1 = 3465; -+ public static final int V23W31A = 3567; -+ public static final int V23W32A = 3569; -+ public static final int V23W33A = 3570; -+ public static final int V23W35A = 3571; -+ public static final int V1_20_2_PRE1 = 3572; -+ public static final int V1_20_2_PRE2 = 3573; -+ public static final int V1_20_2_PRE3 = 3574; -+ public static final int V1_20_2_PRE4 = 3575; -+ public static final int V1_20_2_RC1 = 3576; -+ public static final int V1_20_2_RC2 = 3577; -+ public static final int V1_20_2 = 3578; -+ public static final int V23W40A = 3679; -+ public static final int V23W41A = 3681; -+ public static final int V23W42A = 3684; -+ public static final int V23W43A = 3686; -+ public static final int V23W43B = 3687; -+ public static final int V23W44A = 3688; -+ public static final int V23W45A = 3690; -+ public static final int V23W46A = 3691; -+ public static final int V1_20_3_PRE1 = 3693; -+ public static final int V1_20_3_PRE2 = 3694; -+ public static final int V1_20_3_PRE3 = 3695; -+ public static final int V1_20_3_PRE4 = 3696; -+ public static final int V1_20_3_RC1 = 3697; -+ public static final int V1_20_3 = 3698; -+ public static final int V1_20_4_RC1 = 3699; -+ public static final int V1_20_4 = 3700; -+ public static final int V23W51A = 3801; -+ public static final int V23W51B = 3802; -+ public static final int V24W03A = 3804; -+ public static final int V24W03B = 3805; -+ public static final int V24W04A = 3806; -+ public static final int V24W05A = 3809; -+ public static final int V24W05B = 3811; -+ public static final int V24W06A = 3815; -+ public static final int V24W07A = 3817; -+ public static final int V24W09A = 3819; -+ public static final int V24W10A = 3821; -+ public static final int V24W11A = 3823; -+ public static final int V24W12A = 3824; -+ public static final int V24W13A = 3826; -+ public static final int V24W14A = 3827; -+ public static final int V1_20_5_PRE1 = 3829; -+ public static final int V1_20_5_PRE2 = 3830; -+ public static final int V1_20_5_PRE3 = 3831; -+ public static final int V1_20_5_PRE4 = 3832; -+ public static final int V1_20_5_RC1 = 3834; -+ public static final int V1_20_5_RC2 = 3835; -+ public static final int V1_20_5_RC3 = 3836; -+ public static final int V1_20_5 = 3837; -+ -+ private MCVersions() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterAbstractAdvancementsRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterAbstractAdvancementsRename.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ae3aed21c1fccb688e9a1665e2d317a77508d157 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterAbstractAdvancementsRename.java -@@ -0,0 +1,28 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.advancements; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import java.util.ArrayList; -+import java.util.function.Function; -+ -+public final class ConverterAbstractAdvancementsRename { -+ -+ private ConverterAbstractAdvancementsRename() {} -+ -+ public static void register(final int version, final Function renamer) { -+ register(version, 0, renamer); -+ } -+ -+ public static void register(final int version, final int subVersion, final Function renamer) { -+ MCTypeRegistry.ADVANCEMENTS.addStructureConverter(new DataConverter<>(version, subVersion) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ RenameHelper.renameKeys(data, renamer); -+ return null; -+ } -+ }); -+ } -+ -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterCriteriaRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterCriteriaRename.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b2a4d16e6a2f9d71dbfa692922671581c2bec136 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/advancements/ConverterCriteriaRename.java -@@ -0,0 +1,42 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.advancements; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.types.MapType; -+import java.util.function.Function; -+ -+public final class ConverterCriteriaRename extends DataConverter, MapType> { -+ -+ public final String path; -+ public final Function conversion; -+ -+ public ConverterCriteriaRename(final int toVersion, final String path, final Function conversion) { -+ super(toVersion); -+ this.path = path; -+ this.conversion = conversion; -+ } -+ -+ public ConverterCriteriaRename(final int toVersion, final int versionStep, final String path, final Function conversion) { -+ super(toVersion, versionStep); -+ this.path = path; -+ this.conversion = conversion; -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType advancement = data.getMap(this.path); -+ if (advancement == null) { -+ return null; -+ } -+ -+ final MapType criteria = advancement.getMap("criteria"); -+ if (criteria == null) { -+ return null; -+ } -+ -+ RenameHelper.renameKeys(criteria, this.conversion); -+ -+ return null; -+ } -+ -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractAttributesRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractAttributesRename.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c465036923656e8c974cc447bb57169b1a882e88 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/attributes/ConverterAbstractAttributesRename.java -@@ -0,0 +1,57 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.attributes; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import java.util.function.Function; -+ -+public final class ConverterAbstractAttributesRename { -+ -+ public static void register(final int version, final Function renamer) { -+ register(version, 0, renamer); -+ } -+ -+ public static void register(final int version, final int versionStep, final Function renamer) { -+ final DataConverter, MapType> entityConverter = new DataConverter<>(version, versionStep) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final ListType attributes = data.getList("Attributes", ObjectType.MAP); -+ -+ if (attributes == null) { -+ return null; -+ } -+ -+ for (int i = 0, len = attributes.size(); i < len; ++i) { -+ RenameHelper.renameString(attributes.getMap(i), "Name", renamer); -+ } -+ -+ return null; -+ } -+ }; -+ -+ MCTypeRegistry.ENTITY.addStructureConverter(entityConverter); -+ MCTypeRegistry.PLAYER.addStructureConverter(entityConverter); -+ -+ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(version, versionStep) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final ListType attributes = data.getList("AttributeModifiers", ObjectType.MAP); -+ -+ if (attributes == null) { -+ return null; -+ } -+ -+ for (int i = 0, len = attributes.size(); i < len; ++i) { -+ RenameHelper.renameString(attributes.getMap(i), "AttributeName", renamer); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private ConverterAbstractAttributesRename() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/blockname/ConverterAbstractBlockRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/blockname/ConverterAbstractBlockRename.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7b47879a7c2e8c21fae43bf5247585c716d75565 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/blockname/ConverterAbstractBlockRename.java -@@ -0,0 +1,64 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.blockname; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import java.util.function.Function; -+ -+public final class ConverterAbstractBlockRename { -+ -+ private ConverterAbstractBlockRename() {} -+ -+ public static void register(final int version, final Function renamer) { -+ register(version, 0, renamer); -+ } -+ -+ public static void register(final int version, final int subVersion, final Function renamer) { -+ ConverterAbstractStringValueTypeRename.register(version, subVersion, MCTypeRegistry.BLOCK_NAME, renamer); -+ MCTypeRegistry.BLOCK_STATE.addStructureConverter(new DataConverter<>(version, subVersion) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final String name = data.getString("Name"); -+ if (name != null) { -+ final String converted = renamer.apply(name); -+ if (converted != null) { -+ data.setString("Name", converted); -+ } -+ } -+ return null; -+ } -+ }); -+ MCTypeRegistry.FLAT_BLOCK_STATE.addConverter(new DataConverter<>(version, subVersion) { -+ @Override -+ public Object convert(final Object data, final long sourceVersion, final long toVersion) { -+ if (!(data instanceof String string)) { -+ return null; -+ } -+ -+ if (string.isEmpty()) { -+ return null; -+ } -+ -+ final int nbtStart1 = string.indexOf('['); -+ final int nbtStart2 = string.indexOf('{'); -+ int stateNameEnd = string.length(); -+ if (nbtStart1 > 0) { -+ stateNameEnd = nbtStart1; -+ } -+ -+ if (nbtStart2 > 0) { -+ stateNameEnd = Math.min(stateNameEnd, nbtStart2); -+ } -+ -+ final String blockStateName = string.substring(0, stateNameEnd); -+ final String converted = renamer.apply(blockStateName); -+ if (converted == null) { -+ return null; -+ } -+ -+ return converted.concat(string.substring(stateNameEnd)); -+ } -+ }); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterAddBlendingData.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterAddBlendingData.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d4cd5362e77eb71cb8eb45ffcc73185e01be1157 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterAddBlendingData.java -@@ -0,0 +1,65 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.chunk; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.Types; -+import ca.spottedleaf.dataconverter.util.NamespaceUtil; -+import java.util.Arrays; -+import java.util.HashSet; -+import java.util.Set; -+ -+public final class ConverterAddBlendingData extends DataConverter, MapType> { -+ -+ private static final Set STATUSES_TO_SKIP_BLENDING = new HashSet<>( -+ Arrays.asList( -+ "minecraft:empty", -+ "minecraft:structure_starts", -+ "minecraft:structure_references", -+ "minecraft:biomes" -+ ) -+ ); -+ -+ public ConverterAddBlendingData(final int toVersion) { -+ super(toVersion); -+ } -+ -+ public ConverterAddBlendingData(final int toVersion, final int versionStep) { -+ super(toVersion, versionStep); -+ } -+ -+ private static MapType createBlendingData(final int height, final int minY) { -+ final MapType ret = Types.NBT.createEmptyMap(); -+ -+ ret.setInt("min_section", minY >> 4); -+ ret.setInt("max_section", (minY + height) >> 4); -+ -+ return ret; -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ data.remove("blending_data"); -+ final MapType context = data.getMap("__context"); -+ if (!"minecraft:overworld".equals(context == null ? null : context.getString("dimension"))) { -+ return null; -+ } -+ -+ final String status = NamespaceUtil.correctNamespace(data.getString("Status")); -+ if (status == null) { -+ return null; -+ } -+ -+ final MapType belowZeroRetrogen = data.getMap("below_zero_retrogen"); -+ -+ if (!STATUSES_TO_SKIP_BLENDING.contains(status)) { -+ data.setMap("blending_data", createBlendingData(384, -64)); -+ } else if (belowZeroRetrogen != null) { -+ final String realStatus = NamespaceUtil.correctNamespace(belowZeroRetrogen.getString("target_status", "empty")); -+ if (!STATUSES_TO_SKIP_BLENDING.contains(realStatus)) { -+ data.setMap("blending_data", createBlendingData(256, 0)); -+ } -+ } -+ -+ return null; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterFlattenChunk.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterFlattenChunk.java -new file mode 100644 -index 0000000000000000000000000000000000000000..300c2d14818b1e0cfe7341aba573ec75d0581b26 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterFlattenChunk.java -@@ -0,0 +1,1016 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.chunk; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.HelperBlockFlatteningV1450; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.HelperItemNameV102; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.Types; -+import com.mojang.datafixers.DataFixUtils; -+import com.mojang.logging.LogUtils; -+import it.unimi.dsi.fastutil.ints.Int2ObjectLinkedOpenHashMap; -+import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -+import it.unimi.dsi.fastutil.ints.IntArrayList; -+import it.unimi.dsi.fastutil.ints.IntIterator; -+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; -+import net.minecraft.util.datafix.PackedBitStorage; -+import org.slf4j.Logger; -+import java.util.Arrays; -+import java.util.BitSet; -+import java.util.HashMap; -+import java.util.Iterator; -+import java.util.Map; -+import java.util.Objects; -+ -+import static it.unimi.dsi.fastutil.HashCommon.arraySize; -+ -+public final class ConverterFlattenChunk extends DataConverter, MapType> { -+ -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ -+ static final BitSet VIRTUAL_SET = new BitSet(256); -+ static final BitSet IDS_NEEDING_FIX_SET = new BitSet(256); -+ -+ static { -+ IDS_NEEDING_FIX_SET.set(2); -+ IDS_NEEDING_FIX_SET.set(3); -+ IDS_NEEDING_FIX_SET.set(110); -+ IDS_NEEDING_FIX_SET.set(140); -+ IDS_NEEDING_FIX_SET.set(144); -+ IDS_NEEDING_FIX_SET.set(25); -+ IDS_NEEDING_FIX_SET.set(86); -+ IDS_NEEDING_FIX_SET.set(26); -+ IDS_NEEDING_FIX_SET.set(176); -+ IDS_NEEDING_FIX_SET.set(177); -+ IDS_NEEDING_FIX_SET.set(175); -+ IDS_NEEDING_FIX_SET.set(64); -+ IDS_NEEDING_FIX_SET.set(71); -+ IDS_NEEDING_FIX_SET.set(193); -+ IDS_NEEDING_FIX_SET.set(194); -+ IDS_NEEDING_FIX_SET.set(195); -+ IDS_NEEDING_FIX_SET.set(196); -+ IDS_NEEDING_FIX_SET.set(197); -+ -+ VIRTUAL_SET.set(54); -+ VIRTUAL_SET.set(146); -+ VIRTUAL_SET.set(25); -+ VIRTUAL_SET.set(26); -+ VIRTUAL_SET.set(51); -+ VIRTUAL_SET.set(53); -+ VIRTUAL_SET.set(67); -+ VIRTUAL_SET.set(108); -+ VIRTUAL_SET.set(109); -+ VIRTUAL_SET.set(114); -+ VIRTUAL_SET.set(128); -+ VIRTUAL_SET.set(134); -+ VIRTUAL_SET.set(135); -+ VIRTUAL_SET.set(136); -+ VIRTUAL_SET.set(156); -+ VIRTUAL_SET.set(163); -+ VIRTUAL_SET.set(164); -+ VIRTUAL_SET.set(180); -+ VIRTUAL_SET.set(203); -+ VIRTUAL_SET.set(55); -+ VIRTUAL_SET.set(85); -+ VIRTUAL_SET.set(113); -+ VIRTUAL_SET.set(188); -+ VIRTUAL_SET.set(189); -+ VIRTUAL_SET.set(190); -+ VIRTUAL_SET.set(191); -+ VIRTUAL_SET.set(192); -+ VIRTUAL_SET.set(93); -+ VIRTUAL_SET.set(94); -+ VIRTUAL_SET.set(101); -+ VIRTUAL_SET.set(102); -+ VIRTUAL_SET.set(160); -+ VIRTUAL_SET.set(106); -+ VIRTUAL_SET.set(107); -+ VIRTUAL_SET.set(183); -+ VIRTUAL_SET.set(184); -+ VIRTUAL_SET.set(185); -+ VIRTUAL_SET.set(186); -+ VIRTUAL_SET.set(187); -+ VIRTUAL_SET.set(132); -+ VIRTUAL_SET.set(139); -+ VIRTUAL_SET.set(199); -+ } -+ -+ static final boolean[] VIRTUAL = toBooleanArray(VIRTUAL_SET); -+ static final boolean[] IDS_NEEDING_FIX = toBooleanArray(IDS_NEEDING_FIX_SET); -+ -+ private static boolean[] toBooleanArray(final BitSet set) { -+ final boolean[] ret = new boolean[4096]; -+ for (int i = 0; i < 4096; ++i) { -+ ret[i] = set.get(i); -+ } -+ -+ return ret; -+ } -+ -+ static final MapType PUMPKIN = HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:pumpkin'}"); -+ static final MapType SNOWY_PODZOL = HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:podzol',Properties:{snowy:'true'}}"); -+ static final MapType SNOWY_GRASS = HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:grass_block',Properties:{snowy:'true'}}"); -+ static final MapType SNOWY_MYCELIUM = HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:mycelium',Properties:{snowy:'true'}}"); -+ static final MapType UPPER_SUNFLOWER = HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:sunflower',Properties:{half:'upper'}}"); -+ static final MapType UPPER_LILAC = HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:lilac',Properties:{half:'upper'}}"); -+ static final MapType UPPER_TALL_GRASS = HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:tall_grass',Properties:{half:'upper'}}"); -+ static final MapType UPPER_LARGE_FERN = HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:large_fern',Properties:{half:'upper'}}"); -+ static final MapType UPPER_ROSE_BUSH = HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:rose_bush',Properties:{half:'upper'}}"); -+ static final MapType UPPER_PEONY = HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:peony',Properties:{half:'upper'}}"); -+ -+ static final Map> FLOWER_POT_MAP = new HashMap<>(); -+ static { -+ FLOWER_POT_MAP.put("minecraft:air0", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:flower_pot'}")); -+ FLOWER_POT_MAP.put("minecraft:red_flower0", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_poppy'}")); -+ FLOWER_POT_MAP.put("minecraft:red_flower1", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_blue_orchid'}")); -+ FLOWER_POT_MAP.put("minecraft:red_flower2", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_allium'}")); -+ FLOWER_POT_MAP.put("minecraft:red_flower3", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_azure_bluet'}")); -+ FLOWER_POT_MAP.put("minecraft:red_flower4", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_red_tulip'}")); -+ FLOWER_POT_MAP.put("minecraft:red_flower5", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_orange_tulip'}")); -+ FLOWER_POT_MAP.put("minecraft:red_flower6", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_white_tulip'}")); -+ FLOWER_POT_MAP.put("minecraft:red_flower7", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_pink_tulip'}")); -+ FLOWER_POT_MAP.put("minecraft:red_flower8", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_oxeye_daisy'}")); -+ FLOWER_POT_MAP.put("minecraft:yellow_flower0", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_dandelion'}")); -+ FLOWER_POT_MAP.put("minecraft:sapling0", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_oak_sapling'}")); -+ FLOWER_POT_MAP.put("minecraft:sapling1", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_spruce_sapling'}")); -+ FLOWER_POT_MAP.put("minecraft:sapling2", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_birch_sapling'}")); -+ FLOWER_POT_MAP.put("minecraft:sapling3", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_jungle_sapling'}")); -+ FLOWER_POT_MAP.put("minecraft:sapling4", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_acacia_sapling'}")); -+ FLOWER_POT_MAP.put("minecraft:sapling5", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_dark_oak_sapling'}")); -+ FLOWER_POT_MAP.put("minecraft:red_mushroom0", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_red_mushroom'}")); -+ FLOWER_POT_MAP.put("minecraft:brown_mushroom0", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_brown_mushroom'}")); -+ FLOWER_POT_MAP.put("minecraft:deadbush0", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_dead_bush'}")); -+ FLOWER_POT_MAP.put("minecraft:tallgrass2", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_fern'}")); -+ FLOWER_POT_MAP.put("minecraft:cactus0", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:potted_cactus'}")); // we change default to empty -+ } -+ -+ static final Map> SKULL_MAP = new HashMap<>(); -+ static { -+ mapSkull(SKULL_MAP, 0, "skeleton", "skull"); -+ mapSkull(SKULL_MAP, 1, "wither_skeleton", "skull"); -+ mapSkull(SKULL_MAP, 2, "zombie", "head"); -+ mapSkull(SKULL_MAP, 3, "player", "head"); -+ mapSkull(SKULL_MAP, 4, "creeper", "head"); -+ mapSkull(SKULL_MAP, 5, "dragon", "head"); -+ }; -+ -+ private static void mapSkull(final Map> into, final int oldId, final String newId, final String skullType) { -+ into.put(oldId + "north", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + newId + "_wall_" + skullType + "',Properties:{facing:'north'}}")); -+ into.put(oldId + "east", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + newId + "_wall_" + skullType + "',Properties:{facing:'east'}}")); -+ into.put(oldId + "south", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + newId + "_wall_" + skullType + "',Properties:{facing:'south'}}")); -+ into.put(oldId + "west", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + newId + "_wall_" + skullType + "',Properties:{facing:'west'}}")); -+ -+ for (int rotation = 0; rotation < 16; ++rotation) { -+ into.put(oldId + "" + rotation, -+ HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + newId + "_" + skullType + "',Properties:{rotation:'" + rotation + "'}}")); -+ } -+ } -+ -+ static final Map> DOOR_MAP = new HashMap<>(); -+ static { -+ mapDoor(DOOR_MAP, "oak_door", 1024); -+ mapDoor(DOOR_MAP, "iron_door", 1136); -+ mapDoor(DOOR_MAP, "spruce_door", 3088); -+ mapDoor(DOOR_MAP, "birch_door", 3104); -+ mapDoor(DOOR_MAP, "jungle_door", 3120); -+ mapDoor(DOOR_MAP, "acacia_door", 3136); -+ mapDoor(DOOR_MAP, "dark_oak_door", 3152); -+ }; -+ -+ private static void mapDoor(final Map> into, final String type, final int oldId) { -+ into.put("minecraft:" + type + "eastlowerleftfalsefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'false'}}")); -+ into.put("minecraft:" + type + "eastlowerleftfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'true'}}")); -+ into.put("minecraft:" + type + "eastlowerlefttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'false'}}")); -+ into.put("minecraft:" + type + "eastlowerlefttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'true'}}")); -+ into.put("minecraft:" + type + "eastlowerrightfalsefalse", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId))); -+ into.put("minecraft:" + type + "eastlowerrightfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'true'}}")); -+ into.put("minecraft:" + type + "eastlowerrighttruefalse", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 4))); -+ into.put("minecraft:" + type + "eastlowerrighttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'true'}}")); -+ into.put("minecraft:" + type + "eastupperleftfalsefalse", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 8))); -+ into.put("minecraft:" + type + "eastupperleftfalsetrue", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 10))); -+ into.put("minecraft:" + type + "eastupperlefttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'false'}}")); -+ into.put("minecraft:" + type + "eastupperlefttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'true'}}")); -+ into.put("minecraft:" + type + "eastupperrightfalsefalse", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 9))); -+ into.put("minecraft:" + type + "eastupperrightfalsetrue", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 11))); -+ into.put("minecraft:" + type + "eastupperrighttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'false'}}")); -+ into.put("minecraft:" + type + "eastupperrighttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'true'}}")); -+ into.put("minecraft:" + type + "northlowerleftfalsefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'false'}}")); -+ into.put("minecraft:" + type + "northlowerleftfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'true'}}")); -+ into.put("minecraft:" + type + "northlowerlefttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'false'}}")); -+ into.put("minecraft:" + type + "northlowerlefttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'true'}}")); -+ into.put("minecraft:" + type + "northlowerrightfalsefalse", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 3))); -+ into.put("minecraft:" + type + "northlowerrightfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'true'}}")); -+ into.put("minecraft:" + type + "northlowerrighttruefalse", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 7))); -+ into.put("minecraft:" + type + "northlowerrighttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'true'}}")); -+ into.put("minecraft:" + type + "northupperleftfalsefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'false'}}")); -+ into.put("minecraft:" + type + "northupperleftfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'true'}}")); -+ into.put("minecraft:" + type + "northupperlefttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'false'}}")); -+ into.put("minecraft:" + type + "northupperlefttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'true'}}")); -+ into.put("minecraft:" + type + "northupperrightfalsefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'false'}}")); -+ into.put("minecraft:" + type + "northupperrightfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'true'}}")); -+ into.put("minecraft:" + type + "northupperrighttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'false'}}")); -+ into.put("minecraft:" + type + "northupperrighttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'true'}}")); -+ into.put("minecraft:" + type + "southlowerleftfalsefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'false'}}")); -+ into.put("minecraft:" + type + "southlowerleftfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'true'}}")); -+ into.put("minecraft:" + type + "southlowerlefttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'false'}}")); -+ into.put("minecraft:" + type + "southlowerlefttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'true'}}")); -+ into.put("minecraft:" + type + "southlowerrightfalsefalse", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 1))); -+ into.put("minecraft:" + type + "southlowerrightfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'true'}}")); -+ into.put("minecraft:" + type + "southlowerrighttruefalse", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 5))); -+ into.put("minecraft:" + type + "southlowerrighttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'true'}}")); -+ into.put("minecraft:" + type + "southupperleftfalsefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'false'}}")); -+ into.put("minecraft:" + type + "southupperleftfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'true'}}")); -+ into.put("minecraft:" + type + "southupperlefttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'false'}}")); -+ into.put("minecraft:" + type + "southupperlefttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'true'}}")); -+ into.put("minecraft:" + type + "southupperrightfalsefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'false'}}")); -+ into.put("minecraft:" + type + "southupperrightfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'true'}}")); -+ into.put("minecraft:" + type + "southupperrighttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'false'}}")); -+ into.put("minecraft:" + type + "southupperrighttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'true'}}")); -+ into.put("minecraft:" + type + "westlowerleftfalsefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'false'}}")); -+ into.put("minecraft:" + type + "westlowerleftfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'true'}}")); -+ into.put("minecraft:" + type + "westlowerlefttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'false'}}")); -+ into.put("minecraft:" + type + "westlowerlefttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'true'}}")); -+ into.put("minecraft:" + type + "westlowerrightfalsefalse", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 2))); -+ into.put("minecraft:" + type + "westlowerrightfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'true'}}")); -+ into.put("minecraft:" + type + "westlowerrighttruefalse", Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(oldId + 6))); -+ into.put("minecraft:" + type + "westlowerrighttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'true'}}")); -+ into.put("minecraft:" + type + "westupperleftfalsefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'false'}}")); -+ into.put("minecraft:" + type + "westupperleftfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'true'}}")); -+ into.put("minecraft:" + type + "westupperlefttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'false'}}")); -+ into.put("minecraft:" + type + "westupperlefttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'true'}}")); -+ into.put("minecraft:" + type + "westupperrightfalsefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'false'}}")); -+ into.put("minecraft:" + type + "westupperrightfalsetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'true'}}")); -+ into.put("minecraft:" + type + "westupperrighttruefalse", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'false'}}")); -+ into.put("minecraft:" + type + "westupperrighttruetrue", HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + type + "',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'true'}}")); -+ } -+ -+ static final Map> NOTE_BLOCK_MAP = new HashMap<>(); -+ static { -+ for(int note = 0; note < 26; ++note) { -+ NOTE_BLOCK_MAP.put("true" + note, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:note_block',Properties:{powered:'true',note:'" + note + "'}}")); -+ NOTE_BLOCK_MAP.put("false" + note, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:note_block',Properties:{powered:'false',note:'" + note + "'}}")); -+ } -+ } -+ -+ static final Int2ObjectOpenHashMap DYE_COLOR_MAP = new Int2ObjectOpenHashMap<>(); -+ static { -+ DYE_COLOR_MAP.put(0, "white"); -+ DYE_COLOR_MAP.put(1, "orange"); -+ DYE_COLOR_MAP.put(2, "magenta"); -+ DYE_COLOR_MAP.put(3, "light_blue"); -+ DYE_COLOR_MAP.put(4, "yellow"); -+ DYE_COLOR_MAP.put(5, "lime"); -+ DYE_COLOR_MAP.put(6, "pink"); -+ DYE_COLOR_MAP.put(7, "gray"); -+ DYE_COLOR_MAP.put(8, "light_gray"); -+ DYE_COLOR_MAP.put(9, "cyan"); -+ DYE_COLOR_MAP.put(10, "purple"); -+ DYE_COLOR_MAP.put(11, "blue"); -+ DYE_COLOR_MAP.put(12, "brown"); -+ DYE_COLOR_MAP.put(13, "green"); -+ DYE_COLOR_MAP.put(14, "red"); -+ DYE_COLOR_MAP.put(15, "black"); -+ } -+ -+ static final Map> BED_BLOCK_MAP = new HashMap<>(); -+ -+ static { -+ for (final Int2ObjectMap.Entry entry : DYE_COLOR_MAP.int2ObjectEntrySet()) { -+ if (!Objects.equals(entry.getValue(), "red")) { -+ addBeds(BED_BLOCK_MAP, entry.getIntKey(), entry.getValue()); -+ } -+ } -+ } -+ -+ private static void addBeds(final Map> into, final int colourId, final String colourName) { -+ into.put("southfalsefoot" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'south',occupied:'false',part:'foot'}}")); -+ into.put("westfalsefoot" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'west',occupied:'false',part:'foot'}}")); -+ into.put("northfalsefoot" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'north',occupied:'false',part:'foot'}}")); -+ into.put("eastfalsefoot" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'east',occupied:'false',part:'foot'}}")); -+ into.put("southfalsehead" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'south',occupied:'false',part:'head'}}")); -+ into.put("westfalsehead" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'west',occupied:'false',part:'head'}}")); -+ into.put("northfalsehead" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'north',occupied:'false',part:'head'}}")); -+ into.put("eastfalsehead" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'east',occupied:'false',part:'head'}}")); -+ into.put("southtruehead" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'south',occupied:'true',part:'head'}}")); -+ into.put("westtruehead" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'west',occupied:'true',part:'head'}}")); -+ into.put("northtruehead" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'north',occupied:'true',part:'head'}}")); -+ into.put("easttruehead" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_bed',Properties:{facing:'east',occupied:'true',part:'head'}}")); -+ } -+ -+ static final Map> BANNER_BLOCK_MAP = new HashMap<>(); -+ -+ static { -+ for (final Int2ObjectMap.Entry entry : DYE_COLOR_MAP.int2ObjectEntrySet()) { -+ if (!Objects.equals(entry.getValue(), "white")) { -+ addBanners(BANNER_BLOCK_MAP, 15 - entry.getIntKey(), entry.getValue()); -+ } -+ } -+ } -+ -+ private static void addBanners(final Map> into, final int colourId, final String colourName) { -+ for(int rotation = 0; rotation < 16; ++rotation) { -+ into.put("" + rotation + "_" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_banner',Properties:{rotation:'" + rotation + "'}}")); -+ } -+ -+ into.put("north_" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_wall_banner',Properties:{facing:'north'}}")); -+ into.put("south_" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_wall_banner',Properties:{facing:'south'}}")); -+ into.put("west_" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_wall_banner',Properties:{facing:'west'}}")); -+ into.put("east_" + colourId, HelperBlockFlatteningV1450.parseTag("{Name:'minecraft:" + colourName + "_wall_banner',Properties:{facing:'east'}}")); -+ } -+ -+ static final MapType AIR = Objects.requireNonNull(HelperBlockFlatteningV1450.getNBTForId(0)); -+ -+ public ConverterFlattenChunk() { -+ super(MCVersions.V17W47A, 1); -+ } -+ -+ static String getName(final MapType blockState) { -+ return blockState.getString("Name"); -+ } -+ -+ static String getProperty(final MapType blockState, final String propertyName) { -+ final MapType properties = blockState.getMap("Properties"); -+ if (properties == null) { -+ return ""; -+ } -+ -+ return properties.getString(propertyName, ""); -+ } -+ -+ static int getSideMask(final boolean noLeft, final boolean noRight, final boolean noBack, final boolean noForward) { -+ if (noBack) { -+ if (noRight) { -+ return 2; -+ } else if (noLeft) { -+ return 128; -+ } else { -+ return 1; -+ } -+ } else if (noForward) { -+ if (noLeft) { -+ return 32; -+ } else if (noRight) { -+ return 8; -+ } else { -+ return 16; -+ } -+ } else if (noRight) { -+ return 4; -+ } else if (noLeft) { -+ return 64; -+ } else { -+ return 0; -+ } -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType level = data.getMap("Level"); -+ if (level == null) { -+ return null; -+ } -+ -+ if (!level.hasKey("Sections", ObjectType.LIST)) { -+ return null; -+ } -+ -+ data.setMap("Level", new UpgradeChunk(level).writeBackToLevel()); -+ -+ return null; -+ } -+ -+ static enum Direction { -+ DOWN(AxisDirection.NEGATIVE, Axis.Y), -+ UP(AxisDirection.POSITIVE, Axis.Y), -+ NORTH(AxisDirection.NEGATIVE, Axis.Z), -+ SOUTH(AxisDirection.POSITIVE, Axis.Z), -+ WEST(AxisDirection.NEGATIVE, Axis.X), -+ EAST(AxisDirection.POSITIVE, Axis.X); -+ -+ private final Axis axis; -+ private final AxisDirection axisDirection; -+ -+ private Direction(final AxisDirection axisDirection, final Axis axis) { -+ this.axis = axis; -+ this.axisDirection = axisDirection; -+ } -+ -+ public AxisDirection getAxisDirection() { -+ return this.axisDirection; -+ } -+ -+ public Axis getAxis() { -+ return this.axis; -+ } -+ -+ public static enum AxisDirection { -+ POSITIVE(1), -+ NEGATIVE(-1); -+ -+ private final int step; -+ -+ private AxisDirection(final int step) { -+ this.step = step; -+ } -+ -+ public int getStep() { -+ return this.step; -+ } -+ } -+ -+ public static enum Axis { -+ X, Y, Z; -+ } -+ } -+ -+ static class DataLayer { -+ private final byte[] data; -+ -+ public DataLayer() { -+ this.data = new byte[2048]; -+ } -+ -+ public DataLayer(final byte[] data) { -+ this.data = data; -+ if (data.length != 2048) { -+ throw new IllegalArgumentException("ChunkNibbleArrays should be 2048 bytes not: " + data.length); -+ } -+ } -+ -+ public static DataLayer getOrNull(final byte[] data) { -+ return data == null ? null : new DataLayer(data); -+ } -+ -+ public static DataLayer getOrCreate(final byte[] data) { -+ return data == null ? new DataLayer() : new DataLayer(data); -+ } -+ -+ public int get(final int index) { -+ final byte value = this.data[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); -+ } -+ -+ public int get(final int x, final int y, final int z) { -+ final int index = y << 8 | z << 4 | x; -+ final byte value = this.data[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); -+ } -+ } -+ -+ static final class UpgradeChunk { -+ int sides; -+ -+ final Section[] sections = new Section[16]; -+ final MapType level; -+ final int blockX; -+ final int blockZ; -+ final Int2ObjectLinkedOpenHashMap> tileEntities = new Int2ObjectLinkedOpenHashMap<>(16); -+ -+ public UpgradeChunk(final MapType level) { -+ this.level = level; -+ this.blockX = level.getInt("xPos") << 4; -+ this.blockZ = level.getInt("zPos") << 4; -+ -+ final ListType tileEntities = level.getList("TileEntities", ObjectType.MAP); -+ if (tileEntities != null) { -+ for (int i = 0, len = tileEntities.size(); i < len; ++i) { -+ final MapType tileEntity = tileEntities.getMap(i); -+ -+ final int x = (tileEntity.getInt("x") - this.blockX) & 15; -+ final int y = tileEntity.getInt("y"); -+ final int z = (tileEntity.getInt("z") - this.blockZ) & 15; -+ final int index = (y << 8) | (z << 4) | x; -+ if (this.tileEntities.put(index, tileEntity) != null) { -+ LOGGER.warn("In chunk: {}x{} found a duplicate block entity at position (ConverterFlattenChunk): [{}, {}, {}]", this.blockX, this.blockZ, x, y, z); -+ } -+ } -+ } -+ -+ final boolean convertedFromAlphaFormat = level.getBoolean("convertedFromAlphaFormat"); -+ final ListType sections = level.getList("Sections", ObjectType.MAP); -+ if (sections != null) { -+ for (int i = 0, len = sections.size(); i < len; ++i) { -+ final MapType sectionData = sections.getMap(i); -+ final Section section = new Section(sectionData); -+ -+ if (section.y < 0 || section.y > 15) { -+ LOGGER.warn("In chunk: {}x{} found an invalid chunk section y (ConverterFlattenChunk): {}", this.blockX, this.blockZ, section.y); -+ continue; -+ } -+ -+ if (this.sections[section.y] != null) { -+ LOGGER.warn("In chunk: {}x{} found a duplicate chunk section (ConverterFlattenChunk): {}", this.blockX, this.blockZ, section.y); -+ } -+ -+ this.sides = section.upgrade(this.sides); -+ this.sections[section.y] = section; -+ } -+ } -+ -+ for (final Section section : this.sections) { -+ if (section == null) { -+ continue; -+ } -+ -+ final int yIndex = section.y << (8 + 4); -+ -+ for (final Iterator> iterator = section.toFix.int2ObjectEntrySet().fastIterator(); iterator.hasNext();) { -+ final Int2ObjectMap.Entry fixEntry = iterator.next(); -+ final IntIterator positionIterator = fixEntry.getValue().iterator(); -+ switch (fixEntry.getIntKey()) { -+ case 2: { // grass block -+ while (positionIterator.hasNext()) { -+ final int position = positionIterator.nextInt() | yIndex; -+ final MapType blockState = this.getBlock(position); -+ if (!"minecraft:grass_block".equals(getName(blockState))) { -+ continue; -+ } -+ -+ final String blockAbove = getName(getBlock(relative(position, Direction.UP))); -+ if ("minecraft:snow".equals(blockAbove) || "minecraft:snow_layer".equals(blockAbove)) { -+ this.setBlock(position, SNOWY_GRASS); -+ } -+ } -+ break; -+ } -+ case 3: { // dirt -+ while (positionIterator.hasNext()) { -+ final int position = positionIterator.nextInt() | yIndex; -+ final MapType blockState = this.getBlock(position); -+ if (!"minecraft:podzol".equals(getName(blockState))) { -+ continue; -+ } -+ -+ final String blockAbove = getName(getBlock(relative(position, Direction.UP))); -+ if ("minecraft:snow".equals(blockAbove) || "minecraft:snow_layer".equals(blockAbove)) { -+ this.setBlock(position, SNOWY_PODZOL); -+ } -+ } -+ break; -+ } -+ case 25: { // note block -+ while (positionIterator.hasNext()) { -+ final int position = positionIterator.nextInt() | yIndex; -+ final MapType tile = this.removeBlockEntity(position); -+ if (tile != null) { -+ final String state = Boolean.toString(tile.getBoolean("powered")) + (byte) Math.min(Math.max(tile.getInt("note"), 0), 24); -+ this.setBlock(position, NOTE_BLOCK_MAP.getOrDefault(state, NOTE_BLOCK_MAP.get("false0"))); -+ } -+ } -+ break; -+ } -+ case 26: { // bed -+ while (positionIterator.hasNext()) { -+ final int position = positionIterator.nextInt() | yIndex; -+ final MapType tile = this.getBlockEntity(position); -+ -+ if (tile == null) { -+ continue; -+ } -+ -+ final MapType blockState = this.getBlock(position); -+ -+ final int colour = tile.getInt("color"); -+ if (colour != 14 && colour >= 0 && colour < 16) { -+ final String state = getProperty(blockState, "facing") + getProperty(blockState, "occupied") + getProperty(blockState, "part") + colour; -+ -+ final MapType update = BED_BLOCK_MAP.get(state); -+ if (update != null) { -+ this.setBlock(position, update); -+ } -+ } -+ } -+ break; -+ } -+ case 64: // oak door -+ case 71: // iron door -+ case 193: // spruce door -+ case 194: // birch door -+ case 195: // jungle door -+ case 196: // acacia door -+ case 197: { // dark oak door -+ // aka the door updater -+ while (positionIterator.hasNext()) { -+ final int position = positionIterator.nextInt() | yIndex; -+ final MapType blockState = this.getBlock(position); -+ if (!getName(blockState).endsWith("_door")) { -+ continue; -+ } -+ -+ if (!"lower".equals(getProperty(blockState, "half"))) { -+ continue; -+ } -+ -+ final int positionAbove = relative(position, Direction.UP); -+ final MapType blockStateAbove = this.getBlock(positionAbove); -+ -+ final String name = getName(blockState); -+ if (name.equals(getName(blockStateAbove))) { -+ final String facingBelow = getProperty(blockState, "facing"); -+ final String openBelow = getProperty(blockState, "open"); -+ final String hingeAbove = convertedFromAlphaFormat ? "left" : getProperty(blockStateAbove, "hinge"); -+ final String poweredAbove = convertedFromAlphaFormat ? "false" : getProperty(blockStateAbove, "powered"); -+ -+ this.setBlock(position, DOOR_MAP.get(name + facingBelow + "lower" + hingeAbove + openBelow + poweredAbove)); -+ this.setBlock(positionAbove, DOOR_MAP.get(name + facingBelow + "upper" + hingeAbove + openBelow + poweredAbove)); -+ } -+ } -+ break; -+ } -+ case 86: { // pumpkin -+ while (positionIterator.hasNext()) { -+ final int position = positionIterator.nextInt() | yIndex; -+ final MapType blockState = this.getBlock(position); -+ -+ // I guess this is some terrible hack to convert carved pumpkins from world gen into -+ // regular pumpkins? -+ -+ if ("minecraft:carved_pumpkin".equals(getName(blockState))) { -+ final String downName = getName(this.getBlock(relative(position, Direction.DOWN))); -+ if ("minecraft:grass_block".equals(downName) || "minecraft:dirt".equals(downName)) { -+ this.setBlock(position, PUMPKIN); -+ } -+ } -+ } -+ break; -+ } -+ case 110: { // mycelium -+ while (positionIterator.hasNext()) { -+ final int position = positionIterator.nextInt() | yIndex; -+ final MapType blockState = this.getBlock(position); -+ if ("minecraft:mycelium".equals(getName(blockState))) { -+ final String nameAbove = getName(this.getBlock(relative(position, Direction.UP))); -+ if ("minecraft:snow".equals(nameAbove) || "minecraft:snow_layer".equals(nameAbove)) { -+ this.setBlock(position, SNOWY_MYCELIUM); -+ } -+ } -+ } -+ break; -+ } -+ case 140: { // flower pot -+ while (positionIterator.hasNext()) { -+ final int position = positionIterator.nextInt() | yIndex; -+ final MapType tile = this.removeBlockEntity(position); -+ if (tile == null) { -+ continue; -+ } -+ -+ final String item; -+ if (tile.hasKey("Item", ObjectType.NUMBER)) { -+ // the item name converter should have migrated to number, however no legacy converter -+ // ever did this. so we can get data with versions above v102 (old worlds, converted prior to DFU) -+ // that didn't convert. so just do it here. -+ item = HelperItemNameV102.getNameFromId(tile.getInt("Item")); -+ } else { -+ item = tile.getString("Item", ""); -+ } -+ -+ final String state = item + tile.getInt("Data"); -+ this.setBlock(position, FLOWER_POT_MAP.getOrDefault(state, FLOWER_POT_MAP.get("minecraft:air0"))); -+ } -+ break; -+ } -+ case 144: { // mob head -+ while (positionIterator.hasNext()) { -+ final int position = positionIterator.nextInt() | yIndex; -+ final MapType tile = this.getBlockEntity(position); -+ if (tile == null) { -+ continue; -+ } -+ -+ final String typeString = Integer.toString(tile.getInt("SkullType")); -+ final String facing = getProperty(this.getBlock(position), "facing"); -+ final String state; -+ if (!"up".equals(facing) && !"down".equals(facing)) { -+ state = typeString + facing; -+ } else { -+ state = typeString + tile.getInt("Rot"); -+ } -+ -+ tile.remove("SkullType"); -+ tile.remove("facing"); -+ tile.remove("Rot"); -+ -+ this.setBlock(position, SKULL_MAP.getOrDefault(state, SKULL_MAP.get("0north"))); -+ } -+ break; -+ } -+ case 175: { // sunflower -+ while (positionIterator.hasNext()) { -+ final int position = positionIterator.nextInt() | yIndex; -+ final MapType blockState = this.getBlock(position); -+ if (!"upper".equals(getProperty(blockState, "half"))) { -+ continue; -+ } -+ -+ final MapType blockStateBelow = this.getBlock(relative(position, Direction.DOWN)); -+ final String nameBelow = getName(blockStateBelow); -+ switch (nameBelow) { -+ case "minecraft:sunflower": -+ this.setBlock(position, UPPER_SUNFLOWER); -+ break; -+ case "minecraft:lilac": -+ this.setBlock(position, UPPER_LILAC); -+ break; -+ case "minecraft:tall_grass": -+ this.setBlock(position, UPPER_TALL_GRASS); -+ break; -+ case "minecraft:large_fern": -+ this.setBlock(position, UPPER_LARGE_FERN); -+ break; -+ case "minecraft:rose_bush": -+ this.setBlock(position, UPPER_ROSE_BUSH); -+ break; -+ case "minecraft:peony": -+ this.setBlock(position, UPPER_PEONY); -+ break; -+ } -+ } -+ break; -+ } -+ case 176: // free standing banner -+ case 177: { // wall mounted banner -+ while (positionIterator.hasNext()) { -+ final int position = positionIterator.nextInt() | yIndex; -+ final MapType tile = this.getBlockEntity(position); -+ -+ if (tile == null) { -+ continue; -+ } -+ -+ final MapType blockState = this.getBlock(position); -+ -+ final int base = tile.getInt("Base"); -+ if (base != 15 && base >= 0 && base < 16) { -+ final String state = getProperty(blockState, fixEntry.getIntKey() == 176 ? "rotation" : "facing") + "_" + base; -+ final MapType update = BANNER_BLOCK_MAP.get(state); -+ if (update != null) { -+ this.setBlock(position, update); -+ } -+ } -+ } -+ break; -+ } -+ } -+ } -+ } -+ } -+ -+ private MapType getBlockEntity(final int index) { -+ return this.tileEntities.get(index); -+ } -+ -+ private MapType removeBlockEntity(final int index) { -+ return this.tileEntities.remove(index); -+ } -+ -+ public static int relative(final int index, final Direction direction) { -+ switch (direction.getAxis()) { -+ case X: -+ int j = (index & 15) + direction.getAxisDirection().getStep(); -+ return j >= 0 && j <= 15 ? index & -16 | j : -1; -+ case Y: -+ int k = (index >> 8) + direction.getAxisDirection().getStep(); -+ return k >= 0 && k <= 255 ? index & 255 | k << 8 : -1; -+ case Z: -+ int l = (index >> 4 & 15) + direction.getAxisDirection().getStep(); -+ return l >= 0 && l <= 15 ? index & -241 | l << 4 : -1; -+ default: -+ return -1; -+ } -+ } -+ -+ private void setBlock(final int index, final MapType blockState) { -+ if (index >= 0 && index <= 65535) { -+ final Section section = this.getSection(index); -+ if (section != null) { -+ section.setBlock(index & 4095, blockState); -+ } -+ } -+ } -+ -+ private Section getSection(final int index) { -+ final int y = index >> 12; -+ return y < this.sections.length ? this.sections[y] : null; -+ } -+ -+ public MapType getBlock(int i) { -+ if (i >= 0 && i <= 65535) { -+ final Section section = this.getSection(i); -+ return section == null ? AIR : section.getBlock(i & 4095); -+ } else { -+ return AIR; -+ } -+ } -+ -+ public MapType writeBackToLevel() { -+ if (this.tileEntities.isEmpty()) { -+ this.level.remove("TileEntities"); -+ } else { -+ final ListType tileEntities = Types.NBT.createEmptyList(); -+ this.tileEntities.values().forEach(tileEntities::addMap); -+ this.level.setList("TileEntities", tileEntities); -+ } -+ -+ final MapType indices = Types.NBT.createEmptyMap(); -+ final ListType sections = Types.NBT.createEmptyList(); -+ for (final Section section : this.sections) { -+ if (section == null) { -+ continue; -+ } -+ -+ sections.addMap(section.writeBackToSection()); -+ indices.setInts(Integer.toString(section.y), Arrays.copyOf(section.update.elements(), section.update.size())); -+ } -+ -+ this.level.setList("Sections", sections); -+ -+ final MapType upgradeData = Types.NBT.createEmptyMap(); -+ upgradeData.setByte("Sides", (byte)this.sides); -+ upgradeData.setMap("Indices", indices); -+ -+ this.level.setMap("UpgradeData", upgradeData); -+ -+ return this.level; -+ } -+ } -+ -+ static class Section { -+ final Palette palette = new Palette(); -+ -+ static final class Palette extends Reference2IntOpenHashMap> { -+ -+ final ListType paletteStates = Types.NBT.createEmptyList(); -+ -+ private int find(final MapType k) { -+ if (((k) == (null))) -+ return containsNullKey ? n : -(n + 1); -+ MapType curr; -+ final Object[] key = this.key; -+ int pos; -+ // The starting point. -+ if (((curr = (MapType)key[pos = (it.unimi.dsi.fastutil.HashCommon.mix(System.identityHashCode(k))) & mask]) == (null))) -+ return -(pos + 1); -+ if (((k) == (curr))) -+ return pos; -+ // There's always an unused entry. -+ while (true) { -+ if (((curr = (MapType)key[pos = (pos + 1) & mask]) == (null))) -+ return -(pos + 1); -+ if (((k) == (curr))) -+ return pos; -+ } -+ } -+ -+ private void insert(final int pos, final MapType k, final int v) { -+ if (pos == n) -+ containsNullKey = true; -+ ((Object[])key)[pos] = k; -+ value[pos] = v; -+ if (size++ >= maxFill) -+ rehash(arraySize(size + 1, f)); -+ } -+ -+ private MapType[] byId = new MapType[4]; -+ private MapType last = null; -+ -+ public int getOrCreateId(final MapType k) { -+ if (k == this.last) { -+ return this.size - 1; -+ } -+ final int pos = find(k); -+ if (pos >= 0) { -+ return this.value[pos]; -+ } -+ -+ final int insert = this.size; -+ MapType inPalette = k; -+ -+ if ("%%FILTER_ME%%".equals(getName(k))) { -+ inPalette = AIR; -+ } -+ -+ if (insert >= this.byId.length) { -+ this.byId = Arrays.copyOf(this.byId, this.byId.length * 2); -+ this.byId[insert] = k; -+ } else { -+ this.byId[insert] = k; -+ } -+ this.paletteStates.addMap(inPalette); -+ -+ this.last = k; -+ -+ this.insert(-pos - 1, k, insert); -+ -+ return insert; -+ } -+ -+ } -+ -+ final MapType section; -+ final boolean hasData; -+ final Int2ObjectLinkedOpenHashMap toFix = new Int2ObjectLinkedOpenHashMap<>(); -+ final IntArrayList update = new IntArrayList(); -+ final int y; -+ final int[] buffer = new int[4096]; -+ -+ public Section(final MapType section) { -+ this.section = section; -+ this.y = section.getInt("Y"); -+ this.hasData = section.hasKey("Blocks", ObjectType.BYTE_ARRAY); -+ } -+ -+ public MapType getBlock(final int index) { -+ if (index >= 0 && index <= 4095) { -+ final MapType state = this.palette.byId[this.buffer[index]]; -+ return state == null ? AIR : state; -+ } else { -+ return AIR; -+ } -+ } -+ -+ public void setBlock(final int index, final MapType blockState) { -+ this.buffer[index] = this.palette.getOrCreateId(blockState); -+ } -+ -+ public int upgrade(int sides) { -+ if (!this.hasData) { -+ return sides; -+ } -+ -+ final byte[] blocks = this.section.getBytes("Blocks"); -+ final DataLayer data = DataLayer.getOrNull(this.section.getBytes("Data")); -+ final DataLayer add = DataLayer.getOrNull(this.section.getBytes("Add")); -+ -+ this.palette.getOrCreateId(AIR); -+ -+ for (int index = 0; index < 4096; ++index) { -+ final int x = index & 15; -+ final int z = index >> 4 & 15; -+ -+ int blockStateId = (blocks[index] & 255) << 4; -+ if (data != null) { -+ blockStateId |= data.get(index); -+ } -+ if (add != null) { -+ blockStateId |= add.get(index) << 12; -+ } -+ if (IDS_NEEDING_FIX[blockStateId >>> 4]) { -+ this.addFix(blockStateId >>> 4, index); -+ } -+ -+ if (VIRTUAL[blockStateId >>> 4]) { -+ final int additionalSides = getSideMask(x == 0, x == 15, z == 0, z == 15); -+ if (additionalSides == 0) { -+ this.update.add(index); -+ } else { -+ sides |= additionalSides; -+ } -+ } -+ -+ this.setBlock(index, HelperBlockFlatteningV1450.getNBTForId(blockStateId)); -+ } -+ -+ return sides; -+ } -+ -+ private void addFix(final int block, final int index) { -+ this.toFix.computeIfAbsent(block, (final int keyInMap) -> { -+ return new IntArrayList(); -+ }).add(index); -+ } -+ -+ // Note: modifies the current section and returns it. -+ public MapType writeBackToSection() { -+ if (!this.hasData) { -+ return this.section; -+ } -+ -+ this.section.setList("Palette", this.palette.paletteStates.copy()); // deep copy to ensure palette compound tags are NOT shared -+ -+ final int bitSize = Math.max(4, DataFixUtils.ceillog2(this.palette.size())); -+ final PackedBitStorage packedIds = new PackedBitStorage(bitSize, 4096); -+ -+ for(int index = 0; index < this.buffer.length; ++index) { -+ packedIds.set(index, this.buffer[index]); -+ } -+ -+ this.section.setLongs("BlockStates", packedIds.getRaw()); -+ -+ this.section.remove("Blocks"); -+ this.section.remove("Data"); -+ this.section.remove("Add"); -+ -+ return this.section; -+ } -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterRenameStatus.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterRenameStatus.java -new file mode 100644 -index 0000000000000000000000000000000000000000..084c67a46bc5ec7f5a4bef3216805a87b32c83d0 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/chunk/ConverterRenameStatus.java -@@ -0,0 +1,32 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.chunk; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.util.NamespaceUtil; -+import java.util.function.Function; -+ -+public final class ConverterRenameStatus extends DataConverter, MapType> { -+ -+ private final Function renamer; -+ -+ public ConverterRenameStatus(final int toVersion, final Function renamer) { -+ this(toVersion, 0, renamer); -+ } -+ -+ public ConverterRenameStatus(final int toVersion, final int versionStep, final Function renamer) { -+ super(toVersion, versionStep); -+ this.renamer = renamer; -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ // Note: DFU technically enforces namespace due to how they wrote their converter, so we will do the same. -+ NamespaceUtil.enforceForPath(data, "Status"); -+ RenameHelper.renameString(data, "Status", this.renamer); -+ -+ NamespaceUtil.enforceForPath(data.getMap("below_zero_retrogen"), "target_status"); -+ RenameHelper.renameString(data.getMap("below_zero_retrogen"), "target_status", this.renamer); -+ return null; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/custom/V3818_Commands.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/custom/V3818_Commands.java -new file mode 100644 -index 0000000000000000000000000000000000000000..17e75ef0fe1f7e20d4100b40c445c9fccbf23833 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/custom/V3818_Commands.java -@@ -0,0 +1,288 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.custom; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCDataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.util.CommandArgumentUpgrader; -+import com.google.common.base.Suppliers; -+import com.google.gson.JsonArray; -+import com.google.gson.JsonElement; -+import com.google.gson.JsonObject; -+import com.google.gson.JsonParseException; -+import com.google.gson.JsonParser; -+import com.google.gson.JsonPrimitive; -+import com.mojang.brigadier.exceptions.CommandSyntaxException; -+import com.mojang.serialization.Dynamic; -+import com.mojang.serialization.JsonOps; -+import net.minecraft.SharedConstants; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.nbt.NbtOps; -+import net.minecraft.nbt.Tag; -+import net.minecraft.nbt.TagParser; -+import net.minecraft.util.GsonHelper; -+import java.util.Iterator; -+import java.util.function.Supplier; -+ -+public final class V3818_Commands { -+ -+ private static final int VERSION = MCVersions.V24W07A + 1; -+ -+ private static final boolean DISABLE_COMMAND_CONVERTER = Boolean.getBoolean("Paper.DisableCommandConverter"); -+ -+ public static String toCommandFormat(final CompoundTag components) { -+ final StringBuilder ret = new StringBuilder(); -+ ret.append('['); -+ for (final Iterator iterator = components.getAllKeys().iterator(); iterator.hasNext();) { -+ final String key = iterator.next(); -+ ret.append(key); -+ ret.append('='); -+ ret.append(components.get(key).toString()); -+ if (iterator.hasNext()) { -+ ret.append(','); -+ } -+ } -+ ret.append(']'); -+ -+ return ret.toString(); -+ } -+ -+ public static JsonElement convertToJson(final Tag tag) { -+ // We don't have conversion utilities, but DFU does... -+ -+ return new Dynamic<>(NbtOps.INSTANCE, tag).convert(JsonOps.INSTANCE).getValue(); -+ } -+ -+ public static void walkComponent(final JsonElement primitive) { -+ if (!(primitive instanceof JsonObject root)) { -+ if (primitive instanceof JsonArray array) { -+ for (final JsonElement component : array) { -+ walkComponent(component); -+ } -+ } -+ return; -+ } -+ -+ final JsonElement clickEventElement = root.get("clickEvent"); -+ if (clickEventElement instanceof JsonObject clickEvent) { -+ final JsonElement actionElement = clickEvent.get("action"); -+ final JsonElement cmdElement = clickEvent.get("value"); -+ if (actionElement instanceof JsonPrimitive action && cmdElement instanceof JsonPrimitive cmd) { -+ final String actionString = action.getAsString(); -+ final String cmdString = cmd.getAsString(); -+ -+ if ((actionString.equals("suggest_command") && cmdString.startsWith("/")) || actionString.equals("run_command")) { -+ final Object res = MCTypeRegistry.DATACONVERTER_CUSTOM_TYPE_COMMAND.convert( -+ cmdString, MCVersions.V1_20_4, SharedConstants.getCurrentVersion().getDataVersion().getVersion() -+ ); -+ if (res instanceof String newCmd) { -+ clickEvent.addProperty("value", newCmd); -+ } -+ } -+ } -+ } -+ -+ final JsonElement hoverEventElement = root.get("hoverEvent"); -+ if (hoverEventElement instanceof JsonObject hoverEvent) { -+ final JsonElement showText = hoverEvent.get("action"); -+ if (showText instanceof JsonPrimitive showTextPrimitive && showTextPrimitive.getAsString().equals("show_item")) { -+ final JsonElement contentsElement = hoverEvent.get("contents"); -+ if (contentsElement instanceof JsonObject contents) { -+ final JsonElement idElement = contents.get("id"); -+ final JsonElement tagElement = contents.get("tag"); -+ -+ if (idElement instanceof JsonPrimitive idPrimitive) { -+ final CompoundTag itemNBT = new CompoundTag(); -+ itemNBT.putString("id", idPrimitive.getAsString()); -+ itemNBT.putInt("Count", 1); -+ -+ if (tagElement instanceof JsonPrimitive tagPrimitive) { -+ try { -+ final CompoundTag tag = TagParser.parseTag(tagPrimitive.getAsString()); -+ itemNBT.put("tag", tag); -+ } catch (final CommandSyntaxException ignore) {} -+ } -+ -+ final CompoundTag converted = MCDataConverter.convertTag( -+ MCTypeRegistry.ITEM_STACK, itemNBT, MCVersions.V1_20_4, -+ SharedConstants.getCurrentVersion().getDataVersion().getVersion() -+ ); -+ -+ contents.remove("tag"); -+ -+ contents.addProperty("id", converted.getString("id")); -+ -+ if (converted.contains("components", Tag.TAG_COMPOUND)) { -+ contents.add("components", convertToJson(converted.getCompound("components"))); -+ } -+ } -+ } -+ final JsonElement valueElement = hoverEvent.get("value"); -+ if (valueElement instanceof JsonPrimitive valuePrimitive) { -+ try { -+ final CompoundTag itemNBT = TagParser.parseTag(valuePrimitive.getAsString()); -+ if (itemNBT.contains("id", Tag.TAG_STRING)) { -+ final boolean explicitCount = itemNBT.contains("Count", Tag.TAG_ANY_NUMERIC); -+ if (!explicitCount) { -+ itemNBT.putInt("Count", 1); -+ } -+ final CompoundTag converted = MCDataConverter.convertTag( -+ MCTypeRegistry.ITEM_STACK, itemNBT, MCVersions.V1_20_4, -+ SharedConstants.getCurrentVersion().getDataVersion().getVersion() -+ ); -+ -+ hoverEvent.remove("value"); -+ -+ final JsonObject contents = new JsonObject(); -+ hoverEvent.add("contents", contents); -+ -+ contents.addProperty("id", converted.getString("id")); -+ if (explicitCount) { -+ contents.addProperty("count", converted.getInt("count")); -+ } -+ -+ if (converted.contains("components", Tag.TAG_COMPOUND)) { -+ contents.add("components", convertToJson(converted.getCompound("components"))); -+ } -+ } -+ } catch (final CommandSyntaxException ignore) {} -+ } -+ } -+ } -+ -+ final JsonElement extra = root.get("extra"); -+ if (extra instanceof JsonArray array) { -+ for (final JsonElement component : array) { -+ walkComponent(component); -+ } -+ } -+ } -+ -+ private static String walkComponent(final String json) { -+ if (json == null || json.isEmpty()) { -+ return json; -+ } -+ -+ try { -+ final JsonElement element = JsonParser.parseString(json); -+ walkComponent(element); -+ return GsonHelper.toStableString(element); -+ } catch (final JsonParseException ex) { -+ return json; -+ } -+ } -+ -+ // this is AFTER all the converters for subversion 5, so these run AFTER them -+ public static void register_5() { -+ if (DISABLE_COMMAND_CONVERTER) { -+ return; -+ } -+ // Command is already registered in walker for command blocks -+ MCTypeRegistry.DATACONVERTER_CUSTOM_TYPE_COMMAND.addConverter(new DataConverter<>(VERSION, 5) { -+ private static final Supplier COMMAND_UPGRADER = Suppliers.memoize(() -> -+ CommandArgumentUpgrader.upgrader_1_20_4_to_1_20_5(999)); -+ -+ @Override -+ public Object convert(final Object data, final long sourceVersion, final long toVersion) { -+ if (!(data instanceof String cmd)) { -+ return null; -+ } -+ // We use startsWith("/") because we aren't supporting WorldEdit style commands, -+ // and passing the context of whether the use supports leading slash would be high effort low return -+ return COMMAND_UPGRADER.get().upgradeCommandArguments(cmd, cmd.startsWith("/")); -+ } -+ }); -+ -+ // command is not registered in any walkers for books/signs, and we don't want to do that as we would parse -+ // the json every walk. instead, we create a one time converter to avoid the additional cost of parsing the json -+ // for future updates -+ -+ // books -+ // note: at this stage, item is converted to components, so we can use the data components type -+ MCTypeRegistry.DATA_COMPONENTS.addStructureConverter(new DataConverter<>(VERSION, 5) { -+ private static void walkPath(final MapType data, final String path) { -+ final String str = data.getString(path); -+ if (str == null) { -+ return; -+ } -+ -+ final String newStr = walkComponent(str); -+ if (newStr != null) { -+ data.setString(path, newStr); -+ } -+ } -+ -+ private static void walkBookContent(final MapType data, final String path) { -+ if (data == null) { -+ return; -+ } -+ -+ final MapType content = data.getMap(path); -+ if (content == null) { -+ return; -+ } -+ -+ final ListType pages = content.getList("pages", ObjectType.MAP); -+ if (pages == null) { -+ return; -+ } -+ -+ for (int i = 0, len = pages.size(); i < len; ++i) { -+ final MapType text = pages.getMap(i); -+ -+ walkPath(text, "raw"); -+ walkPath(text, "filtered"); -+ } -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ walkBookContent(data, "minecraft:written_book_content"); -+ return null; -+ } -+ }); -+ -+ // signs -+ -+ final DataConverter, MapType> signTileConverter = new DataConverter<>(VERSION, 5) { -+ private static void walkText(final MapType data, final String path) { -+ if (data == null) { -+ return; -+ } -+ -+ final MapType text = data.getMap(path); -+ if (text == null) { -+ return; -+ } -+ -+ final ListType messages = text.getList("messages", ObjectType.STRING); -+ if (messages != null) { -+ for (int i = 0, len = Math.min(4, messages.size()); i < len; ++i) { -+ messages.setString(i, walkComponent(messages.getString(i))); -+ } -+ } -+ -+ final ListType filteredMessages = text.getList("filtered_messages", ObjectType.STRING); -+ -+ if (filteredMessages != null) { -+ for (int i = 0, len = Math.min(4, filteredMessages.size()); i < len; ++i) { -+ filteredMessages.setString(i, walkComponent(filteredMessages.getString(i))); -+ } -+ } -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ walkText(data, "front_text"); -+ walkText(data, "back_text"); -+ return null; -+ } -+ }; -+ -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:sign", signTileConverter); -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:hanging_sign", signTileConverter); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterAbstractEntityRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterAbstractEntityRename.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6684915d6c0c44328a9296dc3ceb530e69482083 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterAbstractEntityRename.java -@@ -0,0 +1,38 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.entity; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import java.util.function.Function; -+ -+public final class ConverterAbstractEntityRename { -+ -+ private ConverterAbstractEntityRename() {} -+ -+ public static void register(final int version, final Function renamer) { -+ register(version, 0, renamer); -+ } -+ -+ public static void register(final int version, final int subVersion, final Function renamer) { -+ MCTypeRegistry.ENTITY.addStructureConverter(new DataConverter<>(version, subVersion) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final String id = data.getString("id"); -+ if (id == null) { -+ return null; -+ } -+ -+ final String converted = renamer.apply(id); -+ -+ if (converted != null) { -+ data.setString("id", converted); -+ } -+ -+ return null; -+ } -+ }); -+ ConverterAbstractStringValueTypeRename.register(version, subVersion, MCTypeRegistry.ENTITY_NAME, renamer); -+ } -+ -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityToVariant.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityToVariant.java -new file mode 100644 -index 0000000000000000000000000000000000000000..985af815e3c23ad7c8b774eac46a7202d3020234 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityToVariant.java -@@ -0,0 +1,44 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.entity; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.types.MapType; -+import java.util.function.IntFunction; -+ -+public final class ConverterEntityToVariant extends DataConverter, MapType> { -+ -+ public final String path; -+ public final IntFunction conversion; -+ -+ public ConverterEntityToVariant(final int toVersion, final String path, final IntFunction conversion) { -+ super(toVersion); -+ this.path = path; -+ this.conversion = conversion; -+ } -+ -+ public ConverterEntityToVariant(final int toVersion, final int versionStep, final String path, final IntFunction conversion) { -+ super(toVersion, versionStep); -+ this.path = path; -+ this.conversion = conversion; -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final Number value = data.getNumber(this.path); -+ if (value == null) { -+ // nothing to do, DFU does the same -+ return null; -+ } -+ -+ final String converted = this.conversion.apply(value.intValue()); -+ -+ if (converted == null) { -+ throw new NullPointerException("Conversion " + this.conversion + " cannot return null value!"); -+ } -+ -+ // DFU doesn't appear to remove the old field, so why should we? -+ -+ data.setString("variant", converted); -+ -+ return null; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityVariantRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityVariantRename.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ed5dcf6f8160742c07e23e98c85409209350a7d4 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterEntityVariantRename.java -@@ -0,0 +1,37 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.entity; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.types.MapType; -+import java.util.function.Function; -+ -+public final class ConverterEntityVariantRename extends DataConverter, MapType> { -+ -+ private final Function renamer; -+ -+ public ConverterEntityVariantRename(final int toVersion, final Function renamer) { -+ super(toVersion); -+ this.renamer = renamer; -+ } -+ -+ public ConverterEntityVariantRename(final int toVersion, final int versionStep, final Function renamer) { -+ super(toVersion, versionStep); -+ this.renamer = renamer; -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final String variant = data.getString("variant"); -+ -+ if (variant == null) { -+ return null; -+ } -+ -+ final String rename = this.renamer.apply(variant); -+ -+ if (rename != null) { -+ data.setString("variant", rename); -+ } -+ -+ return null; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterFlattenEntity.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterFlattenEntity.java -new file mode 100644 -index 0000000000000000000000000000000000000000..afad2d92f78d4727ff4440ad2778f018d5a2a609 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/entity/ConverterFlattenEntity.java -@@ -0,0 +1,371 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.entity; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.HelperBlockFlatteningV1450; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+ -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class ConverterFlattenEntity extends DataConverter, MapType> { -+ -+ private static final Map BLOCK_NAME_TO_ID = new HashMap<>(); -+ static { -+ BLOCK_NAME_TO_ID.put("minecraft:air", 0); -+ BLOCK_NAME_TO_ID.put("minecraft:stone", 1); -+ BLOCK_NAME_TO_ID.put("minecraft:grass", 2); -+ BLOCK_NAME_TO_ID.put("minecraft:dirt", 3); -+ BLOCK_NAME_TO_ID.put("minecraft:cobblestone", 4); -+ BLOCK_NAME_TO_ID.put("minecraft:planks", 5); -+ BLOCK_NAME_TO_ID.put("minecraft:sapling", 6); -+ BLOCK_NAME_TO_ID.put("minecraft:bedrock", 7); -+ BLOCK_NAME_TO_ID.put("minecraft:flowing_water", 8); -+ BLOCK_NAME_TO_ID.put("minecraft:water", 9); -+ BLOCK_NAME_TO_ID.put("minecraft:flowing_lava", 10); -+ BLOCK_NAME_TO_ID.put("minecraft:lava", 11); -+ BLOCK_NAME_TO_ID.put("minecraft:sand", 12); -+ BLOCK_NAME_TO_ID.put("minecraft:gravel", 13); -+ BLOCK_NAME_TO_ID.put("minecraft:gold_ore", 14); -+ BLOCK_NAME_TO_ID.put("minecraft:iron_ore", 15); -+ BLOCK_NAME_TO_ID.put("minecraft:coal_ore", 16); -+ BLOCK_NAME_TO_ID.put("minecraft:log", 17); -+ BLOCK_NAME_TO_ID.put("minecraft:leaves", 18); -+ BLOCK_NAME_TO_ID.put("minecraft:sponge", 19); -+ BLOCK_NAME_TO_ID.put("minecraft:glass", 20); -+ BLOCK_NAME_TO_ID.put("minecraft:lapis_ore", 21); -+ BLOCK_NAME_TO_ID.put("minecraft:lapis_block", 22); -+ BLOCK_NAME_TO_ID.put("minecraft:dispenser", 23); -+ BLOCK_NAME_TO_ID.put("minecraft:sandstone", 24); -+ BLOCK_NAME_TO_ID.put("minecraft:noteblock", 25); -+ BLOCK_NAME_TO_ID.put("minecraft:bed", 26); -+ BLOCK_NAME_TO_ID.put("minecraft:golden_rail", 27); -+ BLOCK_NAME_TO_ID.put("minecraft:detector_rail", 28); -+ BLOCK_NAME_TO_ID.put("minecraft:sticky_piston", 29); -+ BLOCK_NAME_TO_ID.put("minecraft:web", 30); -+ BLOCK_NAME_TO_ID.put("minecraft:tallgrass", 31); -+ BLOCK_NAME_TO_ID.put("minecraft:deadbush", 32); -+ BLOCK_NAME_TO_ID.put("minecraft:piston", 33); -+ BLOCK_NAME_TO_ID.put("minecraft:piston_head", 34); -+ BLOCK_NAME_TO_ID.put("minecraft:wool", 35); -+ BLOCK_NAME_TO_ID.put("minecraft:piston_extension", 36); -+ BLOCK_NAME_TO_ID.put("minecraft:yellow_flower", 37); -+ BLOCK_NAME_TO_ID.put("minecraft:red_flower", 38); -+ BLOCK_NAME_TO_ID.put("minecraft:brown_mushroom", 39); -+ BLOCK_NAME_TO_ID.put("minecraft:red_mushroom", 40); -+ BLOCK_NAME_TO_ID.put("minecraft:gold_block", 41); -+ BLOCK_NAME_TO_ID.put("minecraft:iron_block", 42); -+ BLOCK_NAME_TO_ID.put("minecraft:double_stone_slab", 43); -+ BLOCK_NAME_TO_ID.put("minecraft:stone_slab", 44); -+ BLOCK_NAME_TO_ID.put("minecraft:brick_block", 45); -+ BLOCK_NAME_TO_ID.put("minecraft:tnt", 46); -+ BLOCK_NAME_TO_ID.put("minecraft:bookshelf", 47); -+ BLOCK_NAME_TO_ID.put("minecraft:mossy_cobblestone", 48); -+ BLOCK_NAME_TO_ID.put("minecraft:obsidian", 49); -+ BLOCK_NAME_TO_ID.put("minecraft:torch", 50); -+ BLOCK_NAME_TO_ID.put("minecraft:fire", 51); -+ BLOCK_NAME_TO_ID.put("minecraft:mob_spawner", 52); -+ BLOCK_NAME_TO_ID.put("minecraft:oak_stairs", 53); -+ BLOCK_NAME_TO_ID.put("minecraft:chest", 54); -+ BLOCK_NAME_TO_ID.put("minecraft:redstone_wire", 55); -+ BLOCK_NAME_TO_ID.put("minecraft:diamond_ore", 56); -+ BLOCK_NAME_TO_ID.put("minecraft:diamond_block", 57); -+ BLOCK_NAME_TO_ID.put("minecraft:crafting_table", 58); -+ BLOCK_NAME_TO_ID.put("minecraft:wheat", 59); -+ BLOCK_NAME_TO_ID.put("minecraft:farmland", 60); -+ BLOCK_NAME_TO_ID.put("minecraft:furnace", 61); -+ BLOCK_NAME_TO_ID.put("minecraft:lit_furnace", 62); -+ BLOCK_NAME_TO_ID.put("minecraft:standing_sign", 63); -+ BLOCK_NAME_TO_ID.put("minecraft:wooden_door", 64); -+ BLOCK_NAME_TO_ID.put("minecraft:ladder", 65); -+ BLOCK_NAME_TO_ID.put("minecraft:rail", 66); -+ BLOCK_NAME_TO_ID.put("minecraft:stone_stairs", 67); -+ BLOCK_NAME_TO_ID.put("minecraft:wall_sign", 68); -+ BLOCK_NAME_TO_ID.put("minecraft:lever", 69); -+ BLOCK_NAME_TO_ID.put("minecraft:stone_pressure_plate", 70); -+ BLOCK_NAME_TO_ID.put("minecraft:iron_door", 71); -+ BLOCK_NAME_TO_ID.put("minecraft:wooden_pressure_plate", 72); -+ BLOCK_NAME_TO_ID.put("minecraft:redstone_ore", 73); -+ BLOCK_NAME_TO_ID.put("minecraft:lit_redstone_ore", 74); -+ BLOCK_NAME_TO_ID.put("minecraft:unlit_redstone_torch", 75); -+ BLOCK_NAME_TO_ID.put("minecraft:redstone_torch", 76); -+ BLOCK_NAME_TO_ID.put("minecraft:stone_button", 77); -+ BLOCK_NAME_TO_ID.put("minecraft:snow_layer", 78); -+ BLOCK_NAME_TO_ID.put("minecraft:ice", 79); -+ BLOCK_NAME_TO_ID.put("minecraft:snow", 80); -+ BLOCK_NAME_TO_ID.put("minecraft:cactus", 81); -+ BLOCK_NAME_TO_ID.put("minecraft:clay", 82); -+ BLOCK_NAME_TO_ID.put("minecraft:reeds", 83); -+ BLOCK_NAME_TO_ID.put("minecraft:jukebox", 84); -+ BLOCK_NAME_TO_ID.put("minecraft:fence", 85); -+ BLOCK_NAME_TO_ID.put("minecraft:pumpkin", 86); -+ BLOCK_NAME_TO_ID.put("minecraft:netherrack", 87); -+ BLOCK_NAME_TO_ID.put("minecraft:soul_sand", 88); -+ BLOCK_NAME_TO_ID.put("minecraft:glowstone", 89); -+ BLOCK_NAME_TO_ID.put("minecraft:portal", 90); -+ BLOCK_NAME_TO_ID.put("minecraft:lit_pumpkin", 91); -+ BLOCK_NAME_TO_ID.put("minecraft:cake", 92); -+ BLOCK_NAME_TO_ID.put("minecraft:unpowered_repeater", 93); -+ BLOCK_NAME_TO_ID.put("minecraft:powered_repeater", 94); -+ BLOCK_NAME_TO_ID.put("minecraft:stained_glass", 95); -+ BLOCK_NAME_TO_ID.put("minecraft:trapdoor", 96); -+ BLOCK_NAME_TO_ID.put("minecraft:monster_egg", 97); -+ BLOCK_NAME_TO_ID.put("minecraft:stonebrick", 98); -+ BLOCK_NAME_TO_ID.put("minecraft:brown_mushroom_block", 99); -+ BLOCK_NAME_TO_ID.put("minecraft:red_mushroom_block", 100); -+ BLOCK_NAME_TO_ID.put("minecraft:iron_bars", 101); -+ BLOCK_NAME_TO_ID.put("minecraft:glass_pane", 102); -+ BLOCK_NAME_TO_ID.put("minecraft:melon_block", 103); -+ BLOCK_NAME_TO_ID.put("minecraft:pumpkin_stem", 104); -+ BLOCK_NAME_TO_ID.put("minecraft:melon_stem", 105); -+ BLOCK_NAME_TO_ID.put("minecraft:vine", 106); -+ BLOCK_NAME_TO_ID.put("minecraft:fence_gate", 107); -+ BLOCK_NAME_TO_ID.put("minecraft:brick_stairs", 108); -+ BLOCK_NAME_TO_ID.put("minecraft:stone_brick_stairs", 109); -+ BLOCK_NAME_TO_ID.put("minecraft:mycelium", 110); -+ BLOCK_NAME_TO_ID.put("minecraft:waterlily", 111); -+ BLOCK_NAME_TO_ID.put("minecraft:nether_brick", 112); -+ BLOCK_NAME_TO_ID.put("minecraft:nether_brick_fence", 113); -+ BLOCK_NAME_TO_ID.put("minecraft:nether_brick_stairs", 114); -+ BLOCK_NAME_TO_ID.put("minecraft:nether_wart", 115); -+ BLOCK_NAME_TO_ID.put("minecraft:enchanting_table", 116); -+ BLOCK_NAME_TO_ID.put("minecraft:brewing_stand", 117); -+ BLOCK_NAME_TO_ID.put("minecraft:cauldron", 118); -+ BLOCK_NAME_TO_ID.put("minecraft:end_portal", 119); -+ BLOCK_NAME_TO_ID.put("minecraft:end_portal_frame", 120); -+ BLOCK_NAME_TO_ID.put("minecraft:end_stone", 121); -+ BLOCK_NAME_TO_ID.put("minecraft:dragon_egg", 122); -+ BLOCK_NAME_TO_ID.put("minecraft:redstone_lamp", 123); -+ BLOCK_NAME_TO_ID.put("minecraft:lit_redstone_lamp", 124); -+ BLOCK_NAME_TO_ID.put("minecraft:double_wooden_slab", 125); -+ BLOCK_NAME_TO_ID.put("minecraft:wooden_slab", 126); -+ BLOCK_NAME_TO_ID.put("minecraft:cocoa", 127); -+ BLOCK_NAME_TO_ID.put("minecraft:sandstone_stairs", 128); -+ BLOCK_NAME_TO_ID.put("minecraft:emerald_ore", 129); -+ BLOCK_NAME_TO_ID.put("minecraft:ender_chest", 130); -+ BLOCK_NAME_TO_ID.put("minecraft:tripwire_hook", 131); -+ BLOCK_NAME_TO_ID.put("minecraft:tripwire", 132); -+ BLOCK_NAME_TO_ID.put("minecraft:emerald_block", 133); -+ BLOCK_NAME_TO_ID.put("minecraft:spruce_stairs", 134); -+ BLOCK_NAME_TO_ID.put("minecraft:birch_stairs", 135); -+ BLOCK_NAME_TO_ID.put("minecraft:jungle_stairs", 136); -+ BLOCK_NAME_TO_ID.put("minecraft:command_block", 137); -+ BLOCK_NAME_TO_ID.put("minecraft:beacon", 138); -+ BLOCK_NAME_TO_ID.put("minecraft:cobblestone_wall", 139); -+ BLOCK_NAME_TO_ID.put("minecraft:flower_pot", 140); -+ BLOCK_NAME_TO_ID.put("minecraft:carrots", 141); -+ BLOCK_NAME_TO_ID.put("minecraft:potatoes", 142); -+ BLOCK_NAME_TO_ID.put("minecraft:wooden_button", 143); -+ BLOCK_NAME_TO_ID.put("minecraft:skull", 144); -+ BLOCK_NAME_TO_ID.put("minecraft:anvil", 145); -+ BLOCK_NAME_TO_ID.put("minecraft:trapped_chest", 146); -+ BLOCK_NAME_TO_ID.put("minecraft:light_weighted_pressure_plate", 147); -+ BLOCK_NAME_TO_ID.put("minecraft:heavy_weighted_pressure_plate", 148); -+ BLOCK_NAME_TO_ID.put("minecraft:unpowered_comparator", 149); -+ BLOCK_NAME_TO_ID.put("minecraft:powered_comparator", 150); -+ BLOCK_NAME_TO_ID.put("minecraft:daylight_detector", 151); -+ BLOCK_NAME_TO_ID.put("minecraft:redstone_block", 152); -+ BLOCK_NAME_TO_ID.put("minecraft:quartz_ore", 153); -+ BLOCK_NAME_TO_ID.put("minecraft:hopper", 154); -+ BLOCK_NAME_TO_ID.put("minecraft:quartz_block", 155); -+ BLOCK_NAME_TO_ID.put("minecraft:quartz_stairs", 156); -+ BLOCK_NAME_TO_ID.put("minecraft:activator_rail", 157); -+ BLOCK_NAME_TO_ID.put("minecraft:dropper", 158); -+ BLOCK_NAME_TO_ID.put("minecraft:stained_hardened_clay", 159); -+ BLOCK_NAME_TO_ID.put("minecraft:stained_glass_pane", 160); -+ BLOCK_NAME_TO_ID.put("minecraft:leaves2", 161); -+ BLOCK_NAME_TO_ID.put("minecraft:log2", 162); -+ BLOCK_NAME_TO_ID.put("minecraft:acacia_stairs", 163); -+ BLOCK_NAME_TO_ID.put("minecraft:dark_oak_stairs", 164); -+ BLOCK_NAME_TO_ID.put("minecraft:slime", 165); -+ BLOCK_NAME_TO_ID.put("minecraft:barrier", 166); -+ BLOCK_NAME_TO_ID.put("minecraft:iron_trapdoor", 167); -+ BLOCK_NAME_TO_ID.put("minecraft:prismarine", 168); -+ BLOCK_NAME_TO_ID.put("minecraft:sea_lantern", 169); -+ BLOCK_NAME_TO_ID.put("minecraft:hay_block", 170); -+ BLOCK_NAME_TO_ID.put("minecraft:carpet", 171); -+ BLOCK_NAME_TO_ID.put("minecraft:hardened_clay", 172); -+ BLOCK_NAME_TO_ID.put("minecraft:coal_block", 173); -+ BLOCK_NAME_TO_ID.put("minecraft:packed_ice", 174); -+ BLOCK_NAME_TO_ID.put("minecraft:double_plant", 175); -+ BLOCK_NAME_TO_ID.put("minecraft:standing_banner", 176); -+ BLOCK_NAME_TO_ID.put("minecraft:wall_banner", 177); -+ BLOCK_NAME_TO_ID.put("minecraft:daylight_detector_inverted", 178); -+ BLOCK_NAME_TO_ID.put("minecraft:red_sandstone", 179); -+ BLOCK_NAME_TO_ID.put("minecraft:red_sandstone_stairs", 180); -+ BLOCK_NAME_TO_ID.put("minecraft:double_stone_slab2", 181); -+ BLOCK_NAME_TO_ID.put("minecraft:stone_slab2", 182); -+ BLOCK_NAME_TO_ID.put("minecraft:spruce_fence_gate", 183); -+ BLOCK_NAME_TO_ID.put("minecraft:birch_fence_gate", 184); -+ BLOCK_NAME_TO_ID.put("minecraft:jungle_fence_gate", 185); -+ BLOCK_NAME_TO_ID.put("minecraft:dark_oak_fence_gate", 186); -+ BLOCK_NAME_TO_ID.put("minecraft:acacia_fence_gate", 187); -+ BLOCK_NAME_TO_ID.put("minecraft:spruce_fence", 188); -+ BLOCK_NAME_TO_ID.put("minecraft:birch_fence", 189); -+ BLOCK_NAME_TO_ID.put("minecraft:jungle_fence", 190); -+ BLOCK_NAME_TO_ID.put("minecraft:dark_oak_fence", 191); -+ BLOCK_NAME_TO_ID.put("minecraft:acacia_fence", 192); -+ BLOCK_NAME_TO_ID.put("minecraft:spruce_door", 193); -+ BLOCK_NAME_TO_ID.put("minecraft:birch_door", 194); -+ BLOCK_NAME_TO_ID.put("minecraft:jungle_door", 195); -+ BLOCK_NAME_TO_ID.put("minecraft:acacia_door", 196); -+ BLOCK_NAME_TO_ID.put("minecraft:dark_oak_door", 197); -+ BLOCK_NAME_TO_ID.put("minecraft:end_rod", 198); -+ BLOCK_NAME_TO_ID.put("minecraft:chorus_plant", 199); -+ BLOCK_NAME_TO_ID.put("minecraft:chorus_flower", 200); -+ BLOCK_NAME_TO_ID.put("minecraft:purpur_block", 201); -+ BLOCK_NAME_TO_ID.put("minecraft:purpur_pillar", 202); -+ BLOCK_NAME_TO_ID.put("minecraft:purpur_stairs", 203); -+ BLOCK_NAME_TO_ID.put("minecraft:purpur_double_slab", 204); -+ BLOCK_NAME_TO_ID.put("minecraft:purpur_slab", 205); -+ BLOCK_NAME_TO_ID.put("minecraft:end_bricks", 206); -+ BLOCK_NAME_TO_ID.put("minecraft:beetroots", 207); -+ BLOCK_NAME_TO_ID.put("minecraft:grass_path", 208); -+ BLOCK_NAME_TO_ID.put("minecraft:end_gateway", 209); -+ BLOCK_NAME_TO_ID.put("minecraft:repeating_command_block", 210); -+ BLOCK_NAME_TO_ID.put("minecraft:chain_command_block", 211); -+ BLOCK_NAME_TO_ID.put("minecraft:frosted_ice", 212); -+ BLOCK_NAME_TO_ID.put("minecraft:magma", 213); -+ BLOCK_NAME_TO_ID.put("minecraft:nether_wart_block", 214); -+ BLOCK_NAME_TO_ID.put("minecraft:red_nether_brick", 215); -+ BLOCK_NAME_TO_ID.put("minecraft:bone_block", 216); -+ BLOCK_NAME_TO_ID.put("minecraft:structure_void", 217); -+ BLOCK_NAME_TO_ID.put("minecraft:observer", 218); -+ BLOCK_NAME_TO_ID.put("minecraft:white_shulker_box", 219); -+ BLOCK_NAME_TO_ID.put("minecraft:orange_shulker_box", 220); -+ BLOCK_NAME_TO_ID.put("minecraft:magenta_shulker_box", 221); -+ BLOCK_NAME_TO_ID.put("minecraft:light_blue_shulker_box", 222); -+ BLOCK_NAME_TO_ID.put("minecraft:yellow_shulker_box", 223); -+ BLOCK_NAME_TO_ID.put("minecraft:lime_shulker_box", 224); -+ BLOCK_NAME_TO_ID.put("minecraft:pink_shulker_box", 225); -+ BLOCK_NAME_TO_ID.put("minecraft:gray_shulker_box", 226); -+ BLOCK_NAME_TO_ID.put("minecraft:silver_shulker_box", 227); -+ BLOCK_NAME_TO_ID.put("minecraft:cyan_shulker_box", 228); -+ BLOCK_NAME_TO_ID.put("minecraft:purple_shulker_box", 229); -+ BLOCK_NAME_TO_ID.put("minecraft:blue_shulker_box", 230); -+ BLOCK_NAME_TO_ID.put("minecraft:brown_shulker_box", 231); -+ BLOCK_NAME_TO_ID.put("minecraft:green_shulker_box", 232); -+ BLOCK_NAME_TO_ID.put("minecraft:red_shulker_box", 233); -+ BLOCK_NAME_TO_ID.put("minecraft:black_shulker_box", 234); -+ BLOCK_NAME_TO_ID.put("minecraft:white_glazed_terracotta", 235); -+ BLOCK_NAME_TO_ID.put("minecraft:orange_glazed_terracotta", 236); -+ BLOCK_NAME_TO_ID.put("minecraft:magenta_glazed_terracotta", 237); -+ BLOCK_NAME_TO_ID.put("minecraft:light_blue_glazed_terracotta", 238); -+ BLOCK_NAME_TO_ID.put("minecraft:yellow_glazed_terracotta", 239); -+ BLOCK_NAME_TO_ID.put("minecraft:lime_glazed_terracotta", 240); -+ BLOCK_NAME_TO_ID.put("minecraft:pink_glazed_terracotta", 241); -+ BLOCK_NAME_TO_ID.put("minecraft:gray_glazed_terracotta", 242); -+ BLOCK_NAME_TO_ID.put("minecraft:silver_glazed_terracotta", 243); -+ BLOCK_NAME_TO_ID.put("minecraft:cyan_glazed_terracotta", 244); -+ BLOCK_NAME_TO_ID.put("minecraft:purple_glazed_terracotta", 245); -+ BLOCK_NAME_TO_ID.put("minecraft:blue_glazed_terracotta", 246); -+ BLOCK_NAME_TO_ID.put("minecraft:brown_glazed_terracotta", 247); -+ BLOCK_NAME_TO_ID.put("minecraft:green_glazed_terracotta", 248); -+ BLOCK_NAME_TO_ID.put("minecraft:red_glazed_terracotta", 249); -+ BLOCK_NAME_TO_ID.put("minecraft:black_glazed_terracotta", 250); -+ BLOCK_NAME_TO_ID.put("minecraft:concrete", 251); -+ BLOCK_NAME_TO_ID.put("minecraft:concrete_powder", 252); -+ BLOCK_NAME_TO_ID.put("minecraft:structure_block", 255); -+ } -+ -+ protected static final int VERSION = MCVersions.V17W47A; -+ -+ protected final String[] paths; -+ -+ public ConverterFlattenEntity(final String... paths) { -+ super(VERSION, 3); -+ this.paths = paths; -+ } -+ -+ private static void register(final String id, final String... paths) { -+ MCTypeRegistry.ENTITY.addConverterForId(id, new ConverterFlattenEntity(paths)); -+ } -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:falling_block", new DataConverter<>(VERSION, 3) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final int blockId; -+ if (data.hasKey("Block")) { -+ final Number id = data.getNumber("Block"); -+ if (id != null) { -+ blockId = id.intValue(); -+ } else { -+ blockId = getBlockId(data.getString("Block")); -+ } -+ } else { -+ final Number tileId = data.getNumber("TileID"); -+ if (tileId != null) { -+ blockId = tileId.intValue(); -+ } else { -+ blockId = data.getByte("Tile") & 255; -+ } -+ } -+ -+ final int blockData = data.getInt("Data") & 15; -+ -+ data.remove("Block"); // from type update -+ data.remove("Data"); -+ data.remove("TileID"); -+ data.remove("Tile"); -+ -+ // key is from type update -+ data.setMap("BlockState", HelperBlockFlatteningV1450.getNBTForId((blockId << 4) | blockData).copy()); // copy to avoid problems with later state datafixers -+ -+ return null; -+ } -+ }); -+ register("minecraft:enderman", "carried", "carriedData", "carriedBlockState"); -+ register("minecraft:arrow", "inTile", "inData", "inBlockState"); -+ register("minecraft:spectral_arrow", "inTile", "inData", "inBlockState"); -+ register("minecraft:egg", "inTile"); -+ register("minecraft:ender_pearl", "inTile"); -+ register("minecraft:fireball", "inTile"); -+ register("minecraft:potion", "inTile"); -+ register("minecraft:small_fireball", "inTile"); -+ register("minecraft:snowball", "inTile"); -+ register("minecraft:wither_skull", "inTile"); -+ register("minecraft:xp_bottle", "inTile"); -+ register("minecraft:commandblock_minecart", "DisplayTile", "DisplayData", "DisplayState"); -+ register("minecraft:minecart", "DisplayTile", "DisplayData", "DisplayState"); -+ register("minecraft:chest_minecart", "DisplayTile", "DisplayData", "DisplayState"); -+ register("minecraft:furnace_minecart", "DisplayTile", "DisplayData", "DisplayState"); -+ register("minecraft:tnt_minecart", "DisplayTile", "DisplayData", "DisplayState"); -+ register("minecraft:hopper_minecart", "DisplayTile", "DisplayData", "DisplayState"); -+ register("minecraft:spawner_minecart", "DisplayTile", "DisplayData", "DisplayState"); -+ } -+ -+ public static int getBlockId(final String block) { -+ final Integer ret = BLOCK_NAME_TO_ID.get(block); -+ return ret == null ? 0 : ret.intValue(); -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (this.paths.length == 1) { -+ data.remove(this.paths[0]); -+ return null; -+ } -+ final String idPath = this.paths[0]; -+ final String dataPath = this.paths[1]; -+ final String outputStatePath = this.paths[2]; -+ -+ final int blockId; -+ if (data.hasKey(idPath, ObjectType.NUMBER)) { -+ blockId = data.getInt(idPath); -+ } else { -+ blockId = getBlockId(data.getString(idPath)); -+ } -+ -+ final int blockData = data.getInt(dataPath) & 15; -+ -+ data.remove(idPath); // from type update -+ data.remove(dataPath); -+ -+ data.setMap(outputStatePath, HelperBlockFlatteningV1450.getNBTForId((blockId << 4) | blockData).copy()); // copy to avoid problems with later state datafixers -+ -+ return null; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/AddFlagIfAbsent.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/AddFlagIfAbsent.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4ab607f946782cc483535564e86fa9753dd7897a ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/AddFlagIfAbsent.java -@@ -0,0 +1,30 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.helpers; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class AddFlagIfAbsent extends DataConverter, MapType> { -+ -+ public final String path; -+ public final boolean dfl; -+ -+ public AddFlagIfAbsent(final int toVersion, final String path, final boolean dfl) { -+ super(toVersion); -+ this.path = path; -+ this.dfl = dfl; -+ } -+ -+ public AddFlagIfAbsent(final int toVersion, final int versionStep, final String path, final boolean dfl) { -+ super(toVersion, versionStep); -+ this.path = path; -+ this.dfl = dfl; -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (!data.hasKey(this.path)) { -+ data.setBoolean(this.path, this.dfl); -+ } -+ return null; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/ConverterAbstractStringValueTypeRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/ConverterAbstractStringValueTypeRename.java -new file mode 100644 -index 0000000000000000000000000000000000000000..bc79670f47aaa413ea3e96ef6a32e14099ad8a58 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/ConverterAbstractStringValueTypeRename.java -@@ -0,0 +1,24 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.helpers; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCValueType; -+import java.util.function.Function; -+ -+public final class ConverterAbstractStringValueTypeRename { -+ -+ private ConverterAbstractStringValueTypeRename() {} -+ -+ public static void register(final int version, final MCValueType type, final Function renamer) { -+ register(version, 0, type, renamer); -+ } -+ public static void register(final int version, final int subVersion, final MCValueType type, final Function renamer) { -+ type.addConverter(new DataConverter<>(version, subVersion) { -+ @Override -+ public Object convert(final Object data, final long sourceVersion, final long toVersion) { -+ final String ret = (data instanceof String) ? renamer.apply((String)data) : null; -+ return ret == data ? null : ret; -+ } -+ }); -+ } -+ -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperBlockFlatteningV1450.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperBlockFlatteningV1450.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4f4f4cb6037c2a46ffcf427f5812164bbb98b8b7 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperBlockFlatteningV1450.java -@@ -0,0 +1,1829 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.helpers; -+ -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.nbt.NBTMapType; -+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; -+import net.minecraft.nbt.TagParser; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class HelperBlockFlatteningV1450 { -+ -+ protected static final MapType[] FLATTENED_BY_ID = new MapType[4096]; -+ protected static final MapType[] BLOCK_DEFAULTS = new MapType[4096]; -+ -+ private static final Object2IntOpenHashMap> ID_BY_OLD_NBT = new Object2IntOpenHashMap>(64, 0.7f) { -+ @Override -+ public int put(final MapType o, final int v) { -+ if (this.containsKey(o)) { -+ throw new RuntimeException("Already contains mapping for " + o); -+ } -+ -+ return super.put(o, v); -+ } -+ }; -+ static { -+ ID_BY_OLD_NBT.defaultReturnValue(-1); -+ } -+ -+ private static final Object2IntOpenHashMap ID_BY_OLD_NAME = new Object2IntOpenHashMap(64, 0.7f) { -+ @Override -+ public int put(final String o, final int v) { -+ if (this.containsKey(o)) { -+ throw new RuntimeException("Already contains mapping for " + o); -+ } -+ -+ return super.put(o, v); -+ } -+ }; -+ static { -+ ID_BY_OLD_NAME.defaultReturnValue(-1); -+ } -+ -+ // map used to ensure that each parsed block state contains no duplicates -+ protected static final Map, MapType> IDENTITY_ENSURE = new HashMap<>(); -+ -+ public static MapType parseTag(final String blockstate) { -+ try { -+ final MapType ret = new NBTMapType(TagParser.parseTag(blockstate.replace('\'', '"'))); -+ -+ synchronized (IDENTITY_ENSURE) { -+ final MapType identity = IDENTITY_ENSURE.putIfAbsent(ret, ret); -+ -+ return identity == null ? ret : identity; -+ } -+ -+ } catch (final Exception ex) { -+ throw new RuntimeException("Exception parsing " + blockstate, ex); -+ } -+ } -+ -+ private static void register(final int id, final String flattened, final String... preFlattenings) { -+ final MapType flattenedNBT = parseTag(flattened); -+ if (FLATTENED_BY_ID[id] != null) { -+ throw new RuntimeException("Mapping already exists for id " + id); -+ } -+ FLATTENED_BY_ID[id] = flattenedNBT; -+ -+ // it's important that we register ids from smallest to largest, so that -+ // the default is going to be correct -+ final int block = id >> 4; -+ if (BLOCK_DEFAULTS[block] == null) { -+ BLOCK_DEFAULTS[block] = flattenedNBT; -+ } -+ -+ for (final String preFlattening : preFlattenings) { -+ final MapType preFlatteningNBT = parseTag(preFlattening); -+ final String name = preFlatteningNBT.getString("Name"); -+ if (name == null) { -+ throw new RuntimeException("Name does not exist for pre flattenings for id " + id); -+ } -+ -+ // putIfAbsent so we default to the lowest id, which is going to be the block default -+ ID_BY_OLD_NAME.putIfAbsent(name, id); -+ ID_BY_OLD_NBT.put(preFlatteningNBT, id); -+ } -+ } -+ -+ private static void finalizeMaps() { -+ for(int i = 0; i < FLATTENED_BY_ID.length; ++i) { -+ if (FLATTENED_BY_ID[i] == null) { -+ FLATTENED_BY_ID[i] = BLOCK_DEFAULTS[i >> 4]; -+ } -+ } -+ } -+ -+ public static MapType flattenNBT(final MapType old) { -+ final int id = ID_BY_OLD_NBT.getInt(old); -+ final MapType ret = getNBTForIdRaw(id); -+ -+ return ret == null ? old : ret; -+ } -+ -+ public static String getNewBlockName(final String old) { -+ final int id = ID_BY_OLD_NAME.getInt(old); -+ final MapType ret = getNBTForIdRaw(id); -+ return ret == null ? old : ret.getString("Name"); -+ } -+ -+ public static String getNameForId(final int block) { -+ final MapType nbt = getNBTForIdRaw(block); -+ return nbt == null ? "minecraft:air" : nbt.getString("Name"); -+ } -+ -+ protected static MapType getNBTForIdRaw(final int block) { -+ return block >= 0 && block < FLATTENED_BY_ID.length ? FLATTENED_BY_ID[block] : null; -+ } -+ -+ public static MapType getNBTForId(final int block) { -+ MapType ret = getNBTForIdRaw(block); -+ return ret == null ? FLATTENED_BY_ID[0] : ret; -+ } -+ -+ private HelperBlockFlatteningV1450() {} -+ -+ static { -+ ID_BY_OLD_NBT.defaultReturnValue(-1); -+ register(0, "{Name:'minecraft:air'}", "{Name:'minecraft:air'}"); -+ register(16, "{Name:'minecraft:stone'}", "{Name:'minecraft:stone',Properties:{variant:'stone'}}"); -+ register(17, "{Name:'minecraft:granite'}", "{Name:'minecraft:stone',Properties:{variant:'granite'}}"); -+ register(18, "{Name:'minecraft:polished_granite'}", "{Name:'minecraft:stone',Properties:{variant:'smooth_granite'}}"); -+ register(19, "{Name:'minecraft:diorite'}", "{Name:'minecraft:stone',Properties:{variant:'diorite'}}"); -+ register(20, "{Name:'minecraft:polished_diorite'}", "{Name:'minecraft:stone',Properties:{variant:'smooth_diorite'}}"); -+ register(21, "{Name:'minecraft:andesite'}", "{Name:'minecraft:stone',Properties:{variant:'andesite'}}"); -+ register(22, "{Name:'minecraft:polished_andesite'}", "{Name:'minecraft:stone',Properties:{variant:'smooth_andesite'}}"); -+ register(32, "{Name:'minecraft:grass_block',Properties:{snowy:'false'}}", "{Name:'minecraft:grass',Properties:{snowy:'false'}}", "{Name:'minecraft:grass',Properties:{snowy:'true'}}"); -+ register(48, "{Name:'minecraft:dirt'}", "{Name:'minecraft:dirt',Properties:{snowy:'false',variant:'dirt'}}", "{Name:'minecraft:dirt',Properties:{snowy:'true',variant:'dirt'}}"); -+ register(49, "{Name:'minecraft:coarse_dirt'}", "{Name:'minecraft:dirt',Properties:{snowy:'false',variant:'coarse_dirt'}}", "{Name:'minecraft:dirt',Properties:{snowy:'true',variant:'coarse_dirt'}}"); -+ register(50, "{Name:'minecraft:podzol',Properties:{snowy:'false'}}", "{Name:'minecraft:dirt',Properties:{snowy:'false',variant:'podzol'}}", "{Name:'minecraft:dirt',Properties:{snowy:'true',variant:'podzol'}}"); -+ register(64, "{Name:'minecraft:cobblestone'}", "{Name:'minecraft:cobblestone'}"); -+ register(80, "{Name:'minecraft:oak_planks'}", "{Name:'minecraft:planks',Properties:{variant:'oak'}}"); -+ register(81, "{Name:'minecraft:spruce_planks'}", "{Name:'minecraft:planks',Properties:{variant:'spruce'}}"); -+ register(82, "{Name:'minecraft:birch_planks'}", "{Name:'minecraft:planks',Properties:{variant:'birch'}}"); -+ register(83, "{Name:'minecraft:jungle_planks'}", "{Name:'minecraft:planks',Properties:{variant:'jungle'}}"); -+ register(84, "{Name:'minecraft:acacia_planks'}", "{Name:'minecraft:planks',Properties:{variant:'acacia'}}"); -+ register(85, "{Name:'minecraft:dark_oak_planks'}", "{Name:'minecraft:planks',Properties:{variant:'dark_oak'}}"); -+ register(96, "{Name:'minecraft:oak_sapling',Properties:{stage:'0'}}", "{Name:'minecraft:sapling',Properties:{stage:'0',type:'oak'}}"); -+ register(97, "{Name:'minecraft:spruce_sapling',Properties:{stage:'0'}}", "{Name:'minecraft:sapling',Properties:{stage:'0',type:'spruce'}}"); -+ register(98, "{Name:'minecraft:birch_sapling',Properties:{stage:'0'}}", "{Name:'minecraft:sapling',Properties:{stage:'0',type:'birch'}}"); -+ register(99, "{Name:'minecraft:jungle_sapling',Properties:{stage:'0'}}", "{Name:'minecraft:sapling',Properties:{stage:'0',type:'jungle'}}"); -+ register(100, "{Name:'minecraft:acacia_sapling',Properties:{stage:'0'}}", "{Name:'minecraft:sapling',Properties:{stage:'0',type:'acacia'}}"); -+ register(101, "{Name:'minecraft:dark_oak_sapling',Properties:{stage:'0'}}", "{Name:'minecraft:sapling',Properties:{stage:'0',type:'dark_oak'}}"); -+ register(104, "{Name:'minecraft:oak_sapling',Properties:{stage:'1'}}", "{Name:'minecraft:sapling',Properties:{stage:'1',type:'oak'}}"); -+ register(105, "{Name:'minecraft:spruce_sapling',Properties:{stage:'1'}}", "{Name:'minecraft:sapling',Properties:{stage:'1',type:'spruce'}}"); -+ register(106, "{Name:'minecraft:birch_sapling',Properties:{stage:'1'}}", "{Name:'minecraft:sapling',Properties:{stage:'1',type:'birch'}}"); -+ register(107, "{Name:'minecraft:jungle_sapling',Properties:{stage:'1'}}", "{Name:'minecraft:sapling',Properties:{stage:'1',type:'jungle'}}"); -+ register(108, "{Name:'minecraft:acacia_sapling',Properties:{stage:'1'}}", "{Name:'minecraft:sapling',Properties:{stage:'1',type:'acacia'}}"); -+ register(109, "{Name:'minecraft:dark_oak_sapling',Properties:{stage:'1'}}", "{Name:'minecraft:sapling',Properties:{stage:'1',type:'dark_oak'}}"); -+ register(112, "{Name:'minecraft:bedrock'}", "{Name:'minecraft:bedrock'}"); -+ register(128, "{Name:'minecraft:water',Properties:{level:'0'}}", "{Name:'minecraft:flowing_water',Properties:{level:'0'}}"); -+ register(129, "{Name:'minecraft:water',Properties:{level:'1'}}", "{Name:'minecraft:flowing_water',Properties:{level:'1'}}"); -+ register(130, "{Name:'minecraft:water',Properties:{level:'2'}}", "{Name:'minecraft:flowing_water',Properties:{level:'2'}}"); -+ register(131, "{Name:'minecraft:water',Properties:{level:'3'}}", "{Name:'minecraft:flowing_water',Properties:{level:'3'}}"); -+ register(132, "{Name:'minecraft:water',Properties:{level:'4'}}", "{Name:'minecraft:flowing_water',Properties:{level:'4'}}"); -+ register(133, "{Name:'minecraft:water',Properties:{level:'5'}}", "{Name:'minecraft:flowing_water',Properties:{level:'5'}}"); -+ register(134, "{Name:'minecraft:water',Properties:{level:'6'}}", "{Name:'minecraft:flowing_water',Properties:{level:'6'}}"); -+ register(135, "{Name:'minecraft:water',Properties:{level:'7'}}", "{Name:'minecraft:flowing_water',Properties:{level:'7'}}"); -+ register(136, "{Name:'minecraft:water',Properties:{level:'8'}}", "{Name:'minecraft:flowing_water',Properties:{level:'8'}}"); -+ register(137, "{Name:'minecraft:water',Properties:{level:'9'}}", "{Name:'minecraft:flowing_water',Properties:{level:'9'}}"); -+ register(138, "{Name:'minecraft:water',Properties:{level:'10'}}", "{Name:'minecraft:flowing_water',Properties:{level:'10'}}"); -+ register(139, "{Name:'minecraft:water',Properties:{level:'11'}}", "{Name:'minecraft:flowing_water',Properties:{level:'11'}}"); -+ register(140, "{Name:'minecraft:water',Properties:{level:'12'}}", "{Name:'minecraft:flowing_water',Properties:{level:'12'}}"); -+ register(141, "{Name:'minecraft:water',Properties:{level:'13'}}", "{Name:'minecraft:flowing_water',Properties:{level:'13'}}"); -+ register(142, "{Name:'minecraft:water',Properties:{level:'14'}}", "{Name:'minecraft:flowing_water',Properties:{level:'14'}}"); -+ register(143, "{Name:'minecraft:water',Properties:{level:'15'}}", "{Name:'minecraft:flowing_water',Properties:{level:'15'}}"); -+ register(144, "{Name:'minecraft:water',Properties:{level:'0'}}", "{Name:'minecraft:water',Properties:{level:'0'}}"); -+ register(145, "{Name:'minecraft:water',Properties:{level:'1'}}", "{Name:'minecraft:water',Properties:{level:'1'}}"); -+ register(146, "{Name:'minecraft:water',Properties:{level:'2'}}", "{Name:'minecraft:water',Properties:{level:'2'}}"); -+ register(147, "{Name:'minecraft:water',Properties:{level:'3'}}", "{Name:'minecraft:water',Properties:{level:'3'}}"); -+ register(148, "{Name:'minecraft:water',Properties:{level:'4'}}", "{Name:'minecraft:water',Properties:{level:'4'}}"); -+ register(149, "{Name:'minecraft:water',Properties:{level:'5'}}", "{Name:'minecraft:water',Properties:{level:'5'}}"); -+ register(150, "{Name:'minecraft:water',Properties:{level:'6'}}", "{Name:'minecraft:water',Properties:{level:'6'}}"); -+ register(151, "{Name:'minecraft:water',Properties:{level:'7'}}", "{Name:'minecraft:water',Properties:{level:'7'}}"); -+ register(152, "{Name:'minecraft:water',Properties:{level:'8'}}", "{Name:'minecraft:water',Properties:{level:'8'}}"); -+ register(153, "{Name:'minecraft:water',Properties:{level:'9'}}", "{Name:'minecraft:water',Properties:{level:'9'}}"); -+ register(154, "{Name:'minecraft:water',Properties:{level:'10'}}", "{Name:'minecraft:water',Properties:{level:'10'}}"); -+ register(155, "{Name:'minecraft:water',Properties:{level:'11'}}", "{Name:'minecraft:water',Properties:{level:'11'}}"); -+ register(156, "{Name:'minecraft:water',Properties:{level:'12'}}", "{Name:'minecraft:water',Properties:{level:'12'}}"); -+ register(157, "{Name:'minecraft:water',Properties:{level:'13'}}", "{Name:'minecraft:water',Properties:{level:'13'}}"); -+ register(158, "{Name:'minecraft:water',Properties:{level:'14'}}", "{Name:'minecraft:water',Properties:{level:'14'}}"); -+ register(159, "{Name:'minecraft:water',Properties:{level:'15'}}", "{Name:'minecraft:water',Properties:{level:'15'}}"); -+ register(160, "{Name:'minecraft:lava',Properties:{level:'0'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'0'}}"); -+ register(161, "{Name:'minecraft:lava',Properties:{level:'1'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'1'}}"); -+ register(162, "{Name:'minecraft:lava',Properties:{level:'2'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'2'}}"); -+ register(163, "{Name:'minecraft:lava',Properties:{level:'3'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'3'}}"); -+ register(164, "{Name:'minecraft:lava',Properties:{level:'4'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'4'}}"); -+ register(165, "{Name:'minecraft:lava',Properties:{level:'5'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'5'}}"); -+ register(166, "{Name:'minecraft:lava',Properties:{level:'6'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'6'}}"); -+ register(167, "{Name:'minecraft:lava',Properties:{level:'7'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'7'}}"); -+ register(168, "{Name:'minecraft:lava',Properties:{level:'8'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'8'}}"); -+ register(169, "{Name:'minecraft:lava',Properties:{level:'9'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'9'}}"); -+ register(170, "{Name:'minecraft:lava',Properties:{level:'10'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'10'}}"); -+ register(171, "{Name:'minecraft:lava',Properties:{level:'11'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'11'}}"); -+ register(172, "{Name:'minecraft:lava',Properties:{level:'12'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'12'}}"); -+ register(173, "{Name:'minecraft:lava',Properties:{level:'13'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'13'}}"); -+ register(174, "{Name:'minecraft:lava',Properties:{level:'14'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'14'}}"); -+ register(175, "{Name:'minecraft:lava',Properties:{level:'15'}}", "{Name:'minecraft:flowing_lava',Properties:{level:'15'}}"); -+ register(176, "{Name:'minecraft:lava',Properties:{level:'0'}}", "{Name:'minecraft:lava',Properties:{level:'0'}}"); -+ register(177, "{Name:'minecraft:lava',Properties:{level:'1'}}", "{Name:'minecraft:lava',Properties:{level:'1'}}"); -+ register(178, "{Name:'minecraft:lava',Properties:{level:'2'}}", "{Name:'minecraft:lava',Properties:{level:'2'}}"); -+ register(179, "{Name:'minecraft:lava',Properties:{level:'3'}}", "{Name:'minecraft:lava',Properties:{level:'3'}}"); -+ register(180, "{Name:'minecraft:lava',Properties:{level:'4'}}", "{Name:'minecraft:lava',Properties:{level:'4'}}"); -+ register(181, "{Name:'minecraft:lava',Properties:{level:'5'}}", "{Name:'minecraft:lava',Properties:{level:'5'}}"); -+ register(182, "{Name:'minecraft:lava',Properties:{level:'6'}}", "{Name:'minecraft:lava',Properties:{level:'6'}}"); -+ register(183, "{Name:'minecraft:lava',Properties:{level:'7'}}", "{Name:'minecraft:lava',Properties:{level:'7'}}"); -+ register(184, "{Name:'minecraft:lava',Properties:{level:'8'}}", "{Name:'minecraft:lava',Properties:{level:'8'}}"); -+ register(185, "{Name:'minecraft:lava',Properties:{level:'9'}}", "{Name:'minecraft:lava',Properties:{level:'9'}}"); -+ register(186, "{Name:'minecraft:lava',Properties:{level:'10'}}", "{Name:'minecraft:lava',Properties:{level:'10'}}"); -+ register(187, "{Name:'minecraft:lava',Properties:{level:'11'}}", "{Name:'minecraft:lava',Properties:{level:'11'}}"); -+ register(188, "{Name:'minecraft:lava',Properties:{level:'12'}}", "{Name:'minecraft:lava',Properties:{level:'12'}}"); -+ register(189, "{Name:'minecraft:lava',Properties:{level:'13'}}", "{Name:'minecraft:lava',Properties:{level:'13'}}"); -+ register(190, "{Name:'minecraft:lava',Properties:{level:'14'}}", "{Name:'minecraft:lava',Properties:{level:'14'}}"); -+ register(191, "{Name:'minecraft:lava',Properties:{level:'15'}}", "{Name:'minecraft:lava',Properties:{level:'15'}}"); -+ register(192, "{Name:'minecraft:sand'}", "{Name:'minecraft:sand',Properties:{variant:'sand'}}"); -+ register(193, "{Name:'minecraft:red_sand'}", "{Name:'minecraft:sand',Properties:{variant:'red_sand'}}"); -+ register(208, "{Name:'minecraft:gravel'}", "{Name:'minecraft:gravel'}"); -+ register(224, "{Name:'minecraft:gold_ore'}", "{Name:'minecraft:gold_ore'}"); -+ register(240, "{Name:'minecraft:iron_ore'}", "{Name:'minecraft:iron_ore'}"); -+ register(256, "{Name:'minecraft:coal_ore'}", "{Name:'minecraft:coal_ore'}"); -+ register(272, "{Name:'minecraft:oak_log',Properties:{axis:'y'}}", "{Name:'minecraft:log',Properties:{axis:'y',variant:'oak'}}"); -+ register(273, "{Name:'minecraft:spruce_log',Properties:{axis:'y'}}", "{Name:'minecraft:log',Properties:{axis:'y',variant:'spruce'}}"); -+ register(274, "{Name:'minecraft:birch_log',Properties:{axis:'y'}}", "{Name:'minecraft:log',Properties:{axis:'y',variant:'birch'}}"); -+ register(275, "{Name:'minecraft:jungle_log',Properties:{axis:'y'}}", "{Name:'minecraft:log',Properties:{axis:'y',variant:'jungle'}}"); -+ register(276, "{Name:'minecraft:oak_log',Properties:{axis:'x'}}", "{Name:'minecraft:log',Properties:{axis:'x',variant:'oak'}}"); -+ register(277, "{Name:'minecraft:spruce_log',Properties:{axis:'x'}}", "{Name:'minecraft:log',Properties:{axis:'x',variant:'spruce'}}"); -+ register(278, "{Name:'minecraft:birch_log',Properties:{axis:'x'}}", "{Name:'minecraft:log',Properties:{axis:'x',variant:'birch'}}"); -+ register(279, "{Name:'minecraft:jungle_log',Properties:{axis:'x'}}", "{Name:'minecraft:log',Properties:{axis:'x',variant:'jungle'}}"); -+ register(280, "{Name:'minecraft:oak_log',Properties:{axis:'z'}}", "{Name:'minecraft:log',Properties:{axis:'z',variant:'oak'}}"); -+ register(281, "{Name:'minecraft:spruce_log',Properties:{axis:'z'}}", "{Name:'minecraft:log',Properties:{axis:'z',variant:'spruce'}}"); -+ register(282, "{Name:'minecraft:birch_log',Properties:{axis:'z'}}", "{Name:'minecraft:log',Properties:{axis:'z',variant:'birch'}}"); -+ register(283, "{Name:'minecraft:jungle_log',Properties:{axis:'z'}}", "{Name:'minecraft:log',Properties:{axis:'z',variant:'jungle'}}"); -+ register(284, "{Name:'minecraft:oak_bark'}", "{Name:'minecraft:log',Properties:{axis:'none',variant:'oak'}}"); -+ register(285, "{Name:'minecraft:spruce_bark'}", "{Name:'minecraft:log',Properties:{axis:'none',variant:'spruce'}}"); -+ register(286, "{Name:'minecraft:birch_bark'}", "{Name:'minecraft:log',Properties:{axis:'none',variant:'birch'}}"); -+ register(287, "{Name:'minecraft:jungle_bark'}", "{Name:'minecraft:log',Properties:{axis:'none',variant:'jungle'}}"); -+ register(288, "{Name:'minecraft:oak_leaves',Properties:{check_decay:'false',decayable:'true'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'false',decayable:'true',variant:'oak'}}"); -+ register(289, "{Name:'minecraft:spruce_leaves',Properties:{check_decay:'false',decayable:'true'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'false',decayable:'true',variant:'spruce'}}"); -+ register(290, "{Name:'minecraft:birch_leaves',Properties:{check_decay:'false',decayable:'true'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'false',decayable:'true',variant:'birch'}}"); -+ register(291, "{Name:'minecraft:jungle_leaves',Properties:{check_decay:'false',decayable:'true'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'false',decayable:'true',variant:'jungle'}}"); -+ register(292, "{Name:'minecraft:oak_leaves',Properties:{check_decay:'false',decayable:'false'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'false',decayable:'false',variant:'oak'}}"); -+ register(293, "{Name:'minecraft:spruce_leaves',Properties:{check_decay:'false',decayable:'false'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'false',decayable:'false',variant:'spruce'}}"); -+ register(294, "{Name:'minecraft:birch_leaves',Properties:{check_decay:'false',decayable:'false'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'false',decayable:'false',variant:'birch'}}"); -+ register(295, "{Name:'minecraft:jungle_leaves',Properties:{check_decay:'false',decayable:'false'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'false',decayable:'false',variant:'jungle'}}"); -+ register(296, "{Name:'minecraft:oak_leaves',Properties:{check_decay:'true',decayable:'true'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'true',decayable:'true',variant:'oak'}}"); -+ register(297, "{Name:'minecraft:spruce_leaves',Properties:{check_decay:'true',decayable:'true'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'true',decayable:'true',variant:'spruce'}}"); -+ register(298, "{Name:'minecraft:birch_leaves',Properties:{check_decay:'true',decayable:'true'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'true',decayable:'true',variant:'birch'}}"); -+ register(299, "{Name:'minecraft:jungle_leaves',Properties:{check_decay:'true',decayable:'true'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'true',decayable:'true',variant:'jungle'}}"); -+ register(300, "{Name:'minecraft:oak_leaves',Properties:{check_decay:'true',decayable:'false'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'true',decayable:'false',variant:'oak'}}"); -+ register(301, "{Name:'minecraft:spruce_leaves',Properties:{check_decay:'true',decayable:'false'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'true',decayable:'false',variant:'spruce'}}"); -+ register(302, "{Name:'minecraft:birch_leaves',Properties:{check_decay:'true',decayable:'false'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'true',decayable:'false',variant:'birch'}}"); -+ register(303, "{Name:'minecraft:jungle_leaves',Properties:{check_decay:'true',decayable:'false'}}", "{Name:'minecraft:leaves',Properties:{check_decay:'true',decayable:'false',variant:'jungle'}}"); -+ register(304, "{Name:'minecraft:sponge'}", "{Name:'minecraft:sponge',Properties:{wet:'false'}}"); -+ register(305, "{Name:'minecraft:wet_sponge'}", "{Name:'minecraft:sponge',Properties:{wet:'true'}}"); -+ register(320, "{Name:'minecraft:glass'}", "{Name:'minecraft:glass'}"); -+ register(336, "{Name:'minecraft:lapis_ore'}", "{Name:'minecraft:lapis_ore'}"); -+ register(352, "{Name:'minecraft:lapis_block'}", "{Name:'minecraft:lapis_block'}"); -+ register(368, "{Name:'minecraft:dispenser',Properties:{facing:'down',triggered:'false'}}", "{Name:'minecraft:dispenser',Properties:{facing:'down',triggered:'false'}}"); -+ register(369, "{Name:'minecraft:dispenser',Properties:{facing:'up',triggered:'false'}}", "{Name:'minecraft:dispenser',Properties:{facing:'up',triggered:'false'}}"); -+ register(370, "{Name:'minecraft:dispenser',Properties:{facing:'north',triggered:'false'}}", "{Name:'minecraft:dispenser',Properties:{facing:'north',triggered:'false'}}"); -+ register(371, "{Name:'minecraft:dispenser',Properties:{facing:'south',triggered:'false'}}", "{Name:'minecraft:dispenser',Properties:{facing:'south',triggered:'false'}}"); -+ register(372, "{Name:'minecraft:dispenser',Properties:{facing:'west',triggered:'false'}}", "{Name:'minecraft:dispenser',Properties:{facing:'west',triggered:'false'}}"); -+ register(373, "{Name:'minecraft:dispenser',Properties:{facing:'east',triggered:'false'}}", "{Name:'minecraft:dispenser',Properties:{facing:'east',triggered:'false'}}"); -+ register(376, "{Name:'minecraft:dispenser',Properties:{facing:'down',triggered:'true'}}", "{Name:'minecraft:dispenser',Properties:{facing:'down',triggered:'true'}}"); -+ register(377, "{Name:'minecraft:dispenser',Properties:{facing:'up',triggered:'true'}}", "{Name:'minecraft:dispenser',Properties:{facing:'up',triggered:'true'}}"); -+ register(378, "{Name:'minecraft:dispenser',Properties:{facing:'north',triggered:'true'}}", "{Name:'minecraft:dispenser',Properties:{facing:'north',triggered:'true'}}"); -+ register(379, "{Name:'minecraft:dispenser',Properties:{facing:'south',triggered:'true'}}", "{Name:'minecraft:dispenser',Properties:{facing:'south',triggered:'true'}}"); -+ register(380, "{Name:'minecraft:dispenser',Properties:{facing:'west',triggered:'true'}}", "{Name:'minecraft:dispenser',Properties:{facing:'west',triggered:'true'}}"); -+ register(381, "{Name:'minecraft:dispenser',Properties:{facing:'east',triggered:'true'}}", "{Name:'minecraft:dispenser',Properties:{facing:'east',triggered:'true'}}"); -+ register(384, "{Name:'minecraft:sandstone'}", "{Name:'minecraft:sandstone',Properties:{type:'sandstone'}}"); -+ register(385, "{Name:'minecraft:chiseled_sandstone'}", "{Name:'minecraft:sandstone',Properties:{type:'chiseled_sandstone'}}"); -+ register(386, "{Name:'minecraft:cut_sandstone'}", "{Name:'minecraft:sandstone',Properties:{type:'smooth_sandstone'}}"); -+ register(400, "{Name:'minecraft:note_block'}", "{Name:'minecraft:noteblock'}"); -+ register(416, "{Name:'minecraft:red_bed',Properties:{facing:'south',occupied:'false',part:'foot'}}", "{Name:'minecraft:bed',Properties:{facing:'south',occupied:'false',part:'foot'}}", "{Name:'minecraft:bed',Properties:{facing:'south',occupied:'true',part:'foot'}}"); -+ register(417, "{Name:'minecraft:red_bed',Properties:{facing:'west',occupied:'false',part:'foot'}}", "{Name:'minecraft:bed',Properties:{facing:'west',occupied:'false',part:'foot'}}", "{Name:'minecraft:bed',Properties:{facing:'west',occupied:'true',part:'foot'}}"); -+ register(418, "{Name:'minecraft:red_bed',Properties:{facing:'north',occupied:'false',part:'foot'}}", "{Name:'minecraft:bed',Properties:{facing:'north',occupied:'false',part:'foot'}}", "{Name:'minecraft:bed',Properties:{facing:'north',occupied:'true',part:'foot'}}"); -+ register(419, "{Name:'minecraft:red_bed',Properties:{facing:'east',occupied:'false',part:'foot'}}", "{Name:'minecraft:bed',Properties:{facing:'east',occupied:'false',part:'foot'}}", "{Name:'minecraft:bed',Properties:{facing:'east',occupied:'true',part:'foot'}}"); -+ register(424, "{Name:'minecraft:red_bed',Properties:{facing:'south',occupied:'false',part:'head'}}", "{Name:'minecraft:bed',Properties:{facing:'south',occupied:'false',part:'head'}}"); -+ register(425, "{Name:'minecraft:red_bed',Properties:{facing:'west',occupied:'false',part:'head'}}", "{Name:'minecraft:bed',Properties:{facing:'west',occupied:'false',part:'head'}}"); -+ register(426, "{Name:'minecraft:red_bed',Properties:{facing:'north',occupied:'false',part:'head'}}", "{Name:'minecraft:bed',Properties:{facing:'north',occupied:'false',part:'head'}}"); -+ register(427, "{Name:'minecraft:red_bed',Properties:{facing:'east',occupied:'false',part:'head'}}", "{Name:'minecraft:bed',Properties:{facing:'east',occupied:'false',part:'head'}}"); -+ register(428, "{Name:'minecraft:red_bed',Properties:{facing:'south',occupied:'true',part:'head'}}", "{Name:'minecraft:bed',Properties:{facing:'south',occupied:'true',part:'head'}}"); -+ register(429, "{Name:'minecraft:red_bed',Properties:{facing:'west',occupied:'true',part:'head'}}", "{Name:'minecraft:bed',Properties:{facing:'west',occupied:'true',part:'head'}}"); -+ register(430, "{Name:'minecraft:red_bed',Properties:{facing:'north',occupied:'true',part:'head'}}", "{Name:'minecraft:bed',Properties:{facing:'north',occupied:'true',part:'head'}}"); -+ register(431, "{Name:'minecraft:red_bed',Properties:{facing:'east',occupied:'true',part:'head'}}", "{Name:'minecraft:bed',Properties:{facing:'east',occupied:'true',part:'head'}}"); -+ register(432, "{Name:'minecraft:powered_rail',Properties:{powered:'false',shape:'north_south'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'false',shape:'north_south'}}"); -+ register(433, "{Name:'minecraft:powered_rail',Properties:{powered:'false',shape:'east_west'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'false',shape:'east_west'}}"); -+ register(434, "{Name:'minecraft:powered_rail',Properties:{powered:'false',shape:'ascending_east'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'false',shape:'ascending_east'}}"); -+ register(435, "{Name:'minecraft:powered_rail',Properties:{powered:'false',shape:'ascending_west'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'false',shape:'ascending_west'}}"); -+ register(436, "{Name:'minecraft:powered_rail',Properties:{powered:'false',shape:'ascending_north'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'false',shape:'ascending_north'}}"); -+ register(437, "{Name:'minecraft:powered_rail',Properties:{powered:'false',shape:'ascending_south'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'false',shape:'ascending_south'}}"); -+ register(440, "{Name:'minecraft:powered_rail',Properties:{powered:'true',shape:'north_south'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'true',shape:'north_south'}}"); -+ register(441, "{Name:'minecraft:powered_rail',Properties:{powered:'true',shape:'east_west'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'true',shape:'east_west'}}"); -+ register(442, "{Name:'minecraft:powered_rail',Properties:{powered:'true',shape:'ascending_east'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'true',shape:'ascending_east'}}"); -+ register(443, "{Name:'minecraft:powered_rail',Properties:{powered:'true',shape:'ascending_west'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'true',shape:'ascending_west'}}"); -+ register(444, "{Name:'minecraft:powered_rail',Properties:{powered:'true',shape:'ascending_north'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'true',shape:'ascending_north'}}"); -+ register(445, "{Name:'minecraft:powered_rail',Properties:{powered:'true',shape:'ascending_south'}}", "{Name:'minecraft:golden_rail',Properties:{powered:'true',shape:'ascending_south'}}"); -+ register(448, "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'north_south'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'north_south'}}"); -+ register(449, "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'east_west'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'east_west'}}"); -+ register(450, "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'ascending_east'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'ascending_east'}}"); -+ register(451, "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'ascending_west'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'ascending_west'}}"); -+ register(452, "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'ascending_north'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'ascending_north'}}"); -+ register(453, "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'ascending_south'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'false',shape:'ascending_south'}}"); -+ register(456, "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'north_south'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'north_south'}}"); -+ register(457, "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'east_west'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'east_west'}}"); -+ register(458, "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'ascending_east'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'ascending_east'}}"); -+ register(459, "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'ascending_west'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'ascending_west'}}"); -+ register(460, "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'ascending_north'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'ascending_north'}}"); -+ register(461, "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'ascending_south'}}", "{Name:'minecraft:detector_rail',Properties:{powered:'true',shape:'ascending_south'}}"); -+ register(464, "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'down'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'down'}}"); -+ register(465, "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'up'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'up'}}"); -+ register(466, "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'north'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'north'}}"); -+ register(467, "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'south'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'south'}}"); -+ register(468, "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'west'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'west'}}"); -+ register(469, "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'east'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'false',facing:'east'}}"); -+ register(472, "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'down'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'down'}}"); -+ register(473, "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'up'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'up'}}"); -+ register(474, "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'north'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'north'}}"); -+ register(475, "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'south'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'south'}}"); -+ register(476, "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'west'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'west'}}"); -+ register(477, "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'east'}}", "{Name:'minecraft:sticky_piston',Properties:{extended:'true',facing:'east'}}"); -+ register(480, "{Name:'minecraft:cobweb'}", "{Name:'minecraft:web'}"); -+ register(496, "{Name:'minecraft:dead_bush'}", "{Name:'minecraft:tallgrass',Properties:{type:'dead_bush'}}"); -+ register(497, "{Name:'minecraft:grass'}", "{Name:'minecraft:tallgrass',Properties:{type:'tall_grass'}}"); -+ register(498, "{Name:'minecraft:fern'}", "{Name:'minecraft:tallgrass',Properties:{type:'fern'}}"); -+ register(512, "{Name:'minecraft:dead_bush'}", "{Name:'minecraft:deadbush'}"); -+ register(528, "{Name:'minecraft:piston',Properties:{extended:'false',facing:'down'}}", "{Name:'minecraft:piston',Properties:{extended:'false',facing:'down'}}"); -+ register(529, "{Name:'minecraft:piston',Properties:{extended:'false',facing:'up'}}", "{Name:'minecraft:piston',Properties:{extended:'false',facing:'up'}}"); -+ register(530, "{Name:'minecraft:piston',Properties:{extended:'false',facing:'north'}}", "{Name:'minecraft:piston',Properties:{extended:'false',facing:'north'}}"); -+ register(531, "{Name:'minecraft:piston',Properties:{extended:'false',facing:'south'}}", "{Name:'minecraft:piston',Properties:{extended:'false',facing:'south'}}"); -+ register(532, "{Name:'minecraft:piston',Properties:{extended:'false',facing:'west'}}", "{Name:'minecraft:piston',Properties:{extended:'false',facing:'west'}}"); -+ register(533, "{Name:'minecraft:piston',Properties:{extended:'false',facing:'east'}}", "{Name:'minecraft:piston',Properties:{extended:'false',facing:'east'}}"); -+ register(536, "{Name:'minecraft:piston',Properties:{extended:'true',facing:'down'}}", "{Name:'minecraft:piston',Properties:{extended:'true',facing:'down'}}"); -+ register(537, "{Name:'minecraft:piston',Properties:{extended:'true',facing:'up'}}", "{Name:'minecraft:piston',Properties:{extended:'true',facing:'up'}}"); -+ register(538, "{Name:'minecraft:piston',Properties:{extended:'true',facing:'north'}}", "{Name:'minecraft:piston',Properties:{extended:'true',facing:'north'}}"); -+ register(539, "{Name:'minecraft:piston',Properties:{extended:'true',facing:'south'}}", "{Name:'minecraft:piston',Properties:{extended:'true',facing:'south'}}"); -+ register(540, "{Name:'minecraft:piston',Properties:{extended:'true',facing:'west'}}", "{Name:'minecraft:piston',Properties:{extended:'true',facing:'west'}}"); -+ register(541, "{Name:'minecraft:piston',Properties:{extended:'true',facing:'east'}}", "{Name:'minecraft:piston',Properties:{extended:'true',facing:'east'}}"); -+ register(544, "{Name:'minecraft:piston_head',Properties:{facing:'down',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'down',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'down',short:'true',type:'normal'}}"); -+ register(545, "{Name:'minecraft:piston_head',Properties:{facing:'up',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'up',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'up',short:'true',type:'normal'}}"); -+ register(546, "{Name:'minecraft:piston_head',Properties:{facing:'north',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'north',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'north',short:'true',type:'normal'}}"); -+ register(547, "{Name:'minecraft:piston_head',Properties:{facing:'south',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'south',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'south',short:'true',type:'normal'}}"); -+ register(548, "{Name:'minecraft:piston_head',Properties:{facing:'west',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'west',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'west',short:'true',type:'normal'}}"); -+ register(549, "{Name:'minecraft:piston_head',Properties:{facing:'east',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'east',short:'false',type:'normal'}}", "{Name:'minecraft:piston_head',Properties:{facing:'east',short:'true',type:'normal'}}"); -+ register(552, "{Name:'minecraft:piston_head',Properties:{facing:'down',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'down',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'down',short:'true',type:'sticky'}}"); -+ register(553, "{Name:'minecraft:piston_head',Properties:{facing:'up',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'up',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'up',short:'true',type:'sticky'}}"); -+ register(554, "{Name:'minecraft:piston_head',Properties:{facing:'north',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'north',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'north',short:'true',type:'sticky'}}"); -+ register(555, "{Name:'minecraft:piston_head',Properties:{facing:'south',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'south',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'south',short:'true',type:'sticky'}}"); -+ register(556, "{Name:'minecraft:piston_head',Properties:{facing:'west',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'west',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'west',short:'true',type:'sticky'}}"); -+ register(557, "{Name:'minecraft:piston_head',Properties:{facing:'east',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'east',short:'false',type:'sticky'}}", "{Name:'minecraft:piston_head',Properties:{facing:'east',short:'true',type:'sticky'}}"); -+ register(560, "{Name:'minecraft:white_wool'}", "{Name:'minecraft:wool',Properties:{color:'white'}}"); -+ register(561, "{Name:'minecraft:orange_wool'}", "{Name:'minecraft:wool',Properties:{color:'orange'}}"); -+ register(562, "{Name:'minecraft:magenta_wool'}", "{Name:'minecraft:wool',Properties:{color:'magenta'}}"); -+ register(563, "{Name:'minecraft:light_blue_wool'}", "{Name:'minecraft:wool',Properties:{color:'light_blue'}}"); -+ register(564, "{Name:'minecraft:yellow_wool'}", "{Name:'minecraft:wool',Properties:{color:'yellow'}}"); -+ register(565, "{Name:'minecraft:lime_wool'}", "{Name:'minecraft:wool',Properties:{color:'lime'}}"); -+ register(566, "{Name:'minecraft:pink_wool'}", "{Name:'minecraft:wool',Properties:{color:'pink'}}"); -+ register(567, "{Name:'minecraft:gray_wool'}", "{Name:'minecraft:wool',Properties:{color:'gray'}}"); -+ register(568, "{Name:'minecraft:light_gray_wool'}", "{Name:'minecraft:wool',Properties:{color:'silver'}}"); -+ register(569, "{Name:'minecraft:cyan_wool'}", "{Name:'minecraft:wool',Properties:{color:'cyan'}}"); -+ register(570, "{Name:'minecraft:purple_wool'}", "{Name:'minecraft:wool',Properties:{color:'purple'}}"); -+ register(571, "{Name:'minecraft:blue_wool'}", "{Name:'minecraft:wool',Properties:{color:'blue'}}"); -+ register(572, "{Name:'minecraft:brown_wool'}", "{Name:'minecraft:wool',Properties:{color:'brown'}}"); -+ register(573, "{Name:'minecraft:green_wool'}", "{Name:'minecraft:wool',Properties:{color:'green'}}"); -+ register(574, "{Name:'minecraft:red_wool'}", "{Name:'minecraft:wool',Properties:{color:'red'}}"); -+ register(575, "{Name:'minecraft:black_wool'}", "{Name:'minecraft:wool',Properties:{color:'black'}}"); -+ register(576, "{Name:'minecraft:moving_piston',Properties:{facing:'down',type:'normal'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'down',type:'normal'}}"); -+ register(577, "{Name:'minecraft:moving_piston',Properties:{facing:'up',type:'normal'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'up',type:'normal'}}"); -+ register(578, "{Name:'minecraft:moving_piston',Properties:{facing:'north',type:'normal'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'north',type:'normal'}}"); -+ register(579, "{Name:'minecraft:moving_piston',Properties:{facing:'south',type:'normal'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'south',type:'normal'}}"); -+ register(580, "{Name:'minecraft:moving_piston',Properties:{facing:'west',type:'normal'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'west',type:'normal'}}"); -+ register(581, "{Name:'minecraft:moving_piston',Properties:{facing:'east',type:'normal'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'east',type:'normal'}}"); -+ register(584, "{Name:'minecraft:moving_piston',Properties:{facing:'down',type:'sticky'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'down',type:'sticky'}}"); -+ register(585, "{Name:'minecraft:moving_piston',Properties:{facing:'up',type:'sticky'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'up',type:'sticky'}}"); -+ register(586, "{Name:'minecraft:moving_piston',Properties:{facing:'north',type:'sticky'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'north',type:'sticky'}}"); -+ register(587, "{Name:'minecraft:moving_piston',Properties:{facing:'south',type:'sticky'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'south',type:'sticky'}}"); -+ register(588, "{Name:'minecraft:moving_piston',Properties:{facing:'west',type:'sticky'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'west',type:'sticky'}}"); -+ register(589, "{Name:'minecraft:moving_piston',Properties:{facing:'east',type:'sticky'}}", "{Name:'minecraft:piston_extension',Properties:{facing:'east',type:'sticky'}}"); -+ register(592, "{Name:'minecraft:dandelion'}", "{Name:'minecraft:yellow_flower',Properties:{type:'dandelion'}}"); -+ register(608, "{Name:'minecraft:poppy'}", "{Name:'minecraft:red_flower',Properties:{type:'poppy'}}"); -+ register(609, "{Name:'minecraft:blue_orchid'}", "{Name:'minecraft:red_flower',Properties:{type:'blue_orchid'}}"); -+ register(610, "{Name:'minecraft:allium'}", "{Name:'minecraft:red_flower',Properties:{type:'allium'}}"); -+ register(611, "{Name:'minecraft:azure_bluet'}", "{Name:'minecraft:red_flower',Properties:{type:'houstonia'}}"); -+ register(612, "{Name:'minecraft:red_tulip'}", "{Name:'minecraft:red_flower',Properties:{type:'red_tulip'}}"); -+ register(613, "{Name:'minecraft:orange_tulip'}", "{Name:'minecraft:red_flower',Properties:{type:'orange_tulip'}}"); -+ register(614, "{Name:'minecraft:white_tulip'}", "{Name:'minecraft:red_flower',Properties:{type:'white_tulip'}}"); -+ register(615, "{Name:'minecraft:pink_tulip'}", "{Name:'minecraft:red_flower',Properties:{type:'pink_tulip'}}"); -+ register(616, "{Name:'minecraft:oxeye_daisy'}", "{Name:'minecraft:red_flower',Properties:{type:'oxeye_daisy'}}"); -+ register(624, "{Name:'minecraft:brown_mushroom'}", "{Name:'minecraft:brown_mushroom'}"); -+ register(640, "{Name:'minecraft:red_mushroom'}", "{Name:'minecraft:red_mushroom'}"); -+ register(656, "{Name:'minecraft:gold_block'}", "{Name:'minecraft:gold_block'}"); -+ register(672, "{Name:'minecraft:iron_block'}", "{Name:'minecraft:iron_block'}"); -+ register(688, "{Name:'minecraft:stone_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'false',variant:'stone'}}"); -+ register(689, "{Name:'minecraft:sandstone_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'false',variant:'sandstone'}}"); -+ register(690, "{Name:'minecraft:petrified_oak_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'false',variant:'wood_old'}}"); -+ register(691, "{Name:'minecraft:cobblestone_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'false',variant:'cobblestone'}}"); -+ register(692, "{Name:'minecraft:brick_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'false',variant:'brick'}}"); -+ register(693, "{Name:'minecraft:stone_brick_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'false',variant:'stone_brick'}}"); -+ register(694, "{Name:'minecraft:nether_brick_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'false',variant:'nether_brick'}}"); -+ register(695, "{Name:'minecraft:quartz_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'false',variant:'quartz'}}"); -+ register(696, "{Name:'minecraft:smooth_stone'}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'true',variant:'stone'}}"); -+ register(697, "{Name:'minecraft:smooth_sandstone'}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'true',variant:'sandstone'}}"); -+ register(698, "{Name:'minecraft:petrified_oak_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'true',variant:'wood_old'}}"); -+ register(699, "{Name:'minecraft:cobblestone_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'true',variant:'cobblestone'}}"); -+ register(700, "{Name:'minecraft:brick_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'true',variant:'brick'}}"); -+ register(701, "{Name:'minecraft:stone_brick_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'true',variant:'stone_brick'}}"); -+ register(702, "{Name:'minecraft:nether_brick_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'true',variant:'nether_brick'}}"); -+ register(703, "{Name:'minecraft:smooth_quartz'}", "{Name:'minecraft:double_stone_slab',Properties:{seamless:'true',variant:'quartz'}}"); -+ register(704, "{Name:'minecraft:stone_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:stone_slab',Properties:{half:'bottom',variant:'stone'}}"); -+ register(705, "{Name:'minecraft:sandstone_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:stone_slab',Properties:{half:'bottom',variant:'sandstone'}}"); -+ register(706, "{Name:'minecraft:petrified_oak_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:stone_slab',Properties:{half:'bottom',variant:'wood_old'}}"); -+ register(707, "{Name:'minecraft:cobblestone_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:stone_slab',Properties:{half:'bottom',variant:'cobblestone'}}"); -+ register(708, "{Name:'minecraft:brick_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:stone_slab',Properties:{half:'bottom',variant:'brick'}}"); -+ register(709, "{Name:'minecraft:stone_brick_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:stone_slab',Properties:{half:'bottom',variant:'stone_brick'}}"); -+ register(710, "{Name:'minecraft:nether_brick_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:stone_slab',Properties:{half:'bottom',variant:'nether_brick'}}"); -+ register(711, "{Name:'minecraft:quartz_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:stone_slab',Properties:{half:'bottom',variant:'quartz'}}"); -+ register(712, "{Name:'minecraft:stone_slab',Properties:{type:'top'}}", "{Name:'minecraft:stone_slab',Properties:{half:'top',variant:'stone'}}"); -+ register(713, "{Name:'minecraft:sandstone_slab',Properties:{type:'top'}}", "{Name:'minecraft:stone_slab',Properties:{half:'top',variant:'sandstone'}}"); -+ register(714, "{Name:'minecraft:petrified_oak_slab',Properties:{type:'top'}}", "{Name:'minecraft:stone_slab',Properties:{half:'top',variant:'wood_old'}}"); -+ register(715, "{Name:'minecraft:cobblestone_slab',Properties:{type:'top'}}", "{Name:'minecraft:stone_slab',Properties:{half:'top',variant:'cobblestone'}}"); -+ register(716, "{Name:'minecraft:brick_slab',Properties:{type:'top'}}", "{Name:'minecraft:stone_slab',Properties:{half:'top',variant:'brick'}}"); -+ register(717, "{Name:'minecraft:stone_brick_slab',Properties:{type:'top'}}", "{Name:'minecraft:stone_slab',Properties:{half:'top',variant:'stone_brick'}}"); -+ register(718, "{Name:'minecraft:nether_brick_slab',Properties:{type:'top'}}", "{Name:'minecraft:stone_slab',Properties:{half:'top',variant:'nether_brick'}}"); -+ register(719, "{Name:'minecraft:quartz_slab',Properties:{type:'top'}}", "{Name:'minecraft:stone_slab',Properties:{half:'top',variant:'quartz'}}"); -+ register(720, "{Name:'minecraft:bricks'}", "{Name:'minecraft:brick_block'}"); -+ register(736, "{Name:'minecraft:tnt',Properties:{unstable:'false'}}", "{Name:'minecraft:tnt',Properties:{explode:'false'}}"); -+ register(737, "{Name:'minecraft:tnt',Properties:{unstable:'true'}}", "{Name:'minecraft:tnt',Properties:{explode:'true'}}"); -+ register(752, "{Name:'minecraft:bookshelf'}", "{Name:'minecraft:bookshelf'}"); -+ register(768, "{Name:'minecraft:mossy_cobblestone'}", "{Name:'minecraft:mossy_cobblestone'}"); -+ register(784, "{Name:'minecraft:obsidian'}", "{Name:'minecraft:obsidian'}"); -+ register(801, "{Name:'minecraft:wall_torch',Properties:{facing:'east'}}", "{Name:'minecraft:torch',Properties:{facing:'east'}}"); -+ register(802, "{Name:'minecraft:wall_torch',Properties:{facing:'west'}}", "{Name:'minecraft:torch',Properties:{facing:'west'}}"); -+ register(803, "{Name:'minecraft:wall_torch',Properties:{facing:'south'}}", "{Name:'minecraft:torch',Properties:{facing:'south'}}"); -+ register(804, "{Name:'minecraft:wall_torch',Properties:{facing:'north'}}", "{Name:'minecraft:torch',Properties:{facing:'north'}}"); -+ register(805, "{Name:'minecraft:torch'}", "{Name:'minecraft:torch',Properties:{facing:'up'}}"); -+ register(816, "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'0',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); -+ register(817, "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'1',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); -+ register(818, "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'2',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); -+ register(819, "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'3',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); -+ register(820, "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'4',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); -+ register(821, "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'5',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); -+ register(822, "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'6',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); -+ register(823, "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'7',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); -+ register(824, "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'8',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); -+ register(825, "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'9',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); -+ register(826, "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'10',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); -+ register(827, "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'11',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); -+ register(828, "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'12',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); -+ register(829, "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'13',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); -+ register(830, "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'14',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); -+ register(831, "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:fire',Properties:{age:'15',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); -+ register(832, "{Name:'minecraft:mob_spawner'}", "{Name:'minecraft:mob_spawner'}"); -+ register(848, "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); -+ register(849, "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); -+ register(850, "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); -+ register(851, "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); -+ register(852, "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); -+ register(853, "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); -+ register(854, "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); -+ register(855, "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:oak_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); -+ register(866, "{Name:'minecraft:chest',Properties:{facing:'north',type:'single'}}", "{Name:'minecraft:chest',Properties:{facing:'north'}}"); -+ register(867, "{Name:'minecraft:chest',Properties:{facing:'south',type:'single'}}", "{Name:'minecraft:chest',Properties:{facing:'south'}}"); -+ register(868, "{Name:'minecraft:chest',Properties:{facing:'west',type:'single'}}", "{Name:'minecraft:chest',Properties:{facing:'west'}}"); -+ register(869, "{Name:'minecraft:chest',Properties:{facing:'east',type:'single'}}", "{Name:'minecraft:chest',Properties:{facing:'east'}}"); -+ register(880, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'0',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'0',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'0',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'0',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'0',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'0',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'0',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'0',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'0',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'0',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'0',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'0',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'0',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'0',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'0',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'0',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'0',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'0',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'0',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'0',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'0',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'0',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'0',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'0',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'0',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'0',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'0',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'0',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'0',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'0',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'0',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'0',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'0',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'0',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'0',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'0',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'0',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'0',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'0',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'0',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'0',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'0',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'0',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'0',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'0',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'0',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'0',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'0',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'0',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'0',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'0',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'0',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'0',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'0',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'0',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'0',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'0',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'0',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'0',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'0',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'0',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'0',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'0',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'0',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'0',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'0',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'0',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'0',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'0',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'0',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'0',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'0',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'0',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'0',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'0',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'0',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'0',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'0',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'0',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'0',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'0',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'0',south:'up',west:'up'}}"); -+ register(881, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'1',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'1',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'1',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'1',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'1',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'1',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'1',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'1',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'1',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'1',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'1',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'1',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'1',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'1',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'1',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'1',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'1',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'1',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'1',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'1',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'1',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'1',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'1',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'1',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'1',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'1',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'1',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'1',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'1',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'1',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'1',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'1',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'1',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'1',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'1',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'1',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'1',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'1',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'1',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'1',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'1',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'1',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'1',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'1',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'1',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'1',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'1',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'1',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'1',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'1',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'1',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'1',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'1',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'1',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'1',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'1',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'1',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'1',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'1',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'1',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'1',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'1',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'1',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'1',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'1',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'1',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'1',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'1',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'1',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'1',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'1',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'1',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'1',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'1',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'1',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'1',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'1',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'1',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'1',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'1',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'1',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'1',south:'up',west:'up'}}"); -+ register(882, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'2',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'2',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'2',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'2',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'2',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'2',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'2',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'2',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'2',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'2',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'2',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'2',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'2',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'2',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'2',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'2',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'2',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'2',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'2',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'2',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'2',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'2',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'2',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'2',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'2',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'2',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'2',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'2',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'2',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'2',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'2',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'2',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'2',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'2',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'2',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'2',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'2',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'2',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'2',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'2',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'2',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'2',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'2',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'2',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'2',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'2',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'2',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'2',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'2',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'2',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'2',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'2',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'2',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'2',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'2',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'2',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'2',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'2',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'2',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'2',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'2',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'2',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'2',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'2',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'2',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'2',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'2',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'2',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'2',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'2',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'2',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'2',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'2',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'2',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'2',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'2',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'2',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'2',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'2',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'2',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'2',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'2',south:'up',west:'up'}}"); -+ register(883, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'3',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'3',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'3',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'3',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'3',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'3',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'3',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'3',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'3',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'3',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'3',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'3',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'3',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'3',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'3',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'3',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'3',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'3',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'3',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'3',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'3',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'3',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'3',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'3',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'3',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'3',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'3',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'3',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'3',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'3',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'3',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'3',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'3',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'3',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'3',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'3',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'3',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'3',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'3',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'3',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'3',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'3',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'3',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'3',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'3',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'3',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'3',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'3',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'3',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'3',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'3',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'3',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'3',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'3',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'3',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'3',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'3',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'3',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'3',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'3',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'3',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'3',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'3',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'3',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'3',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'3',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'3',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'3',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'3',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'3',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'3',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'3',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'3',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'3',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'3',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'3',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'3',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'3',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'3',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'3',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'3',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'3',south:'up',west:'up'}}"); -+ register(884, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'4',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'4',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'4',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'4',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'4',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'4',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'4',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'4',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'4',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'4',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'4',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'4',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'4',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'4',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'4',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'4',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'4',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'4',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'4',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'4',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'4',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'4',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'4',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'4',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'4',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'4',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'4',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'4',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'4',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'4',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'4',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'4',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'4',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'4',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'4',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'4',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'4',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'4',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'4',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'4',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'4',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'4',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'4',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'4',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'4',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'4',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'4',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'4',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'4',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'4',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'4',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'4',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'4',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'4',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'4',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'4',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'4',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'4',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'4',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'4',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'4',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'4',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'4',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'4',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'4',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'4',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'4',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'4',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'4',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'4',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'4',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'4',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'4',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'4',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'4',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'4',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'4',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'4',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'4',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'4',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'4',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'4',south:'up',west:'up'}}"); -+ register(885, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'5',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'5',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'5',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'5',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'5',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'5',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'5',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'5',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'5',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'5',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'5',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'5',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'5',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'5',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'5',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'5',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'5',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'5',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'5',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'5',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'5',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'5',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'5',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'5',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'5',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'5',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'5',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'5',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'5',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'5',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'5',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'5',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'5',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'5',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'5',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'5',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'5',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'5',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'5',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'5',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'5',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'5',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'5',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'5',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'5',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'5',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'5',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'5',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'5',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'5',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'5',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'5',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'5',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'5',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'5',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'5',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'5',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'5',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'5',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'5',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'5',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'5',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'5',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'5',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'5',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'5',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'5',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'5',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'5',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'5',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'5',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'5',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'5',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'5',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'5',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'5',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'5',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'5',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'5',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'5',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'5',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'5',south:'up',west:'up'}}"); -+ register(886, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'6',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'6',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'6',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'6',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'6',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'6',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'6',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'6',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'6',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'6',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'6',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'6',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'6',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'6',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'6',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'6',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'6',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'6',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'6',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'6',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'6',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'6',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'6',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'6',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'6',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'6',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'6',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'6',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'6',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'6',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'6',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'6',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'6',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'6',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'6',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'6',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'6',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'6',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'6',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'6',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'6',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'6',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'6',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'6',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'6',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'6',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'6',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'6',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'6',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'6',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'6',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'6',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'6',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'6',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'6',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'6',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'6',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'6',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'6',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'6',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'6',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'6',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'6',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'6',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'6',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'6',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'6',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'6',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'6',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'6',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'6',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'6',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'6',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'6',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'6',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'6',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'6',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'6',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'6',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'6',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'6',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'6',south:'up',west:'up'}}"); -+ register(887, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'7',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'7',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'7',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'7',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'7',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'7',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'7',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'7',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'7',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'7',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'7',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'7',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'7',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'7',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'7',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'7',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'7',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'7',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'7',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'7',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'7',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'7',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'7',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'7',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'7',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'7',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'7',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'7',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'7',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'7',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'7',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'7',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'7',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'7',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'7',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'7',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'7',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'7',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'7',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'7',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'7',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'7',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'7',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'7',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'7',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'7',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'7',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'7',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'7',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'7',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'7',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'7',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'7',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'7',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'7',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'7',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'7',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'7',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'7',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'7',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'7',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'7',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'7',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'7',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'7',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'7',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'7',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'7',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'7',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'7',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'7',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'7',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'7',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'7',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'7',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'7',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'7',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'7',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'7',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'7',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'7',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'7',south:'up',west:'up'}}"); -+ register(888, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'8',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'8',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'8',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'8',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'8',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'8',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'8',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'8',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'8',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'8',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'8',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'8',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'8',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'8',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'8',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'8',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'8',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'8',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'8',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'8',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'8',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'8',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'8',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'8',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'8',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'8',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'8',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'8',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'8',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'8',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'8',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'8',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'8',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'8',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'8',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'8',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'8',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'8',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'8',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'8',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'8',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'8',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'8',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'8',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'8',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'8',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'8',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'8',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'8',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'8',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'8',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'8',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'8',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'8',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'8',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'8',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'8',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'8',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'8',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'8',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'8',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'8',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'8',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'8',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'8',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'8',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'8',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'8',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'8',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'8',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'8',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'8',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'8',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'8',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'8',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'8',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'8',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'8',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'8',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'8',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'8',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'8',south:'up',west:'up'}}"); -+ register(889, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'9',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'9',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'9',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'9',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'9',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'9',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'9',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'9',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'9',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'9',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'9',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'9',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'9',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'9',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'9',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'9',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'9',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'9',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'9',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'9',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'9',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'9',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'9',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'9',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'9',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'9',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'9',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'9',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'9',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'9',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'9',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'9',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'9',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'9',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'9',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'9',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'9',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'9',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'9',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'9',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'9',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'9',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'9',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'9',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'9',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'9',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'9',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'9',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'9',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'9',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'9',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'9',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'9',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'9',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'9',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'9',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'9',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'9',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'9',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'9',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'9',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'9',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'9',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'9',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'9',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'9',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'9',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'9',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'9',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'9',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'9',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'9',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'9',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'9',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'9',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'9',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'9',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'9',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'9',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'9',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'9',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'9',south:'up',west:'up'}}"); -+ register(890, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'10',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'10',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'10',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'10',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'10',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'10',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'10',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'10',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'10',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'10',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'10',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'10',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'10',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'10',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'10',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'10',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'10',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'10',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'10',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'10',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'10',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'10',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'10',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'10',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'10',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'10',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'10',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'10',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'10',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'10',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'10',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'10',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'10',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'10',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'10',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'10',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'10',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'10',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'10',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'10',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'10',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'10',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'10',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'10',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'10',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'10',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'10',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'10',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'10',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'10',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'10',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'10',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'10',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'10',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'10',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'10',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'10',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'10',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'10',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'10',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'10',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'10',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'10',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'10',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'10',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'10',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'10',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'10',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'10',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'10',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'10',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'10',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'10',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'10',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'10',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'10',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'10',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'10',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'10',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'10',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'10',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'10',south:'up',west:'up'}}"); -+ register(891, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'11',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'11',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'11',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'11',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'11',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'11',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'11',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'11',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'11',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'11',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'11',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'11',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'11',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'11',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'11',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'11',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'11',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'11',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'11',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'11',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'11',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'11',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'11',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'11',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'11',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'11',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'11',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'11',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'11',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'11',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'11',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'11',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'11',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'11',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'11',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'11',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'11',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'11',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'11',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'11',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'11',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'11',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'11',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'11',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'11',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'11',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'11',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'11',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'11',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'11',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'11',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'11',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'11',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'11',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'11',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'11',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'11',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'11',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'11',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'11',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'11',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'11',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'11',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'11',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'11',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'11',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'11',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'11',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'11',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'11',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'11',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'11',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'11',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'11',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'11',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'11',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'11',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'11',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'11',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'11',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'11',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'11',south:'up',west:'up'}}"); -+ register(892, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'12',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'12',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'12',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'12',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'12',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'12',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'12',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'12',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'12',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'12',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'12',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'12',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'12',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'12',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'12',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'12',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'12',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'12',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'12',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'12',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'12',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'12',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'12',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'12',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'12',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'12',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'12',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'12',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'12',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'12',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'12',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'12',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'12',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'12',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'12',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'12',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'12',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'12',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'12',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'12',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'12',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'12',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'12',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'12',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'12',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'12',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'12',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'12',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'12',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'12',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'12',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'12',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'12',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'12',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'12',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'12',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'12',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'12',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'12',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'12',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'12',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'12',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'12',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'12',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'12',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'12',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'12',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'12',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'12',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'12',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'12',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'12',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'12',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'12',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'12',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'12',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'12',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'12',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'12',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'12',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'12',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'12',south:'up',west:'up'}}"); -+ register(893, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'13',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'13',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'13',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'13',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'13',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'13',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'13',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'13',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'13',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'13',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'13',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'13',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'13',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'13',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'13',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'13',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'13',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'13',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'13',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'13',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'13',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'13',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'13',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'13',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'13',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'13',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'13',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'13',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'13',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'13',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'13',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'13',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'13',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'13',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'13',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'13',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'13',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'13',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'13',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'13',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'13',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'13',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'13',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'13',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'13',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'13',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'13',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'13',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'13',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'13',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'13',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'13',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'13',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'13',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'13',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'13',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'13',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'13',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'13',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'13',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'13',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'13',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'13',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'13',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'13',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'13',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'13',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'13',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'13',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'13',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'13',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'13',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'13',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'13',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'13',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'13',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'13',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'13',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'13',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'13',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'13',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'13',south:'up',west:'up'}}"); -+ register(894, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'14',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'14',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'14',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'14',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'14',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'14',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'14',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'14',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'14',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'14',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'14',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'14',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'14',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'14',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'14',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'14',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'14',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'14',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'14',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'14',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'14',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'14',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'14',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'14',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'14',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'14',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'14',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'14',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'14',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'14',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'14',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'14',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'14',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'14',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'14',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'14',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'14',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'14',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'14',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'14',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'14',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'14',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'14',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'14',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'14',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'14',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'14',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'14',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'14',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'14',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'14',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'14',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'14',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'14',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'14',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'14',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'14',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'14',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'14',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'14',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'14',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'14',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'14',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'14',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'14',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'14',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'14',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'14',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'14',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'14',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'14',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'14',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'14',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'14',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'14',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'14',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'14',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'14',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'14',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'14',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'14',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'14',south:'up',west:'up'}}"); -+ register(895, "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'15',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'15',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'15',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'15',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'15',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'15',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'15',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'15',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'15',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'none',power:'15',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'15',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'15',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'15',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'15',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'15',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'15',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'15',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'15',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'side',power:'15',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'15',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'15',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'15',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'15',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'15',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'15',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'15',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'15',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'none',north:'up',power:'15',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'15',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'15',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'15',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'15',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'15',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'15',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'15',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'15',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'none',power:'15',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'15',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'15',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'15',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'15',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'15',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'15',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'15',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'15',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'side',power:'15',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'15',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'15',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'15',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'15',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'15',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'15',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'15',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'15',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'side',north:'up',power:'15',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'15',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'15',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'15',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'15',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'15',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'15',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'15',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'15',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'none',power:'15',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'15',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'15',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'15',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'15',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'15',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'15',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'15',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'15',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'side',power:'15',south:'up',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'15',south:'none',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'15',south:'none',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'15',south:'none',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'15',south:'side',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'15',south:'side',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'15',south:'side',west:'up'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'15',south:'up',west:'none'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'15',south:'up',west:'side'}}", "{Name:'minecraft:redstone_wire',Properties:{east:'up',north:'up',power:'15',south:'up',west:'up'}}"); -+ register(896, "{Name:'minecraft:diamond_ore'}", "{Name:'minecraft:diamond_ore'}"); -+ register(912, "{Name:'minecraft:diamond_block'}", "{Name:'minecraft:diamond_block'}"); -+ register(928, "{Name:'minecraft:crafting_table'}", "{Name:'minecraft:crafting_table'}"); -+ register(944, "{Name:'minecraft:wheat',Properties:{age:'0'}}", "{Name:'minecraft:wheat',Properties:{age:'0'}}"); -+ register(945, "{Name:'minecraft:wheat',Properties:{age:'1'}}", "{Name:'minecraft:wheat',Properties:{age:'1'}}"); -+ register(946, "{Name:'minecraft:wheat',Properties:{age:'2'}}", "{Name:'minecraft:wheat',Properties:{age:'2'}}"); -+ register(947, "{Name:'minecraft:wheat',Properties:{age:'3'}}", "{Name:'minecraft:wheat',Properties:{age:'3'}}"); -+ register(948, "{Name:'minecraft:wheat',Properties:{age:'4'}}", "{Name:'minecraft:wheat',Properties:{age:'4'}}"); -+ register(949, "{Name:'minecraft:wheat',Properties:{age:'5'}}", "{Name:'minecraft:wheat',Properties:{age:'5'}}"); -+ register(950, "{Name:'minecraft:wheat',Properties:{age:'6'}}", "{Name:'minecraft:wheat',Properties:{age:'6'}}"); -+ register(951, "{Name:'minecraft:wheat',Properties:{age:'7'}}", "{Name:'minecraft:wheat',Properties:{age:'7'}}"); -+ register(960, "{Name:'minecraft:farmland',Properties:{moisture:'0'}}", "{Name:'minecraft:farmland',Properties:{moisture:'0'}}"); -+ register(961, "{Name:'minecraft:farmland',Properties:{moisture:'1'}}", "{Name:'minecraft:farmland',Properties:{moisture:'1'}}"); -+ register(962, "{Name:'minecraft:farmland',Properties:{moisture:'2'}}", "{Name:'minecraft:farmland',Properties:{moisture:'2'}}"); -+ register(963, "{Name:'minecraft:farmland',Properties:{moisture:'3'}}", "{Name:'minecraft:farmland',Properties:{moisture:'3'}}"); -+ register(964, "{Name:'minecraft:farmland',Properties:{moisture:'4'}}", "{Name:'minecraft:farmland',Properties:{moisture:'4'}}"); -+ register(965, "{Name:'minecraft:farmland',Properties:{moisture:'5'}}", "{Name:'minecraft:farmland',Properties:{moisture:'5'}}"); -+ register(966, "{Name:'minecraft:farmland',Properties:{moisture:'6'}}", "{Name:'minecraft:farmland',Properties:{moisture:'6'}}"); -+ register(967, "{Name:'minecraft:farmland',Properties:{moisture:'7'}}", "{Name:'minecraft:farmland',Properties:{moisture:'7'}}"); -+ register(978, "{Name:'minecraft:furnace',Properties:{facing:'north',lit:'false'}}", "{Name:'minecraft:furnace',Properties:{facing:'north'}}"); -+ register(979, "{Name:'minecraft:furnace',Properties:{facing:'south',lit:'false'}}", "{Name:'minecraft:furnace',Properties:{facing:'south'}}"); -+ register(980, "{Name:'minecraft:furnace',Properties:{facing:'west',lit:'false'}}", "{Name:'minecraft:furnace',Properties:{facing:'west'}}"); -+ register(981, "{Name:'minecraft:furnace',Properties:{facing:'east',lit:'false'}}", "{Name:'minecraft:furnace',Properties:{facing:'east'}}"); -+ register(994, "{Name:'minecraft:furnace',Properties:{facing:'north',lit:'true'}}", "{Name:'minecraft:lit_furnace',Properties:{facing:'north'}}"); -+ register(995, "{Name:'minecraft:furnace',Properties:{facing:'south',lit:'true'}}", "{Name:'minecraft:lit_furnace',Properties:{facing:'south'}}"); -+ register(996, "{Name:'minecraft:furnace',Properties:{facing:'west',lit:'true'}}", "{Name:'minecraft:lit_furnace',Properties:{facing:'west'}}"); -+ register(997, "{Name:'minecraft:furnace',Properties:{facing:'east',lit:'true'}}", "{Name:'minecraft:lit_furnace',Properties:{facing:'east'}}"); -+ register(1008, "{Name:'minecraft:sign',Properties:{rotation:'0'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'0'}}"); -+ register(1009, "{Name:'minecraft:sign',Properties:{rotation:'1'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'1'}}"); -+ register(1010, "{Name:'minecraft:sign',Properties:{rotation:'2'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'2'}}"); -+ register(1011, "{Name:'minecraft:sign',Properties:{rotation:'3'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'3'}}"); -+ register(1012, "{Name:'minecraft:sign',Properties:{rotation:'4'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'4'}}"); -+ register(1013, "{Name:'minecraft:sign',Properties:{rotation:'5'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'5'}}"); -+ register(1014, "{Name:'minecraft:sign',Properties:{rotation:'6'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'6'}}"); -+ register(1015, "{Name:'minecraft:sign',Properties:{rotation:'7'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'7'}}"); -+ register(1016, "{Name:'minecraft:sign',Properties:{rotation:'8'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'8'}}"); -+ register(1017, "{Name:'minecraft:sign',Properties:{rotation:'9'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'9'}}"); -+ register(1018, "{Name:'minecraft:sign',Properties:{rotation:'10'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'10'}}"); -+ register(1019, "{Name:'minecraft:sign',Properties:{rotation:'11'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'11'}}"); -+ register(1020, "{Name:'minecraft:sign',Properties:{rotation:'12'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'12'}}"); -+ register(1021, "{Name:'minecraft:sign',Properties:{rotation:'13'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'13'}}"); -+ register(1022, "{Name:'minecraft:sign',Properties:{rotation:'14'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'14'}}"); -+ register(1023, "{Name:'minecraft:sign',Properties:{rotation:'15'}}", "{Name:'minecraft:standing_sign',Properties:{rotation:'15'}}"); -+ register(1024, "{Name:'minecraft:oak_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(1025, "{Name:'minecraft:oak_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(1026, "{Name:'minecraft:oak_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(1027, "{Name:'minecraft:oak_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(1028, "{Name:'minecraft:oak_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(1029, "{Name:'minecraft:oak_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(1030, "{Name:'minecraft:oak_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(1031, "{Name:'minecraft:oak_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(1032, "{Name:'minecraft:oak_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'false'}}"); -+ register(1033, "{Name:'minecraft:oak_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'false'}}"); -+ register(1034, "{Name:'minecraft:oak_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'true'}}"); -+ register(1035, "{Name:'minecraft:oak_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:wooden_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'true'}}"); -+ register(1036, "{Name:'minecraft:oak_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'false'}}"); -+ register(1037, "{Name:'minecraft:oak_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'false'}}"); -+ register(1038, "{Name:'minecraft:oak_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'false'}}"); -+ register(1039, "{Name:'minecraft:oak_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'false'}}"); -+ register(1042, "{Name:'minecraft:ladder',Properties:{facing:'north'}}", "{Name:'minecraft:ladder',Properties:{facing:'north'}}"); -+ register(1043, "{Name:'minecraft:ladder',Properties:{facing:'south'}}", "{Name:'minecraft:ladder',Properties:{facing:'south'}}"); -+ register(1044, "{Name:'minecraft:ladder',Properties:{facing:'west'}}", "{Name:'minecraft:ladder',Properties:{facing:'west'}}"); -+ register(1045, "{Name:'minecraft:ladder',Properties:{facing:'east'}}", "{Name:'minecraft:ladder',Properties:{facing:'east'}}"); -+ register(1056, "{Name:'minecraft:rail',Properties:{shape:'north_south'}}", "{Name:'minecraft:rail',Properties:{shape:'north_south'}}"); -+ register(1057, "{Name:'minecraft:rail',Properties:{shape:'east_west'}}", "{Name:'minecraft:rail',Properties:{shape:'east_west'}}"); -+ register(1058, "{Name:'minecraft:rail',Properties:{shape:'ascending_east'}}", "{Name:'minecraft:rail',Properties:{shape:'ascending_east'}}"); -+ register(1059, "{Name:'minecraft:rail',Properties:{shape:'ascending_west'}}", "{Name:'minecraft:rail',Properties:{shape:'ascending_west'}}"); -+ register(1060, "{Name:'minecraft:rail',Properties:{shape:'ascending_north'}}", "{Name:'minecraft:rail',Properties:{shape:'ascending_north'}}"); -+ register(1061, "{Name:'minecraft:rail',Properties:{shape:'ascending_south'}}", "{Name:'minecraft:rail',Properties:{shape:'ascending_south'}}"); -+ register(1062, "{Name:'minecraft:rail',Properties:{shape:'south_east'}}", "{Name:'minecraft:rail',Properties:{shape:'south_east'}}"); -+ register(1063, "{Name:'minecraft:rail',Properties:{shape:'south_west'}}", "{Name:'minecraft:rail',Properties:{shape:'south_west'}}"); -+ register(1064, "{Name:'minecraft:rail',Properties:{shape:'north_west'}}", "{Name:'minecraft:rail',Properties:{shape:'north_west'}}"); -+ register(1065, "{Name:'minecraft:rail',Properties:{shape:'north_east'}}", "{Name:'minecraft:rail',Properties:{shape:'north_east'}}"); -+ register(1072, "{Name:'minecraft:cobblestone_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); -+ register(1073, "{Name:'minecraft:cobblestone_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); -+ register(1074, "{Name:'minecraft:cobblestone_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); -+ register(1075, "{Name:'minecraft:cobblestone_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); -+ register(1076, "{Name:'minecraft:cobblestone_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); -+ register(1077, "{Name:'minecraft:cobblestone_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); -+ register(1078, "{Name:'minecraft:cobblestone_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); -+ register(1079, "{Name:'minecraft:cobblestone_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:stone_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); -+ register(1090, "{Name:'minecraft:wall_sign',Properties:{facing:'north'}}", "{Name:'minecraft:wall_sign',Properties:{facing:'north'}}"); -+ register(1091, "{Name:'minecraft:wall_sign',Properties:{facing:'south'}}", "{Name:'minecraft:wall_sign',Properties:{facing:'south'}}"); -+ register(1092, "{Name:'minecraft:wall_sign',Properties:{facing:'west'}}", "{Name:'minecraft:wall_sign',Properties:{facing:'west'}}"); -+ register(1093, "{Name:'minecraft:wall_sign',Properties:{facing:'east'}}", "{Name:'minecraft:wall_sign',Properties:{facing:'east'}}"); -+ register(1104, "{Name:'minecraft:lever',Properties:{face:'ceiling',facing:'west',powered:'false'}}", "{Name:'minecraft:lever',Properties:{facing:'down_x',powered:'false'}}"); -+ register(1105, "{Name:'minecraft:lever',Properties:{face:'wall',facing:'east',powered:'false'}}", "{Name:'minecraft:lever',Properties:{facing:'east',powered:'false'}}"); -+ register(1106, "{Name:'minecraft:lever',Properties:{face:'wall',facing:'west',powered:'false'}}", "{Name:'minecraft:lever',Properties:{facing:'west',powered:'false'}}"); -+ register(1107, "{Name:'minecraft:lever',Properties:{face:'wall',facing:'south',powered:'false'}}", "{Name:'minecraft:lever',Properties:{facing:'south',powered:'false'}}"); -+ register(1108, "{Name:'minecraft:lever',Properties:{face:'wall',facing:'north',powered:'false'}}", "{Name:'minecraft:lever',Properties:{facing:'north',powered:'false'}}"); -+ register(1109, "{Name:'minecraft:lever',Properties:{face:'floor',facing:'north',powered:'false'}}", "{Name:'minecraft:lever',Properties:{facing:'up_z',powered:'false'}}"); -+ register(1110, "{Name:'minecraft:lever',Properties:{face:'floor',facing:'west',powered:'false'}}", "{Name:'minecraft:lever',Properties:{facing:'up_x',powered:'false'}}"); -+ register(1111, "{Name:'minecraft:lever',Properties:{face:'ceiling',facing:'north',powered:'false'}}", "{Name:'minecraft:lever',Properties:{facing:'down_z',powered:'false'}}"); -+ register(1112, "{Name:'minecraft:lever',Properties:{face:'ceiling',facing:'west',powered:'true'}}", "{Name:'minecraft:lever',Properties:{facing:'down_x',powered:'true'}}"); -+ register(1113, "{Name:'minecraft:lever',Properties:{face:'wall',facing:'east',powered:'true'}}", "{Name:'minecraft:lever',Properties:{facing:'east',powered:'true'}}"); -+ register(1114, "{Name:'minecraft:lever',Properties:{face:'wall',facing:'west',powered:'true'}}", "{Name:'minecraft:lever',Properties:{facing:'west',powered:'true'}}"); -+ register(1115, "{Name:'minecraft:lever',Properties:{face:'wall',facing:'south',powered:'true'}}", "{Name:'minecraft:lever',Properties:{facing:'south',powered:'true'}}"); -+ register(1116, "{Name:'minecraft:lever',Properties:{face:'wall',facing:'north',powered:'true'}}", "{Name:'minecraft:lever',Properties:{facing:'north',powered:'true'}}"); -+ register(1117, "{Name:'minecraft:lever',Properties:{face:'floor',facing:'north',powered:'true'}}", "{Name:'minecraft:lever',Properties:{facing:'up_z',powered:'true'}}"); -+ register(1118, "{Name:'minecraft:lever',Properties:{face:'floor',facing:'west',powered:'true'}}", "{Name:'minecraft:lever',Properties:{facing:'up_x',powered:'true'}}"); -+ register(1119, "{Name:'minecraft:lever',Properties:{face:'ceiling',facing:'north',powered:'true'}}", "{Name:'minecraft:lever',Properties:{facing:'down_z',powered:'true'}}"); -+ register(1120, "{Name:'minecraft:stone_pressure_plate',Properties:{powered:'false'}}", "{Name:'minecraft:stone_pressure_plate',Properties:{powered:'false'}}"); -+ register(1121, "{Name:'minecraft:stone_pressure_plate',Properties:{powered:'true'}}", "{Name:'minecraft:stone_pressure_plate',Properties:{powered:'true'}}"); -+ register(1136, "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(1137, "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(1138, "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(1139, "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(1140, "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(1141, "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(1142, "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(1143, "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(1144, "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'false'}}"); -+ register(1145, "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'false'}}"); -+ register(1146, "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'true'}}"); -+ register(1147, "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'true'}}"); -+ register(1148, "{Name:'minecraft:iron_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'false'}}"); -+ register(1149, "{Name:'minecraft:iron_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'false'}}"); -+ register(1150, "{Name:'minecraft:iron_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'false'}}"); -+ register(1151, "{Name:'minecraft:iron_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'false'}}"); -+ register(1152, "{Name:'minecraft:oak_pressure_plate',Properties:{powered:'false'}}", "{Name:'minecraft:wooden_pressure_plate',Properties:{powered:'false'}}"); -+ register(1153, "{Name:'minecraft:oak_pressure_plate',Properties:{powered:'true'}}", "{Name:'minecraft:wooden_pressure_plate',Properties:{powered:'true'}}"); -+ register(1168, "{Name:'minecraft:redstone_ore',Properties:{lit:'false'}}", "{Name:'minecraft:redstone_ore'}"); -+ register(1184, "{Name:'minecraft:redstone_ore',Properties:{lit:'true'}}", "{Name:'minecraft:lit_redstone_ore'}"); -+ register(1201, "{Name:'minecraft:redstone_wall_torch',Properties:{facing:'east',lit:'false'}}", "{Name:'minecraft:unlit_redstone_torch',Properties:{facing:'east'}}"); -+ register(1202, "{Name:'minecraft:redstone_wall_torch',Properties:{facing:'west',lit:'false'}}", "{Name:'minecraft:unlit_redstone_torch',Properties:{facing:'west'}}"); -+ register(1203, "{Name:'minecraft:redstone_wall_torch',Properties:{facing:'south',lit:'false'}}", "{Name:'minecraft:unlit_redstone_torch',Properties:{facing:'south'}}"); -+ register(1204, "{Name:'minecraft:redstone_wall_torch',Properties:{facing:'north',lit:'false'}}", "{Name:'minecraft:unlit_redstone_torch',Properties:{facing:'north'}}"); -+ register(1205, "{Name:'minecraft:redstone_torch',Properties:{lit:'false'}}", "{Name:'minecraft:unlit_redstone_torch',Properties:{facing:'up'}}"); -+ register(1217, "{Name:'minecraft:redstone_wall_torch',Properties:{facing:'east',lit:'true'}}", "{Name:'minecraft:redstone_torch',Properties:{facing:'east'}}"); -+ register(1218, "{Name:'minecraft:redstone_wall_torch',Properties:{facing:'west',lit:'true'}}", "{Name:'minecraft:redstone_torch',Properties:{facing:'west'}}"); -+ register(1219, "{Name:'minecraft:redstone_wall_torch',Properties:{facing:'south',lit:'true'}}", "{Name:'minecraft:redstone_torch',Properties:{facing:'south'}}"); -+ register(1220, "{Name:'minecraft:redstone_wall_torch',Properties:{facing:'north',lit:'true'}}", "{Name:'minecraft:redstone_torch',Properties:{facing:'north'}}"); -+ register(1221, "{Name:'minecraft:redstone_torch',Properties:{lit:'true'}}", "{Name:'minecraft:redstone_torch',Properties:{facing:'up'}}"); -+ register(1232, "{Name:'minecraft:stone_button',Properties:{face:'ceiling',facing:'north',powered:'false'}}", "{Name:'minecraft:stone_button',Properties:{facing:'down',powered:'false'}}"); -+ register(1233, "{Name:'minecraft:stone_button',Properties:{face:'wall',facing:'east',powered:'false'}}", "{Name:'minecraft:stone_button',Properties:{facing:'east',powered:'false'}}"); -+ register(1234, "{Name:'minecraft:stone_button',Properties:{face:'wall',facing:'west',powered:'false'}}", "{Name:'minecraft:stone_button',Properties:{facing:'west',powered:'false'}}"); -+ register(1235, "{Name:'minecraft:stone_button',Properties:{face:'wall',facing:'south',powered:'false'}}", "{Name:'minecraft:stone_button',Properties:{facing:'south',powered:'false'}}"); -+ register(1236, "{Name:'minecraft:stone_button',Properties:{face:'wall',facing:'north',powered:'false'}}", "{Name:'minecraft:stone_button',Properties:{facing:'north',powered:'false'}}"); -+ register(1237, "{Name:'minecraft:stone_button',Properties:{face:'floor',facing:'north',powered:'false'}}", "{Name:'minecraft:stone_button',Properties:{facing:'up',powered:'false'}}"); -+ register(1240, "{Name:'minecraft:stone_button',Properties:{face:'ceiling',facing:'north',powered:'true'}}", "{Name:'minecraft:stone_button',Properties:{facing:'down',powered:'true'}}"); -+ register(1241, "{Name:'minecraft:stone_button',Properties:{face:'wall',facing:'east',powered:'true'}}", "{Name:'minecraft:stone_button',Properties:{facing:'east',powered:'true'}}"); -+ register(1242, "{Name:'minecraft:stone_button',Properties:{face:'wall',facing:'west',powered:'true'}}", "{Name:'minecraft:stone_button',Properties:{facing:'west',powered:'true'}}"); -+ register(1243, "{Name:'minecraft:stone_button',Properties:{face:'wall',facing:'south',powered:'true'}}", "{Name:'minecraft:stone_button',Properties:{facing:'south',powered:'true'}}"); -+ register(1244, "{Name:'minecraft:stone_button',Properties:{face:'wall',facing:'north',powered:'true'}}", "{Name:'minecraft:stone_button',Properties:{facing:'north',powered:'true'}}"); -+ register(1245, "{Name:'minecraft:stone_button',Properties:{face:'floor',facing:'north',powered:'true'}}", "{Name:'minecraft:stone_button',Properties:{facing:'up',powered:'true'}}"); -+ register(1248, "{Name:'minecraft:snow',Properties:{layers:'1'}}", "{Name:'minecraft:snow_layer',Properties:{layers:'1'}}"); -+ register(1249, "{Name:'minecraft:snow',Properties:{layers:'2'}}", "{Name:'minecraft:snow_layer',Properties:{layers:'2'}}"); -+ register(1250, "{Name:'minecraft:snow',Properties:{layers:'3'}}", "{Name:'minecraft:snow_layer',Properties:{layers:'3'}}"); -+ register(1251, "{Name:'minecraft:snow',Properties:{layers:'4'}}", "{Name:'minecraft:snow_layer',Properties:{layers:'4'}}"); -+ register(1252, "{Name:'minecraft:snow',Properties:{layers:'5'}}", "{Name:'minecraft:snow_layer',Properties:{layers:'5'}}"); -+ register(1253, "{Name:'minecraft:snow',Properties:{layers:'6'}}", "{Name:'minecraft:snow_layer',Properties:{layers:'6'}}"); -+ register(1254, "{Name:'minecraft:snow',Properties:{layers:'7'}}", "{Name:'minecraft:snow_layer',Properties:{layers:'7'}}"); -+ register(1255, "{Name:'minecraft:snow',Properties:{layers:'8'}}", "{Name:'minecraft:snow_layer',Properties:{layers:'8'}}"); -+ register(1264, "{Name:'minecraft:ice'}", "{Name:'minecraft:ice'}"); -+ register(1280, "{Name:'minecraft:snow_block'}", "{Name:'minecraft:snow'}"); -+ register(1296, "{Name:'minecraft:cactus',Properties:{age:'0'}}", "{Name:'minecraft:cactus',Properties:{age:'0'}}"); -+ register(1297, "{Name:'minecraft:cactus',Properties:{age:'1'}}", "{Name:'minecraft:cactus',Properties:{age:'1'}}"); -+ register(1298, "{Name:'minecraft:cactus',Properties:{age:'2'}}", "{Name:'minecraft:cactus',Properties:{age:'2'}}"); -+ register(1299, "{Name:'minecraft:cactus',Properties:{age:'3'}}", "{Name:'minecraft:cactus',Properties:{age:'3'}}"); -+ register(1300, "{Name:'minecraft:cactus',Properties:{age:'4'}}", "{Name:'minecraft:cactus',Properties:{age:'4'}}"); -+ register(1301, "{Name:'minecraft:cactus',Properties:{age:'5'}}", "{Name:'minecraft:cactus',Properties:{age:'5'}}"); -+ register(1302, "{Name:'minecraft:cactus',Properties:{age:'6'}}", "{Name:'minecraft:cactus',Properties:{age:'6'}}"); -+ register(1303, "{Name:'minecraft:cactus',Properties:{age:'7'}}", "{Name:'minecraft:cactus',Properties:{age:'7'}}"); -+ register(1304, "{Name:'minecraft:cactus',Properties:{age:'8'}}", "{Name:'minecraft:cactus',Properties:{age:'8'}}"); -+ register(1305, "{Name:'minecraft:cactus',Properties:{age:'9'}}", "{Name:'minecraft:cactus',Properties:{age:'9'}}"); -+ register(1306, "{Name:'minecraft:cactus',Properties:{age:'10'}}", "{Name:'minecraft:cactus',Properties:{age:'10'}}"); -+ register(1307, "{Name:'minecraft:cactus',Properties:{age:'11'}}", "{Name:'minecraft:cactus',Properties:{age:'11'}}"); -+ register(1308, "{Name:'minecraft:cactus',Properties:{age:'12'}}", "{Name:'minecraft:cactus',Properties:{age:'12'}}"); -+ register(1309, "{Name:'minecraft:cactus',Properties:{age:'13'}}", "{Name:'minecraft:cactus',Properties:{age:'13'}}"); -+ register(1310, "{Name:'minecraft:cactus',Properties:{age:'14'}}", "{Name:'minecraft:cactus',Properties:{age:'14'}}"); -+ register(1311, "{Name:'minecraft:cactus',Properties:{age:'15'}}", "{Name:'minecraft:cactus',Properties:{age:'15'}}"); -+ register(1312, "{Name:'minecraft:clay'}", "{Name:'minecraft:clay'}"); -+ register(1328, "{Name:'minecraft:sugar_cane',Properties:{age:'0'}}", "{Name:'minecraft:reeds',Properties:{age:'0'}}"); -+ register(1329, "{Name:'minecraft:sugar_cane',Properties:{age:'1'}}", "{Name:'minecraft:reeds',Properties:{age:'1'}}"); -+ register(1330, "{Name:'minecraft:sugar_cane',Properties:{age:'2'}}", "{Name:'minecraft:reeds',Properties:{age:'2'}}"); -+ register(1331, "{Name:'minecraft:sugar_cane',Properties:{age:'3'}}", "{Name:'minecraft:reeds',Properties:{age:'3'}}"); -+ register(1332, "{Name:'minecraft:sugar_cane',Properties:{age:'4'}}", "{Name:'minecraft:reeds',Properties:{age:'4'}}"); -+ register(1333, "{Name:'minecraft:sugar_cane',Properties:{age:'5'}}", "{Name:'minecraft:reeds',Properties:{age:'5'}}"); -+ register(1334, "{Name:'minecraft:sugar_cane',Properties:{age:'6'}}", "{Name:'minecraft:reeds',Properties:{age:'6'}}"); -+ register(1335, "{Name:'minecraft:sugar_cane',Properties:{age:'7'}}", "{Name:'minecraft:reeds',Properties:{age:'7'}}"); -+ register(1336, "{Name:'minecraft:sugar_cane',Properties:{age:'8'}}", "{Name:'minecraft:reeds',Properties:{age:'8'}}"); -+ register(1337, "{Name:'minecraft:sugar_cane',Properties:{age:'9'}}", "{Name:'minecraft:reeds',Properties:{age:'9'}}"); -+ register(1338, "{Name:'minecraft:sugar_cane',Properties:{age:'10'}}", "{Name:'minecraft:reeds',Properties:{age:'10'}}"); -+ register(1339, "{Name:'minecraft:sugar_cane',Properties:{age:'11'}}", "{Name:'minecraft:reeds',Properties:{age:'11'}}"); -+ register(1340, "{Name:'minecraft:sugar_cane',Properties:{age:'12'}}", "{Name:'minecraft:reeds',Properties:{age:'12'}}"); -+ register(1341, "{Name:'minecraft:sugar_cane',Properties:{age:'13'}}", "{Name:'minecraft:reeds',Properties:{age:'13'}}"); -+ register(1342, "{Name:'minecraft:sugar_cane',Properties:{age:'14'}}", "{Name:'minecraft:reeds',Properties:{age:'14'}}"); -+ register(1343, "{Name:'minecraft:sugar_cane',Properties:{age:'15'}}", "{Name:'minecraft:reeds',Properties:{age:'15'}}"); -+ register(1344, "{Name:'minecraft:jukebox',Properties:{has_record:'false'}}", "{Name:'minecraft:jukebox',Properties:{has_record:'false'}}"); -+ register(1345, "{Name:'minecraft:jukebox',Properties:{has_record:'true'}}", "{Name:'minecraft:jukebox',Properties:{has_record:'true'}}"); -+ register(1360, "{Name:'minecraft:oak_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:fence',Properties:{east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:fence',Properties:{east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:fence',Properties:{east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:fence',Properties:{east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:fence',Properties:{east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:fence',Properties:{east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:fence',Properties:{east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:fence',Properties:{east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:fence',Properties:{east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:fence',Properties:{east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:fence',Properties:{east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:fence',Properties:{east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:fence',Properties:{east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:fence',Properties:{east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:fence',Properties:{east:'true',north:'true',south:'true',west:'true'}}"); -+ register(1376, "{Name:'minecraft:carved_pumpkin',Properties:{facing:'south'}}", "{Name:'minecraft:pumpkin',Properties:{facing:'south'}}"); -+ register(1377, "{Name:'minecraft:carved_pumpkin',Properties:{facing:'west'}}", "{Name:'minecraft:pumpkin',Properties:{facing:'west'}}"); -+ register(1378, "{Name:'minecraft:carved_pumpkin',Properties:{facing:'north'}}", "{Name:'minecraft:pumpkin',Properties:{facing:'north'}}"); -+ register(1379, "{Name:'minecraft:carved_pumpkin',Properties:{facing:'east'}}", "{Name:'minecraft:pumpkin',Properties:{facing:'east'}}"); -+ register(1392, "{Name:'minecraft:netherrack'}", "{Name:'minecraft:netherrack'}"); -+ register(1408, "{Name:'minecraft:soul_sand'}", "{Name:'minecraft:soul_sand'}"); -+ register(1424, "{Name:'minecraft:glowstone'}", "{Name:'minecraft:glowstone'}"); -+ register(1441, "{Name:'minecraft:portal',Properties:{axis:'x'}}", "{Name:'minecraft:portal',Properties:{axis:'x'}}"); -+ register(1442, "{Name:'minecraft:portal',Properties:{axis:'z'}}", "{Name:'minecraft:portal',Properties:{axis:'z'}}"); -+ register(1456, "{Name:'minecraft:jack_o_lantern',Properties:{facing:'south'}}", "{Name:'minecraft:lit_pumpkin',Properties:{facing:'south'}}"); -+ register(1457, "{Name:'minecraft:jack_o_lantern',Properties:{facing:'west'}}", "{Name:'minecraft:lit_pumpkin',Properties:{facing:'west'}}"); -+ register(1458, "{Name:'minecraft:jack_o_lantern',Properties:{facing:'north'}}", "{Name:'minecraft:lit_pumpkin',Properties:{facing:'north'}}"); -+ register(1459, "{Name:'minecraft:jack_o_lantern',Properties:{facing:'east'}}", "{Name:'minecraft:lit_pumpkin',Properties:{facing:'east'}}"); -+ register(1472, "{Name:'minecraft:cake',Properties:{bites:'0'}}", "{Name:'minecraft:cake',Properties:{bites:'0'}}"); -+ register(1473, "{Name:'minecraft:cake',Properties:{bites:'1'}}", "{Name:'minecraft:cake',Properties:{bites:'1'}}"); -+ register(1474, "{Name:'minecraft:cake',Properties:{bites:'2'}}", "{Name:'minecraft:cake',Properties:{bites:'2'}}"); -+ register(1475, "{Name:'minecraft:cake',Properties:{bites:'3'}}", "{Name:'minecraft:cake',Properties:{bites:'3'}}"); -+ register(1476, "{Name:'minecraft:cake',Properties:{bites:'4'}}", "{Name:'minecraft:cake',Properties:{bites:'4'}}"); -+ register(1477, "{Name:'minecraft:cake',Properties:{bites:'5'}}", "{Name:'minecraft:cake',Properties:{bites:'5'}}"); -+ register(1478, "{Name:'minecraft:cake',Properties:{bites:'6'}}", "{Name:'minecraft:cake',Properties:{bites:'6'}}"); -+ register(1488, "{Name:'minecraft:repeater',Properties:{delay:'1',facing:'south',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'1',facing:'south',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'1',facing:'south',locked:'true'}}"); -+ register(1489, "{Name:'minecraft:repeater',Properties:{delay:'1',facing:'west',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'1',facing:'west',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'1',facing:'west',locked:'true'}}"); -+ register(1490, "{Name:'minecraft:repeater',Properties:{delay:'1',facing:'north',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'1',facing:'north',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'1',facing:'north',locked:'true'}}"); -+ register(1491, "{Name:'minecraft:repeater',Properties:{delay:'1',facing:'east',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'1',facing:'east',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'1',facing:'east',locked:'true'}}"); -+ register(1492, "{Name:'minecraft:repeater',Properties:{delay:'2',facing:'south',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'2',facing:'south',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'2',facing:'south',locked:'true'}}"); -+ register(1493, "{Name:'minecraft:repeater',Properties:{delay:'2',facing:'west',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'2',facing:'west',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'2',facing:'west',locked:'true'}}"); -+ register(1494, "{Name:'minecraft:repeater',Properties:{delay:'2',facing:'north',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'2',facing:'north',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'2',facing:'north',locked:'true'}}"); -+ register(1495, "{Name:'minecraft:repeater',Properties:{delay:'2',facing:'east',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'2',facing:'east',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'2',facing:'east',locked:'true'}}"); -+ register(1496, "{Name:'minecraft:repeater',Properties:{delay:'3',facing:'south',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'3',facing:'south',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'3',facing:'south',locked:'true'}}"); -+ register(1497, "{Name:'minecraft:repeater',Properties:{delay:'3',facing:'west',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'3',facing:'west',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'3',facing:'west',locked:'true'}}"); -+ register(1498, "{Name:'minecraft:repeater',Properties:{delay:'3',facing:'north',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'3',facing:'north',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'3',facing:'north',locked:'true'}}"); -+ register(1499, "{Name:'minecraft:repeater',Properties:{delay:'3',facing:'east',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'3',facing:'east',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'3',facing:'east',locked:'true'}}"); -+ register(1500, "{Name:'minecraft:repeater',Properties:{delay:'4',facing:'south',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'4',facing:'south',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'4',facing:'south',locked:'true'}}"); -+ register(1501, "{Name:'minecraft:repeater',Properties:{delay:'4',facing:'west',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'4',facing:'west',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'4',facing:'west',locked:'true'}}"); -+ register(1502, "{Name:'minecraft:repeater',Properties:{delay:'4',facing:'north',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'4',facing:'north',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'4',facing:'north',locked:'true'}}"); -+ register(1503, "{Name:'minecraft:repeater',Properties:{delay:'4',facing:'east',locked:'false',powered:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'4',facing:'east',locked:'false'}}", "{Name:'minecraft:unpowered_repeater',Properties:{delay:'4',facing:'east',locked:'true'}}"); -+ register(1504, "{Name:'minecraft:repeater',Properties:{delay:'1',facing:'south',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'1',facing:'south',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'1',facing:'south',locked:'true'}}"); -+ register(1505, "{Name:'minecraft:repeater',Properties:{delay:'1',facing:'west',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'1',facing:'west',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'1',facing:'west',locked:'true'}}"); -+ register(1506, "{Name:'minecraft:repeater',Properties:{delay:'1',facing:'north',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'1',facing:'north',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'1',facing:'north',locked:'true'}}"); -+ register(1507, "{Name:'minecraft:repeater',Properties:{delay:'1',facing:'east',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'1',facing:'east',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'1',facing:'east',locked:'true'}}"); -+ register(1508, "{Name:'minecraft:repeater',Properties:{delay:'2',facing:'south',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'2',facing:'south',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'2',facing:'south',locked:'true'}}"); -+ register(1509, "{Name:'minecraft:repeater',Properties:{delay:'2',facing:'west',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'2',facing:'west',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'2',facing:'west',locked:'true'}}"); -+ register(1510, "{Name:'minecraft:repeater',Properties:{delay:'2',facing:'north',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'2',facing:'north',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'2',facing:'north',locked:'true'}}"); -+ register(1511, "{Name:'minecraft:repeater',Properties:{delay:'2',facing:'east',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'2',facing:'east',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'2',facing:'east',locked:'true'}}"); -+ register(1512, "{Name:'minecraft:repeater',Properties:{delay:'3',facing:'south',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'3',facing:'south',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'3',facing:'south',locked:'true'}}"); -+ register(1513, "{Name:'minecraft:repeater',Properties:{delay:'3',facing:'west',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'3',facing:'west',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'3',facing:'west',locked:'true'}}"); -+ register(1514, "{Name:'minecraft:repeater',Properties:{delay:'3',facing:'north',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'3',facing:'north',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'3',facing:'north',locked:'true'}}"); -+ register(1515, "{Name:'minecraft:repeater',Properties:{delay:'3',facing:'east',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'3',facing:'east',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'3',facing:'east',locked:'true'}}"); -+ register(1516, "{Name:'minecraft:repeater',Properties:{delay:'4',facing:'south',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'4',facing:'south',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'4',facing:'south',locked:'true'}}"); -+ register(1517, "{Name:'minecraft:repeater',Properties:{delay:'4',facing:'west',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'4',facing:'west',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'4',facing:'west',locked:'true'}}"); -+ register(1518, "{Name:'minecraft:repeater',Properties:{delay:'4',facing:'north',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'4',facing:'north',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'4',facing:'north',locked:'true'}}"); -+ register(1519, "{Name:'minecraft:repeater',Properties:{delay:'4',facing:'east',locked:'false',powered:'true'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'4',facing:'east',locked:'false'}}", "{Name:'minecraft:powered_repeater',Properties:{delay:'4',facing:'east',locked:'true'}}"); -+ register(1520, "{Name:'minecraft:white_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'white'}}"); -+ register(1521, "{Name:'minecraft:orange_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'orange'}}"); -+ register(1522, "{Name:'minecraft:magenta_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'magenta'}}"); -+ register(1523, "{Name:'minecraft:light_blue_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'light_blue'}}"); -+ register(1524, "{Name:'minecraft:yellow_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'yellow'}}"); -+ register(1525, "{Name:'minecraft:lime_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'lime'}}"); -+ register(1526, "{Name:'minecraft:pink_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'pink'}}"); -+ register(1527, "{Name:'minecraft:gray_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'gray'}}"); -+ register(1528, "{Name:'minecraft:light_gray_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'silver'}}"); -+ register(1529, "{Name:'minecraft:cyan_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'cyan'}}"); -+ register(1530, "{Name:'minecraft:purple_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'purple'}}"); -+ register(1531, "{Name:'minecraft:blue_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'blue'}}"); -+ register(1532, "{Name:'minecraft:brown_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'brown'}}"); -+ register(1533, "{Name:'minecraft:green_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'green'}}"); -+ register(1534, "{Name:'minecraft:red_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'red'}}"); -+ register(1535, "{Name:'minecraft:black_stained_glass'}", "{Name:'minecraft:stained_glass',Properties:{color:'black'}}"); -+ register(1536, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'north',half:'bottom',open:'false'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'north',half:'bottom',open:'false'}}"); -+ register(1537, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'south',half:'bottom',open:'false'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'south',half:'bottom',open:'false'}}"); -+ register(1538, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'west',half:'bottom',open:'false'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'west',half:'bottom',open:'false'}}"); -+ register(1539, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'east',half:'bottom',open:'false'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'east',half:'bottom',open:'false'}}"); -+ register(1540, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'north',half:'bottom',open:'true'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'north',half:'bottom',open:'true'}}"); -+ register(1541, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'south',half:'bottom',open:'true'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'south',half:'bottom',open:'true'}}"); -+ register(1542, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'west',half:'bottom',open:'true'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'west',half:'bottom',open:'true'}}"); -+ register(1543, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'east',half:'bottom',open:'true'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'east',half:'bottom',open:'true'}}"); -+ register(1544, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'north',half:'top',open:'false'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'north',half:'top',open:'false'}}"); -+ register(1545, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'south',half:'top',open:'false'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'south',half:'top',open:'false'}}"); -+ register(1546, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'west',half:'top',open:'false'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'west',half:'top',open:'false'}}"); -+ register(1547, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'east',half:'top',open:'false'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'east',half:'top',open:'false'}}"); -+ register(1548, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'north',half:'top',open:'true'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'north',half:'top',open:'true'}}"); -+ register(1549, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'south',half:'top',open:'true'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'south',half:'top',open:'true'}}"); -+ register(1550, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'west',half:'top',open:'true'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'west',half:'top',open:'true'}}"); -+ register(1551, "{Name:'minecraft:oak_trapdoor',Properties:{facing:'east',half:'top',open:'true'}}", "{Name:'minecraft:trapdoor',Properties:{facing:'east',half:'top',open:'true'}}"); -+ register(1552, "{Name:'minecraft:infested_stone'}", "{Name:'minecraft:monster_egg',Properties:{variant:'stone'}}"); -+ register(1553, "{Name:'minecraft:infested_cobblestone'}", "{Name:'minecraft:monster_egg',Properties:{variant:'cobblestone'}}"); -+ register(1554, "{Name:'minecraft:infested_stone_bricks'}", "{Name:'minecraft:monster_egg',Properties:{variant:'stone_brick'}}"); -+ register(1555, "{Name:'minecraft:infested_mossy_stone_bricks'}", "{Name:'minecraft:monster_egg',Properties:{variant:'mossy_brick'}}"); -+ register(1556, "{Name:'minecraft:infested_cracked_stone_bricks'}", "{Name:'minecraft:monster_egg',Properties:{variant:'cracked_brick'}}"); -+ register(1557, "{Name:'minecraft:infested_chiseled_stone_bricks'}", "{Name:'minecraft:monster_egg',Properties:{variant:'chiseled_brick'}}"); -+ register(1568, "{Name:'minecraft:stone_bricks'}", "{Name:'minecraft:stonebrick',Properties:{variant:'stonebrick'}}"); -+ register(1569, "{Name:'minecraft:mossy_stone_bricks'}", "{Name:'minecraft:stonebrick',Properties:{variant:'mossy_stonebrick'}}"); -+ register(1570, "{Name:'minecraft:cracked_stone_bricks'}", "{Name:'minecraft:stonebrick',Properties:{variant:'cracked_stonebrick'}}"); -+ register(1571, "{Name:'minecraft:chiseled_stone_bricks'}", "{Name:'minecraft:stonebrick',Properties:{variant:'chiseled_stonebrick'}}"); -+ register(1584, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'false',up:'false',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'all_inside'}}"); -+ register(1585, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'true',east:'false',south:'false',west:'true',up:'true',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'north_west'}}"); -+ register(1586, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'true',east:'false',south:'false',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'north'}}"); -+ register(1587, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'true',east:'true',south:'false',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'north_east'}}"); -+ register(1588, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'true',up:'true',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'west'}}"); -+ register(1589, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'center'}}"); -+ register(1590, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'false',east:'true',south:'false',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'east'}}"); -+ register(1591, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'false',east:'false',south:'true',west:'true',up:'true',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'south_west'}}"); -+ register(1592, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'false',east:'false',south:'true',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'south'}}"); -+ register(1593, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'false',east:'true',south:'true',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'south_east'}}"); -+ register(1594, "{Name:'minecraft:mushroom_stem',Properties:{north:'true',east:'true',south:'true',west:'true',up:'false',down:'false'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'stem'}}"); -+ register(1595, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'false',up:'false',down:'false'}}"); -+ register(1596, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'false',up:'false',down:'false'}}"); -+ register(1597, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'false',up:'false',down:'false'}}"); -+ register(1598, "{Name:'minecraft:brown_mushroom_block',Properties:{north:'true',east:'true',south:'true',west:'true',up:'true',down:'true'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'all_outside'}}"); -+ register(1599, "{Name:'minecraft:mushroom_stem',Properties:{north:'true',east:'true',south:'true',west:'true',up:'true',down:'true'}}", "{Name:'minecraft:brown_mushroom_block',Properties:{variant:'all_stem'}}"); -+ register(1600, "{Name:'minecraft:red_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'false',up:'false',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'all_inside'}}"); -+ register(1601, "{Name:'minecraft:red_mushroom_block',Properties:{north:'true',east:'false',south:'false',west:'true',up:'true',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'north_west'}}"); -+ register(1602, "{Name:'minecraft:red_mushroom_block',Properties:{north:'true',east:'false',south:'false',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'north'}}"); -+ register(1603, "{Name:'minecraft:red_mushroom_block',Properties:{north:'true',east:'true',south:'false',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'north_east'}}"); -+ register(1604, "{Name:'minecraft:red_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'true',up:'true',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'west'}}"); -+ register(1605, "{Name:'minecraft:red_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'center'}}"); -+ register(1606, "{Name:'minecraft:red_mushroom_block',Properties:{north:'false',east:'true',south:'false',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'east'}}"); -+ register(1607, "{Name:'minecraft:red_mushroom_block',Properties:{north:'false',east:'false',south:'true',west:'true',up:'true',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'south_west'}}"); -+ register(1608, "{Name:'minecraft:red_mushroom_block',Properties:{north:'false',east:'false',south:'true',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'south'}}"); -+ register(1609, "{Name:'minecraft:red_mushroom_block',Properties:{north:'false',east:'true',south:'true',west:'false',up:'true',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'south_east'}}"); -+ register(1610, "{Name:'minecraft:mushroom_stem',Properties:{north:'true',east:'true',south:'true',west:'true',up:'false',down:'false'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'stem'}}"); -+ register(1611, "{Name:'minecraft:red_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'false',up:'false',down:'false'}}"); -+ register(1612, "{Name:'minecraft:red_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'false',up:'false',down:'false'}}"); -+ register(1613, "{Name:'minecraft:red_mushroom_block',Properties:{north:'false',east:'false',south:'false',west:'false',up:'false',down:'false'}}"); -+ register(1614, "{Name:'minecraft:red_mushroom_block',Properties:{north:'true',east:'true',south:'true',west:'true',up:'true',down:'true'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'all_outside'}}"); -+ register(1615, "{Name:'minecraft:mushroom_stem',Properties:{north:'true',east:'true',south:'true',west:'true',up:'true',down:'true'}}", "{Name:'minecraft:red_mushroom_block',Properties:{variant:'all_stem'}}"); -+ register(1616, "{Name:'minecraft:iron_bars',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:iron_bars',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:iron_bars',Properties:{east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:iron_bars',Properties:{east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:iron_bars',Properties:{east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:iron_bars',Properties:{east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:iron_bars',Properties:{east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:iron_bars',Properties:{east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:iron_bars',Properties:{east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:iron_bars',Properties:{east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:iron_bars',Properties:{east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:iron_bars',Properties:{east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:iron_bars',Properties:{east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:iron_bars',Properties:{east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:iron_bars',Properties:{east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:iron_bars',Properties:{east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:iron_bars',Properties:{east:'true',north:'true',south:'true',west:'true'}}"); -+ register(1632, "{Name:'minecraft:glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:glass_pane',Properties:{east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:glass_pane',Properties:{east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:glass_pane',Properties:{east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:glass_pane',Properties:{east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:glass_pane',Properties:{east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:glass_pane',Properties:{east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:glass_pane',Properties:{east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:glass_pane',Properties:{east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:glass_pane',Properties:{east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:glass_pane',Properties:{east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:glass_pane',Properties:{east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:glass_pane',Properties:{east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:glass_pane',Properties:{east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:glass_pane',Properties:{east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:glass_pane',Properties:{east:'true',north:'true',south:'true',west:'true'}}"); -+ register(1648, "{Name:'minecraft:melon_block'}", "{Name:'minecraft:melon_block'}"); -+ register(1664, "{Name:'minecraft:pumpkin_stem',Properties:{age:'0'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'0',facing:'east'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'0',facing:'north'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'0',facing:'south'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'0',facing:'up'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'0',facing:'west'}}"); -+ register(1665, "{Name:'minecraft:pumpkin_stem',Properties:{age:'1'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'1',facing:'east'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'1',facing:'north'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'1',facing:'south'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'1',facing:'up'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'1',facing:'west'}}"); -+ register(1666, "{Name:'minecraft:pumpkin_stem',Properties:{age:'2'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'2',facing:'east'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'2',facing:'north'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'2',facing:'south'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'2',facing:'up'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'2',facing:'west'}}"); -+ register(1667, "{Name:'minecraft:pumpkin_stem',Properties:{age:'3'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'3',facing:'east'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'3',facing:'north'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'3',facing:'south'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'3',facing:'up'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'3',facing:'west'}}"); -+ register(1668, "{Name:'minecraft:pumpkin_stem',Properties:{age:'4'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'4',facing:'east'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'4',facing:'north'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'4',facing:'south'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'4',facing:'up'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'4',facing:'west'}}"); -+ register(1669, "{Name:'minecraft:pumpkin_stem',Properties:{age:'5'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'5',facing:'east'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'5',facing:'north'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'5',facing:'south'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'5',facing:'up'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'5',facing:'west'}}"); -+ register(1670, "{Name:'minecraft:pumpkin_stem',Properties:{age:'6'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'6',facing:'east'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'6',facing:'north'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'6',facing:'south'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'6',facing:'up'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'6',facing:'west'}}"); -+ register(1671, "{Name:'minecraft:pumpkin_stem',Properties:{age:'7'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'7',facing:'east'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'7',facing:'north'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'7',facing:'south'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'7',facing:'up'}}", "{Name:'minecraft:pumpkin_stem',Properties:{age:'7',facing:'west'}}"); -+ register(1680, "{Name:'minecraft:melon_stem',Properties:{age:'0'}}", "{Name:'minecraft:melon_stem',Properties:{age:'0',facing:'east'}}", "{Name:'minecraft:melon_stem',Properties:{age:'0',facing:'north'}}", "{Name:'minecraft:melon_stem',Properties:{age:'0',facing:'south'}}", "{Name:'minecraft:melon_stem',Properties:{age:'0',facing:'up'}}", "{Name:'minecraft:melon_stem',Properties:{age:'0',facing:'west'}}"); -+ register(1681, "{Name:'minecraft:melon_stem',Properties:{age:'1'}}", "{Name:'minecraft:melon_stem',Properties:{age:'1',facing:'east'}}", "{Name:'minecraft:melon_stem',Properties:{age:'1',facing:'north'}}", "{Name:'minecraft:melon_stem',Properties:{age:'1',facing:'south'}}", "{Name:'minecraft:melon_stem',Properties:{age:'1',facing:'up'}}", "{Name:'minecraft:melon_stem',Properties:{age:'1',facing:'west'}}"); -+ register(1682, "{Name:'minecraft:melon_stem',Properties:{age:'2'}}", "{Name:'minecraft:melon_stem',Properties:{age:'2',facing:'east'}}", "{Name:'minecraft:melon_stem',Properties:{age:'2',facing:'north'}}", "{Name:'minecraft:melon_stem',Properties:{age:'2',facing:'south'}}", "{Name:'minecraft:melon_stem',Properties:{age:'2',facing:'up'}}", "{Name:'minecraft:melon_stem',Properties:{age:'2',facing:'west'}}"); -+ register(1683, "{Name:'minecraft:melon_stem',Properties:{age:'3'}}", "{Name:'minecraft:melon_stem',Properties:{age:'3',facing:'east'}}", "{Name:'minecraft:melon_stem',Properties:{age:'3',facing:'north'}}", "{Name:'minecraft:melon_stem',Properties:{age:'3',facing:'south'}}", "{Name:'minecraft:melon_stem',Properties:{age:'3',facing:'up'}}", "{Name:'minecraft:melon_stem',Properties:{age:'3',facing:'west'}}"); -+ register(1684, "{Name:'minecraft:melon_stem',Properties:{age:'4'}}", "{Name:'minecraft:melon_stem',Properties:{age:'4',facing:'east'}}", "{Name:'minecraft:melon_stem',Properties:{age:'4',facing:'north'}}", "{Name:'minecraft:melon_stem',Properties:{age:'4',facing:'south'}}", "{Name:'minecraft:melon_stem',Properties:{age:'4',facing:'up'}}", "{Name:'minecraft:melon_stem',Properties:{age:'4',facing:'west'}}"); -+ register(1685, "{Name:'minecraft:melon_stem',Properties:{age:'5'}}", "{Name:'minecraft:melon_stem',Properties:{age:'5',facing:'east'}}", "{Name:'minecraft:melon_stem',Properties:{age:'5',facing:'north'}}", "{Name:'minecraft:melon_stem',Properties:{age:'5',facing:'south'}}", "{Name:'minecraft:melon_stem',Properties:{age:'5',facing:'up'}}", "{Name:'minecraft:melon_stem',Properties:{age:'5',facing:'west'}}"); -+ register(1686, "{Name:'minecraft:melon_stem',Properties:{age:'6'}}", "{Name:'minecraft:melon_stem',Properties:{age:'6',facing:'east'}}", "{Name:'minecraft:melon_stem',Properties:{age:'6',facing:'north'}}", "{Name:'minecraft:melon_stem',Properties:{age:'6',facing:'south'}}", "{Name:'minecraft:melon_stem',Properties:{age:'6',facing:'up'}}", "{Name:'minecraft:melon_stem',Properties:{age:'6',facing:'west'}}"); -+ register(1687, "{Name:'minecraft:melon_stem',Properties:{age:'7'}}", "{Name:'minecraft:melon_stem',Properties:{age:'7',facing:'east'}}", "{Name:'minecraft:melon_stem',Properties:{age:'7',facing:'north'}}", "{Name:'minecraft:melon_stem',Properties:{age:'7',facing:'south'}}", "{Name:'minecraft:melon_stem',Properties:{age:'7',facing:'up'}}", "{Name:'minecraft:melon_stem',Properties:{age:'7',facing:'west'}}"); -+ register(1696, "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'false',up:'true',west:'false'}}"); -+ register(1697, "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'true',up:'true',west:'false'}}"); -+ register(1698, "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'false',up:'true',west:'true'}}"); -+ register(1699, "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'false',south:'true',up:'true',west:'true'}}"); -+ register(1700, "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'false',up:'true',west:'false'}}"); -+ register(1701, "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'true',up:'true',west:'false'}}"); -+ register(1702, "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'false',up:'true',west:'true'}}"); -+ register(1703, "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'false',north:'true',south:'true',up:'true',west:'true'}}"); -+ register(1704, "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'false',up:'true',west:'false'}}"); -+ register(1705, "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'true',up:'true',west:'false'}}"); -+ register(1706, "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'false',up:'true',west:'true'}}"); -+ register(1707, "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'false',south:'true',up:'true',west:'true'}}"); -+ register(1708, "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'false',up:'true',west:'false'}}"); -+ register(1709, "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'true',up:'true',west:'false'}}"); -+ register(1710, "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'false',up:'true',west:'true'}}"); -+ register(1711, "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:vine',Properties:{east:'true',north:'true',south:'true',up:'true',west:'true'}}"); -+ register(1712, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'false'}}"); -+ register(1713, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'false'}}"); -+ register(1714, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'false'}}"); -+ register(1715, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'false'}}"); -+ register(1716, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'false'}}"); -+ register(1717, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'false'}}"); -+ register(1718, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'false'}}"); -+ register(1719, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'false'}}"); -+ register(1720, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'true'}}"); -+ register(1721, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'true'}}"); -+ register(1722, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'true'}}"); -+ register(1723, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'true'}}"); -+ register(1724, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'true'}}"); -+ register(1725, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'true'}}"); -+ register(1726, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'true'}}"); -+ register(1727, "{Name:'minecraft:oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'true'}}"); -+ register(1728, "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); -+ register(1729, "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); -+ register(1730, "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); -+ register(1731, "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); -+ register(1732, "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); -+ register(1733, "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); -+ register(1734, "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); -+ register(1735, "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:brick_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); -+ register(1744, "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); -+ register(1745, "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); -+ register(1746, "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); -+ register(1747, "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); -+ register(1748, "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); -+ register(1749, "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); -+ register(1750, "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); -+ register(1751, "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:stone_brick_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); -+ register(1760, "{Name:'minecraft:mycelium',Properties:{snowy:'false'}}", "{Name:'minecraft:mycelium',Properties:{snowy:'false'}}", "{Name:'minecraft:mycelium',Properties:{snowy:'true'}}"); -+ register(1776, "{Name:'minecraft:lily_pad'}", "{Name:'minecraft:waterlily'}"); -+ register(1792, "{Name:'minecraft:nether_bricks'}", "{Name:'minecraft:nether_brick'}"); -+ register(1808, "{Name:'minecraft:nether_brick_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:nether_brick_fence',Properties:{east:'true',north:'true',south:'true',west:'true'}}"); -+ register(1824, "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); -+ register(1825, "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); -+ register(1826, "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); -+ register(1827, "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); -+ register(1828, "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); -+ register(1829, "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); -+ register(1830, "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); -+ register(1831, "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:nether_brick_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); -+ register(1840, "{Name:'minecraft:nether_wart',Properties:{age:'0'}}", "{Name:'minecraft:nether_wart',Properties:{age:'0'}}"); -+ register(1841, "{Name:'minecraft:nether_wart',Properties:{age:'1'}}", "{Name:'minecraft:nether_wart',Properties:{age:'1'}}"); -+ register(1842, "{Name:'minecraft:nether_wart',Properties:{age:'2'}}", "{Name:'minecraft:nether_wart',Properties:{age:'2'}}"); -+ register(1843, "{Name:'minecraft:nether_wart',Properties:{age:'3'}}", "{Name:'minecraft:nether_wart',Properties:{age:'3'}}"); -+ register(1856, "{Name:'minecraft:enchanting_table'}", "{Name:'minecraft:enchanting_table'}"); -+ register(1872, "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'false',has_bottle_1:'false',has_bottle_2:'false'}}", "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'false',has_bottle_1:'false',has_bottle_2:'false'}}"); -+ register(1873, "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'true',has_bottle_1:'false',has_bottle_2:'false'}}", "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'true',has_bottle_1:'false',has_bottle_2:'false'}}"); -+ register(1874, "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'false',has_bottle_1:'true',has_bottle_2:'false'}}", "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'false',has_bottle_1:'true',has_bottle_2:'false'}}"); -+ register(1875, "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'true',has_bottle_1:'true',has_bottle_2:'false'}}", "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'true',has_bottle_1:'true',has_bottle_2:'false'}}"); -+ register(1876, "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'false',has_bottle_1:'false',has_bottle_2:'true'}}", "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'false',has_bottle_1:'false',has_bottle_2:'true'}}"); -+ register(1877, "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'true',has_bottle_1:'false',has_bottle_2:'true'}}", "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'true',has_bottle_1:'false',has_bottle_2:'true'}}"); -+ register(1878, "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'false',has_bottle_1:'true',has_bottle_2:'true'}}", "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'false',has_bottle_1:'true',has_bottle_2:'true'}}"); -+ register(1879, "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'true',has_bottle_1:'true',has_bottle_2:'true'}}", "{Name:'minecraft:brewing_stand',Properties:{has_bottle_0:'true',has_bottle_1:'true',has_bottle_2:'true'}}"); -+ register(1888, "{Name:'minecraft:cauldron',Properties:{level:'0'}}", "{Name:'minecraft:cauldron',Properties:{level:'0'}}"); -+ register(1889, "{Name:'minecraft:cauldron',Properties:{level:'1'}}", "{Name:'minecraft:cauldron',Properties:{level:'1'}}"); -+ register(1890, "{Name:'minecraft:cauldron',Properties:{level:'2'}}", "{Name:'minecraft:cauldron',Properties:{level:'2'}}"); -+ register(1891, "{Name:'minecraft:cauldron',Properties:{level:'3'}}", "{Name:'minecraft:cauldron',Properties:{level:'3'}}"); -+ register(1904, "{Name:'minecraft:end_portal'}", "{Name:'minecraft:end_portal'}"); -+ register(1920, "{Name:'minecraft:end_portal_frame',Properties:{eye:'false',facing:'south'}}", "{Name:'minecraft:end_portal_frame',Properties:{eye:'false',facing:'south'}}"); -+ register(1921, "{Name:'minecraft:end_portal_frame',Properties:{eye:'false',facing:'west'}}", "{Name:'minecraft:end_portal_frame',Properties:{eye:'false',facing:'west'}}"); -+ register(1922, "{Name:'minecraft:end_portal_frame',Properties:{eye:'false',facing:'north'}}", "{Name:'minecraft:end_portal_frame',Properties:{eye:'false',facing:'north'}}"); -+ register(1923, "{Name:'minecraft:end_portal_frame',Properties:{eye:'false',facing:'east'}}", "{Name:'minecraft:end_portal_frame',Properties:{eye:'false',facing:'east'}}"); -+ register(1924, "{Name:'minecraft:end_portal_frame',Properties:{eye:'true',facing:'south'}}", "{Name:'minecraft:end_portal_frame',Properties:{eye:'true',facing:'south'}}"); -+ register(1925, "{Name:'minecraft:end_portal_frame',Properties:{eye:'true',facing:'west'}}", "{Name:'minecraft:end_portal_frame',Properties:{eye:'true',facing:'west'}}"); -+ register(1926, "{Name:'minecraft:end_portal_frame',Properties:{eye:'true',facing:'north'}}", "{Name:'minecraft:end_portal_frame',Properties:{eye:'true',facing:'north'}}"); -+ register(1927, "{Name:'minecraft:end_portal_frame',Properties:{eye:'true',facing:'east'}}", "{Name:'minecraft:end_portal_frame',Properties:{eye:'true',facing:'east'}}"); -+ register(1936, "{Name:'minecraft:end_stone'}", "{Name:'minecraft:end_stone'}"); -+ register(1952, "{Name:'minecraft:dragon_egg'}", "{Name:'minecraft:dragon_egg'}"); -+ register(1968, "{Name:'minecraft:redstone_lamp',Properties:{lit:'false'}}", "{Name:'minecraft:redstone_lamp'}"); -+ register(1984, "{Name:'minecraft:redstone_lamp',Properties:{lit:'true'}}", "{Name:'minecraft:lit_redstone_lamp'}"); -+ register(2000, "{Name:'minecraft:oak_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_wooden_slab',Properties:{variant:'oak'}}"); -+ register(2001, "{Name:'minecraft:spruce_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_wooden_slab',Properties:{variant:'spruce'}}"); -+ register(2002, "{Name:'minecraft:birch_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_wooden_slab',Properties:{variant:'birch'}}"); -+ register(2003, "{Name:'minecraft:jungle_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_wooden_slab',Properties:{variant:'jungle'}}"); -+ register(2004, "{Name:'minecraft:acacia_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_wooden_slab',Properties:{variant:'acacia'}}"); -+ register(2005, "{Name:'minecraft:dark_oak_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_wooden_slab',Properties:{variant:'dark_oak'}}"); -+ register(2016, "{Name:'minecraft:oak_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'bottom',variant:'oak'}}"); -+ register(2017, "{Name:'minecraft:spruce_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'bottom',variant:'spruce'}}"); -+ register(2018, "{Name:'minecraft:birch_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'bottom',variant:'birch'}}"); -+ register(2019, "{Name:'minecraft:jungle_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'bottom',variant:'jungle'}}"); -+ register(2020, "{Name:'minecraft:acacia_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'bottom',variant:'acacia'}}"); -+ register(2021, "{Name:'minecraft:dark_oak_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'bottom',variant:'dark_oak'}}"); -+ register(2024, "{Name:'minecraft:oak_slab',Properties:{type:'top'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'top',variant:'oak'}}"); -+ register(2025, "{Name:'minecraft:spruce_slab',Properties:{type:'top'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'top',variant:'spruce'}}"); -+ register(2026, "{Name:'minecraft:birch_slab',Properties:{type:'top'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'top',variant:'birch'}}"); -+ register(2027, "{Name:'minecraft:jungle_slab',Properties:{type:'top'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'top',variant:'jungle'}}"); -+ register(2028, "{Name:'minecraft:acacia_slab',Properties:{type:'top'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'top',variant:'acacia'}}"); -+ register(2029, "{Name:'minecraft:dark_oak_slab',Properties:{type:'top'}}", "{Name:'minecraft:wooden_slab',Properties:{half:'top',variant:'dark_oak'}}"); -+ register(2032, "{Name:'minecraft:cocoa',Properties:{age:'0',facing:'south'}}", "{Name:'minecraft:cocoa',Properties:{age:'0',facing:'south'}}"); -+ register(2033, "{Name:'minecraft:cocoa',Properties:{age:'0',facing:'west'}}", "{Name:'minecraft:cocoa',Properties:{age:'0',facing:'west'}}"); -+ register(2034, "{Name:'minecraft:cocoa',Properties:{age:'0',facing:'north'}}", "{Name:'minecraft:cocoa',Properties:{age:'0',facing:'north'}}"); -+ register(2035, "{Name:'minecraft:cocoa',Properties:{age:'0',facing:'east'}}", "{Name:'minecraft:cocoa',Properties:{age:'0',facing:'east'}}"); -+ register(2036, "{Name:'minecraft:cocoa',Properties:{age:'1',facing:'south'}}", "{Name:'minecraft:cocoa',Properties:{age:'1',facing:'south'}}"); -+ register(2037, "{Name:'minecraft:cocoa',Properties:{age:'1',facing:'west'}}", "{Name:'minecraft:cocoa',Properties:{age:'1',facing:'west'}}"); -+ register(2038, "{Name:'minecraft:cocoa',Properties:{age:'1',facing:'north'}}", "{Name:'minecraft:cocoa',Properties:{age:'1',facing:'north'}}"); -+ register(2039, "{Name:'minecraft:cocoa',Properties:{age:'1',facing:'east'}}", "{Name:'minecraft:cocoa',Properties:{age:'1',facing:'east'}}"); -+ register(2040, "{Name:'minecraft:cocoa',Properties:{age:'2',facing:'south'}}", "{Name:'minecraft:cocoa',Properties:{age:'2',facing:'south'}}"); -+ register(2041, "{Name:'minecraft:cocoa',Properties:{age:'2',facing:'west'}}", "{Name:'minecraft:cocoa',Properties:{age:'2',facing:'west'}}"); -+ register(2042, "{Name:'minecraft:cocoa',Properties:{age:'2',facing:'north'}}", "{Name:'minecraft:cocoa',Properties:{age:'2',facing:'north'}}"); -+ register(2043, "{Name:'minecraft:cocoa',Properties:{age:'2',facing:'east'}}", "{Name:'minecraft:cocoa',Properties:{age:'2',facing:'east'}}"); -+ register(2048, "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); -+ register(2049, "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); -+ register(2050, "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); -+ register(2051, "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); -+ register(2052, "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); -+ register(2053, "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); -+ register(2054, "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); -+ register(2055, "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:sandstone_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); -+ register(2064, "{Name:'minecraft:emerald_ore'}", "{Name:'minecraft:emerald_ore'}"); -+ register(2082, "{Name:'minecraft:ender_chest',Properties:{facing:'north'}}", "{Name:'minecraft:ender_chest',Properties:{facing:'north'}}"); -+ register(2083, "{Name:'minecraft:ender_chest',Properties:{facing:'south'}}", "{Name:'minecraft:ender_chest',Properties:{facing:'south'}}"); -+ register(2084, "{Name:'minecraft:ender_chest',Properties:{facing:'west'}}", "{Name:'minecraft:ender_chest',Properties:{facing:'west'}}"); -+ register(2085, "{Name:'minecraft:ender_chest',Properties:{facing:'east'}}", "{Name:'minecraft:ender_chest',Properties:{facing:'east'}}"); -+ register(2096, "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'south',powered:'false'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'south',powered:'false'}}"); -+ register(2097, "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'west',powered:'false'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'west',powered:'false'}}"); -+ register(2098, "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'north',powered:'false'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'north',powered:'false'}}"); -+ register(2099, "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'east',powered:'false'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'east',powered:'false'}}"); -+ register(2100, "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'south',powered:'false'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'south',powered:'false'}}"); -+ register(2101, "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'west',powered:'false'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'west',powered:'false'}}"); -+ register(2102, "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'north',powered:'false'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'north',powered:'false'}}"); -+ register(2103, "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'east',powered:'false'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'east',powered:'false'}}"); -+ register(2104, "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'south',powered:'true'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'south',powered:'true'}}"); -+ register(2105, "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'west',powered:'true'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'west',powered:'true'}}"); -+ register(2106, "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'north',powered:'true'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'north',powered:'true'}}"); -+ register(2107, "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'east',powered:'true'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'false',facing:'east',powered:'true'}}"); -+ register(2108, "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'south',powered:'true'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'south',powered:'true'}}"); -+ register(2109, "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'west',powered:'true'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'west',powered:'true'}}"); -+ register(2110, "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'north',powered:'true'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'north',powered:'true'}}"); -+ register(2111, "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'east',powered:'true'}}", "{Name:'minecraft:tripwire_hook',Properties:{attached:'true',facing:'east',powered:'true'}}"); -+ register(2112, "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'true',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'true',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'true',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'true',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'false',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'false',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'false',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'true',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'true',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'true',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'true',powered:'false',south:'true',west:'true'}}"); -+ register(2113, "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'true',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'true',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'true',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'true',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'false',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'false',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'false',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'true',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'true',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'true',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'true',north:'true',powered:'true',south:'true',west:'true'}}"); -+ register(2114, "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'false',south:'false',west:'false'}}"); -+ register(2115, "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'false',east:'false',north:'false',powered:'true',south:'false',west:'false'}}"); -+ register(2116, "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'true',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'true',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'true',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'true',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'false',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'false',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'false',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'true',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'true',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'true',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'true',powered:'false',south:'true',west:'true'}}"); -+ register(2117, "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'true',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'true',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'true',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'true',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'false',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'false',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'false',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'true',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'true',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'true',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'true',north:'true',powered:'true',south:'true',west:'true'}}"); -+ register(2118, "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'false',south:'false',west:'false'}}"); -+ register(2119, "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'false',east:'false',north:'false',powered:'true',south:'false',west:'false'}}"); -+ register(2120, "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'true',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'true',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'true',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'true',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'false',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'false',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'false',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'true',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'true',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'true',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'true',powered:'false',south:'true',west:'true'}}"); -+ register(2121, "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'true',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'true',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'true',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'true',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'false',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'false',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'false',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'true',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'true',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'true',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'true',north:'true',powered:'true',south:'true',west:'true'}}"); -+ register(2122, "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'false',south:'false',west:'false'}}"); -+ register(2123, "{Name:'minecraft:tripwire',Properties:{attached:'false',disarmed:'true',east:'false',north:'false',powered:'true',south:'false',west:'false'}}"); -+ register(2124, "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'true',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'true',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'true',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'true',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'false',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'false',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'false',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'false',powered:'false',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'true',powered:'false',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'true',powered:'false',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'true',powered:'false',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'true',powered:'false',south:'true',west:'true'}}"); -+ register(2125, "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'true',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'true',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'true',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'true',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'false',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'false',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'false',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'false',powered:'true',south:'true',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'true',powered:'true',south:'false',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'true',powered:'true',south:'false',west:'true'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'true',powered:'true',south:'true',west:'false'}}", "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'true',north:'true',powered:'true',south:'true',west:'true'}}"); -+ register(2126, "{Name:'minecraft:tripwire',Properties:{attached:'true',disarmed:'true',east:'false',north:'false',powered:'false',south:'false',west:'false'}}"); -+ register(2128, "{Name:'minecraft:emerald_block'}", "{Name:'minecraft:emerald_block'}"); -+ register(2144, "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); -+ register(2145, "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); -+ register(2146, "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); -+ register(2147, "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); -+ register(2148, "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); -+ register(2149, "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); -+ register(2150, "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); -+ register(2151, "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:spruce_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); -+ register(2160, "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); -+ register(2161, "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); -+ register(2162, "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); -+ register(2163, "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); -+ register(2164, "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); -+ register(2165, "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); -+ register(2166, "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); -+ register(2167, "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:birch_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); -+ register(2176, "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); -+ register(2177, "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); -+ register(2178, "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); -+ register(2179, "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); -+ register(2180, "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); -+ register(2181, "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); -+ register(2182, "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); -+ register(2183, "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:jungle_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); -+ register(2192, "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'down'}}", "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'down'}}"); -+ register(2193, "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'up'}}", "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'up'}}"); -+ register(2194, "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'north'}}", "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'north'}}"); -+ register(2195, "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'south'}}", "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'south'}}"); -+ register(2196, "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'west'}}", "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'west'}}"); -+ register(2197, "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'east'}}", "{Name:'minecraft:command_block',Properties:{conditional:'false',facing:'east'}}"); -+ register(2200, "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'down'}}", "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'down'}}"); -+ register(2201, "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'up'}}", "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'up'}}"); -+ register(2202, "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'north'}}", "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'north'}}"); -+ register(2203, "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'south'}}", "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'south'}}"); -+ register(2204, "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'west'}}", "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'west'}}"); -+ register(2205, "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'east'}}", "{Name:'minecraft:command_block',Properties:{conditional:'true',facing:'east'}}"); -+ register(2208, "{Name:'minecraft:beacon'}", "{Name:'minecraft:beacon'}"); -+ register(2224, "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'false',up:'false',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'false',up:'false',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'false',up:'true',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'false',up:'true',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'true',up:'false',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'true',up:'false',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'true',up:'true',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'true',up:'true',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'false',up:'false',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'false',up:'false',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'false',up:'true',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'false',up:'true',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'true',up:'false',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'true',up:'false',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'true',up:'true',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'true',up:'true',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'false',up:'false',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'false',up:'false',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'false',up:'true',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'false',up:'true',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'true',up:'false',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'true',up:'false',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'true',up:'true',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'true',up:'true',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'false',up:'false',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'false',up:'false',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'false',up:'true',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'false',up:'true',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'true',up:'false',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'true',up:'false',variant:'cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'true',up:'true',variant:'cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'true',up:'true',variant:'cobblestone',west:'true'}}"); -+ register(2225, "{Name:'minecraft:mossy_cobblestone_wall',Properties:{east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'false',up:'false',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'false',up:'false',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'false',up:'true',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'false',up:'true',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'true',up:'false',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'true',up:'false',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'true',up:'true',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'false',south:'true',up:'true',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'false',up:'false',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'false',up:'false',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'false',up:'true',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'false',up:'true',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'true',up:'false',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'true',up:'false',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'true',up:'true',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'false',north:'true',south:'true',up:'true',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'false',up:'false',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'false',up:'false',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'false',up:'true',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'false',up:'true',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'true',up:'false',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'true',up:'false',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'true',up:'true',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'false',south:'true',up:'true',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'false',up:'false',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'false',up:'false',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'false',up:'true',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'false',up:'true',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'true',up:'false',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'true',up:'false',variant:'mossy_cobblestone',west:'true'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'true',up:'true',variant:'mossy_cobblestone',west:'false'}}", "{Name:'minecraft:cobblestone_wall',Properties:{east:'true',north:'true',south:'true',up:'true',variant:'mossy_cobblestone',west:'true'}}"); -+ // There are a few changes made to flower pot here, notably handling how legacy data is handled. -+ // The TE itself should contain the target item and from there the proper state can be determined. However, there are -+ // blocks that do not contain a TE. So we need to make sure there is a default to fall on. -+ // I simply followed the legacy handling from BlockFlowerPot from 1.8.8 to find what legacy data mapped to what. -+ // It's better than defaulting everything to a cactus. -+ register(2240, "{Name:'minecraft:flower_pot'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'0'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'0'}}"); -+ register(2241, "{Name:'minecraft:potted_poppy'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'1'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'1'}}"); -+ register(2242, "{Name:'minecraft:potted_dandelion'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'2'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'2'}}"); -+ register(2243, "{Name:'minecraft:potted_oak_sapling'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'3'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'3'}}"); -+ register(2244, "{Name:'minecraft:potted_spruce_sapling'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'4'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'4'}}"); -+ register(2245, "{Name:'minecraft:potted_birch_sapling'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'5'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'5'}}"); -+ register(2246, "{Name:'minecraft:potted_jungle_sapling'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'6'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'6'}}"); -+ register(2247, "{Name:'minecraft:potted_red_mushroom'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'7'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'7'}}"); -+ register(2248, "{Name:'minecraft:potted_brown_mushroom'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'8'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'8'}}"); -+ register(2249, "{Name:'minecraft:potted_cactus'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'9'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'9'}}"); -+ register(2250, "{Name:'minecraft:potted_dead_bush'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'10'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'10'}}"); -+ register(2251, "{Name:'minecraft:potted_fern'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'11'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'11'}}"); -+ register(2252, "{Name:'minecraft:potted_acacia_sapling'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'12'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'12'}}"); -+ register(2253, "{Name:'minecraft:potted_dark_oak_sapling'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'13'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'13'}}"); -+ register(2254, "{Name:'minecraft:flower_pot'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'14'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'14'}}"); -+ register(2255, "{Name:'minecraft:flower_pot'}", "{Name:'minecraft:flower_pot',Properties:{contents:'acacia_sapling',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'allium',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'birch_sapling',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'blue_orchid',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'cactus',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dandelion',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dark_oak_sapling',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'dead_bush',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'empty',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'fern',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'houstonia',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'jungle_sapling',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_brown',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'mushroom_red',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oak_sapling',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'orange_tulip',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'oxeye_daisy',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'pink_tulip',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'red_tulip',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'rose',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'spruce_sapling',legacy_data:'15'}}", "{Name:'minecraft:flower_pot',Properties:{contents:'white_tulip',legacy_data:'15'}}"); -+ register(2256, "{Name:'minecraft:carrots',Properties:{age:'0'}}", "{Name:'minecraft:carrots',Properties:{age:'0'}}"); -+ register(2257, "{Name:'minecraft:carrots',Properties:{age:'1'}}", "{Name:'minecraft:carrots',Properties:{age:'1'}}"); -+ register(2258, "{Name:'minecraft:carrots',Properties:{age:'2'}}", "{Name:'minecraft:carrots',Properties:{age:'2'}}"); -+ register(2259, "{Name:'minecraft:carrots',Properties:{age:'3'}}", "{Name:'minecraft:carrots',Properties:{age:'3'}}"); -+ register(2260, "{Name:'minecraft:carrots',Properties:{age:'4'}}", "{Name:'minecraft:carrots',Properties:{age:'4'}}"); -+ register(2261, "{Name:'minecraft:carrots',Properties:{age:'5'}}", "{Name:'minecraft:carrots',Properties:{age:'5'}}"); -+ register(2262, "{Name:'minecraft:carrots',Properties:{age:'6'}}", "{Name:'minecraft:carrots',Properties:{age:'6'}}"); -+ register(2263, "{Name:'minecraft:carrots',Properties:{age:'7'}}", "{Name:'minecraft:carrots',Properties:{age:'7'}}"); -+ register(2272, "{Name:'minecraft:potatoes',Properties:{age:'0'}}", "{Name:'minecraft:potatoes',Properties:{age:'0'}}"); -+ register(2273, "{Name:'minecraft:potatoes',Properties:{age:'1'}}", "{Name:'minecraft:potatoes',Properties:{age:'1'}}"); -+ register(2274, "{Name:'minecraft:potatoes',Properties:{age:'2'}}", "{Name:'minecraft:potatoes',Properties:{age:'2'}}"); -+ register(2275, "{Name:'minecraft:potatoes',Properties:{age:'3'}}", "{Name:'minecraft:potatoes',Properties:{age:'3'}}"); -+ register(2276, "{Name:'minecraft:potatoes',Properties:{age:'4'}}", "{Name:'minecraft:potatoes',Properties:{age:'4'}}"); -+ register(2277, "{Name:'minecraft:potatoes',Properties:{age:'5'}}", "{Name:'minecraft:potatoes',Properties:{age:'5'}}"); -+ register(2278, "{Name:'minecraft:potatoes',Properties:{age:'6'}}", "{Name:'minecraft:potatoes',Properties:{age:'6'}}"); -+ register(2279, "{Name:'minecraft:potatoes',Properties:{age:'7'}}", "{Name:'minecraft:potatoes',Properties:{age:'7'}}"); -+ register(2288, "{Name:'minecraft:oak_button',Properties:{face:'ceiling',facing:'north',powered:'false'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'down',powered:'false'}}"); -+ register(2289, "{Name:'minecraft:oak_button',Properties:{face:'wall',facing:'east',powered:'false'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'east',powered:'false'}}"); -+ register(2290, "{Name:'minecraft:oak_button',Properties:{face:'wall',facing:'west',powered:'false'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'west',powered:'false'}}"); -+ register(2291, "{Name:'minecraft:oak_button',Properties:{face:'wall',facing:'south',powered:'false'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'south',powered:'false'}}"); -+ register(2292, "{Name:'minecraft:oak_button',Properties:{face:'wall',facing:'north',powered:'false'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'north',powered:'false'}}"); -+ register(2293, "{Name:'minecraft:oak_button',Properties:{face:'floor',facing:'north',powered:'false'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'up',powered:'false'}}"); -+ register(2296, "{Name:'minecraft:oak_button',Properties:{face:'ceiling',facing:'north',powered:'true'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'down',powered:'true'}}"); -+ register(2297, "{Name:'minecraft:oak_button',Properties:{face:'wall',facing:'east',powered:'true'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'east',powered:'true'}}"); -+ register(2298, "{Name:'minecraft:oak_button',Properties:{face:'wall',facing:'west',powered:'true'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'west',powered:'true'}}"); -+ register(2299, "{Name:'minecraft:oak_button',Properties:{face:'wall',facing:'south',powered:'true'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'south',powered:'true'}}"); -+ register(2300, "{Name:'minecraft:oak_button',Properties:{face:'wall',facing:'north',powered:'true'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'north',powered:'true'}}"); -+ register(2301, "{Name:'minecraft:oak_button',Properties:{face:'floor',facing:'north',powered:'true'}}", "{Name:'minecraft:wooden_button',Properties:{facing:'up',powered:'true'}}"); -+ register(2304, "{Name:'%%FILTER_ME%%',Properties:{facing:'down',nodrop:'false'}}", "{Name:'minecraft:skull',Properties:{facing:'down',nodrop:'false'}}"); -+ register(2305, "{Name:'%%FILTER_ME%%',Properties:{facing:'up',nodrop:'false'}}", "{Name:'minecraft:skull',Properties:{facing:'up',nodrop:'false'}}"); -+ register(2306, "{Name:'%%FILTER_ME%%',Properties:{facing:'north',nodrop:'false'}}", "{Name:'minecraft:skull',Properties:{facing:'north',nodrop:'false'}}"); -+ register(2307, "{Name:'%%FILTER_ME%%',Properties:{facing:'south',nodrop:'false'}}", "{Name:'minecraft:skull',Properties:{facing:'south',nodrop:'false'}}"); -+ register(2308, "{Name:'%%FILTER_ME%%',Properties:{facing:'west',nodrop:'false'}}", "{Name:'minecraft:skull',Properties:{facing:'west',nodrop:'false'}}"); -+ register(2309, "{Name:'%%FILTER_ME%%',Properties:{facing:'east',nodrop:'false'}}", "{Name:'minecraft:skull',Properties:{facing:'east',nodrop:'false'}}"); -+ register(2312, "{Name:'%%FILTER_ME%%',Properties:{facing:'down',nodrop:'true'}}", "{Name:'minecraft:skull',Properties:{facing:'down',nodrop:'true'}}"); -+ register(2313, "{Name:'%%FILTER_ME%%',Properties:{facing:'up',nodrop:'true'}}", "{Name:'minecraft:skull',Properties:{facing:'up',nodrop:'true'}}"); -+ register(2314, "{Name:'%%FILTER_ME%%',Properties:{facing:'north',nodrop:'true'}}", "{Name:'minecraft:skull',Properties:{facing:'north',nodrop:'true'}}"); -+ register(2315, "{Name:'%%FILTER_ME%%',Properties:{facing:'south',nodrop:'true'}}", "{Name:'minecraft:skull',Properties:{facing:'south',nodrop:'true'}}"); -+ register(2316, "{Name:'%%FILTER_ME%%',Properties:{facing:'west',nodrop:'true'}}", "{Name:'minecraft:skull',Properties:{facing:'west',nodrop:'true'}}"); -+ register(2317, "{Name:'%%FILTER_ME%%',Properties:{facing:'east',nodrop:'true'}}", "{Name:'minecraft:skull',Properties:{facing:'east',nodrop:'true'}}"); -+ register(2320, "{Name:'minecraft:anvil',Properties:{facing:'south'}}", "{Name:'minecraft:anvil',Properties:{damage:'0',facing:'south'}}"); -+ register(2321, "{Name:'minecraft:anvil',Properties:{facing:'west'}}", "{Name:'minecraft:anvil',Properties:{damage:'0',facing:'west'}}"); -+ register(2322, "{Name:'minecraft:anvil',Properties:{facing:'north'}}", "{Name:'minecraft:anvil',Properties:{damage:'0',facing:'north'}}"); -+ register(2323, "{Name:'minecraft:anvil',Properties:{facing:'east'}}", "{Name:'minecraft:anvil',Properties:{damage:'0',facing:'east'}}"); -+ register(2324, "{Name:'minecraft:chipped_anvil',Properties:{facing:'south'}}", "{Name:'minecraft:anvil',Properties:{damage:'1',facing:'south'}}"); -+ register(2325, "{Name:'minecraft:chipped_anvil',Properties:{facing:'west'}}", "{Name:'minecraft:anvil',Properties:{damage:'1',facing:'west'}}"); -+ register(2326, "{Name:'minecraft:chipped_anvil',Properties:{facing:'north'}}", "{Name:'minecraft:anvil',Properties:{damage:'1',facing:'north'}}"); -+ register(2327, "{Name:'minecraft:chipped_anvil',Properties:{facing:'east'}}", "{Name:'minecraft:anvil',Properties:{damage:'1',facing:'east'}}"); -+ register(2328, "{Name:'minecraft:damaged_anvil',Properties:{facing:'south'}}", "{Name:'minecraft:anvil',Properties:{damage:'2',facing:'south'}}"); -+ register(2329, "{Name:'minecraft:damaged_anvil',Properties:{facing:'west'}}", "{Name:'minecraft:anvil',Properties:{damage:'2',facing:'west'}}"); -+ register(2330, "{Name:'minecraft:damaged_anvil',Properties:{facing:'north'}}", "{Name:'minecraft:anvil',Properties:{damage:'2',facing:'north'}}"); -+ register(2331, "{Name:'minecraft:damaged_anvil',Properties:{facing:'east'}}", "{Name:'minecraft:anvil',Properties:{damage:'2',facing:'east'}}"); -+ register(2338, "{Name:'minecraft:trapped_chest',Properties:{facing:'north',type:'single'}}", "{Name:'minecraft:trapped_chest',Properties:{facing:'north'}}"); -+ register(2339, "{Name:'minecraft:trapped_chest',Properties:{facing:'south',type:'single'}}", "{Name:'minecraft:trapped_chest',Properties:{facing:'south'}}"); -+ register(2340, "{Name:'minecraft:trapped_chest',Properties:{facing:'west',type:'single'}}", "{Name:'minecraft:trapped_chest',Properties:{facing:'west'}}"); -+ register(2341, "{Name:'minecraft:trapped_chest',Properties:{facing:'east',type:'single'}}", "{Name:'minecraft:trapped_chest',Properties:{facing:'east'}}"); -+ register(2352, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'0'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'0'}}"); -+ register(2353, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'1'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'1'}}"); -+ register(2354, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'2'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'2'}}"); -+ register(2355, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'3'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'3'}}"); -+ register(2356, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'4'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'4'}}"); -+ register(2357, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'5'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'5'}}"); -+ register(2358, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'6'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'6'}}"); -+ register(2359, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'7'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'7'}}"); -+ register(2360, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'8'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'8'}}"); -+ register(2361, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'9'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'9'}}"); -+ register(2362, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'10'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'10'}}"); -+ register(2363, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'11'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'11'}}"); -+ register(2364, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'12'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'12'}}"); -+ register(2365, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'13'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'13'}}"); -+ register(2366, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'14'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'14'}}"); -+ register(2367, "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'15'}}", "{Name:'minecraft:light_weighted_pressure_plate',Properties:{power:'15'}}"); -+ register(2368, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'0'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'0'}}"); -+ register(2369, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'1'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'1'}}"); -+ register(2370, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'2'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'2'}}"); -+ register(2371, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'3'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'3'}}"); -+ register(2372, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'4'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'4'}}"); -+ register(2373, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'5'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'5'}}"); -+ register(2374, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'6'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'6'}}"); -+ register(2375, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'7'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'7'}}"); -+ register(2376, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'8'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'8'}}"); -+ register(2377, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'9'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'9'}}"); -+ register(2378, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'10'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'10'}}"); -+ register(2379, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'11'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'11'}}"); -+ register(2380, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'12'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'12'}}"); -+ register(2381, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'13'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'13'}}"); -+ register(2382, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'14'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'14'}}"); -+ register(2383, "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'15'}}", "{Name:'minecraft:heavy_weighted_pressure_plate',Properties:{power:'15'}}"); -+ register(2384, "{Name:'minecraft:comparator',Properties:{facing:'south',mode:'compare',powered:'false'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'south',mode:'compare',powered:'false'}}"); -+ register(2385, "{Name:'minecraft:comparator',Properties:{facing:'west',mode:'compare',powered:'false'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'west',mode:'compare',powered:'false'}}"); -+ register(2386, "{Name:'minecraft:comparator',Properties:{facing:'north',mode:'compare',powered:'false'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'north',mode:'compare',powered:'false'}}"); -+ register(2387, "{Name:'minecraft:comparator',Properties:{facing:'east',mode:'compare',powered:'false'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'east',mode:'compare',powered:'false'}}"); -+ register(2388, "{Name:'minecraft:comparator',Properties:{facing:'south',mode:'subtract',powered:'false'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'south',mode:'subtract',powered:'false'}}"); -+ register(2389, "{Name:'minecraft:comparator',Properties:{facing:'west',mode:'subtract',powered:'false'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'west',mode:'subtract',powered:'false'}}"); -+ register(2390, "{Name:'minecraft:comparator',Properties:{facing:'north',mode:'subtract',powered:'false'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'north',mode:'subtract',powered:'false'}}"); -+ register(2391, "{Name:'minecraft:comparator',Properties:{facing:'east',mode:'subtract',powered:'false'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'east',mode:'subtract',powered:'false'}}"); -+ register(2392, "{Name:'minecraft:comparator',Properties:{facing:'south',mode:'compare',powered:'true'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'south',mode:'compare',powered:'true'}}"); -+ register(2393, "{Name:'minecraft:comparator',Properties:{facing:'west',mode:'compare',powered:'true'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'west',mode:'compare',powered:'true'}}"); -+ register(2394, "{Name:'minecraft:comparator',Properties:{facing:'north',mode:'compare',powered:'true'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'north',mode:'compare',powered:'true'}}"); -+ register(2395, "{Name:'minecraft:comparator',Properties:{facing:'east',mode:'compare',powered:'true'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'east',mode:'compare',powered:'true'}}"); -+ register(2396, "{Name:'minecraft:comparator',Properties:{facing:'south',mode:'subtract',powered:'true'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'south',mode:'subtract',powered:'true'}}"); -+ register(2397, "{Name:'minecraft:comparator',Properties:{facing:'west',mode:'subtract',powered:'true'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'west',mode:'subtract',powered:'true'}}"); -+ register(2398, "{Name:'minecraft:comparator',Properties:{facing:'north',mode:'subtract',powered:'true'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'north',mode:'subtract',powered:'true'}}"); -+ register(2399, "{Name:'minecraft:comparator',Properties:{facing:'east',mode:'subtract',powered:'true'}}", "{Name:'minecraft:unpowered_comparator',Properties:{facing:'east',mode:'subtract',powered:'true'}}"); -+ register(2400, "{Name:'minecraft:comparator',Properties:{facing:'south',mode:'compare',powered:'false'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'south',mode:'compare',powered:'false'}}"); -+ register(2401, "{Name:'minecraft:comparator',Properties:{facing:'west',mode:'compare',powered:'false'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'west',mode:'compare',powered:'false'}}"); -+ register(2402, "{Name:'minecraft:comparator',Properties:{facing:'north',mode:'compare',powered:'false'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'north',mode:'compare',powered:'false'}}"); -+ register(2403, "{Name:'minecraft:comparator',Properties:{facing:'east',mode:'compare',powered:'false'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'east',mode:'compare',powered:'false'}}"); -+ register(2404, "{Name:'minecraft:comparator',Properties:{facing:'south',mode:'subtract',powered:'false'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'south',mode:'subtract',powered:'false'}}"); -+ register(2405, "{Name:'minecraft:comparator',Properties:{facing:'west',mode:'subtract',powered:'false'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'west',mode:'subtract',powered:'false'}}"); -+ register(2406, "{Name:'minecraft:comparator',Properties:{facing:'north',mode:'subtract',powered:'false'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'north',mode:'subtract',powered:'false'}}"); -+ register(2407, "{Name:'minecraft:comparator',Properties:{facing:'east',mode:'subtract',powered:'false'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'east',mode:'subtract',powered:'false'}}"); -+ register(2408, "{Name:'minecraft:comparator',Properties:{facing:'south',mode:'compare',powered:'true'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'south',mode:'compare',powered:'true'}}"); -+ register(2409, "{Name:'minecraft:comparator',Properties:{facing:'west',mode:'compare',powered:'true'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'west',mode:'compare',powered:'true'}}"); -+ register(2410, "{Name:'minecraft:comparator',Properties:{facing:'north',mode:'compare',powered:'true'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'north',mode:'compare',powered:'true'}}"); -+ register(2411, "{Name:'minecraft:comparator',Properties:{facing:'east',mode:'compare',powered:'true'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'east',mode:'compare',powered:'true'}}"); -+ register(2412, "{Name:'minecraft:comparator',Properties:{facing:'south',mode:'subtract',powered:'true'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'south',mode:'subtract',powered:'true'}}"); -+ register(2413, "{Name:'minecraft:comparator',Properties:{facing:'west',mode:'subtract',powered:'true'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'west',mode:'subtract',powered:'true'}}"); -+ register(2414, "{Name:'minecraft:comparator',Properties:{facing:'north',mode:'subtract',powered:'true'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'north',mode:'subtract',powered:'true'}}"); -+ register(2415, "{Name:'minecraft:comparator',Properties:{facing:'east',mode:'subtract',powered:'true'}}", "{Name:'minecraft:powered_comparator',Properties:{facing:'east',mode:'subtract',powered:'true'}}"); -+ register(2416, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'0'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'0'}}"); -+ register(2417, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'1'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'1'}}"); -+ register(2418, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'2'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'2'}}"); -+ register(2419, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'3'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'3'}}"); -+ register(2420, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'4'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'4'}}"); -+ register(2421, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'5'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'5'}}"); -+ register(2422, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'6'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'6'}}"); -+ register(2423, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'7'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'7'}}"); -+ register(2424, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'8'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'8'}}"); -+ register(2425, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'9'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'9'}}"); -+ register(2426, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'10'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'10'}}"); -+ register(2427, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'11'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'11'}}"); -+ register(2428, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'12'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'12'}}"); -+ register(2429, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'13'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'13'}}"); -+ register(2430, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'14'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'14'}}"); -+ register(2431, "{Name:'minecraft:daylight_detector',Properties:{inverted:'false',power:'15'}}", "{Name:'minecraft:daylight_detector',Properties:{power:'15'}}"); -+ register(2432, "{Name:'minecraft:redstone_block'}", "{Name:'minecraft:redstone_block'}"); -+ register(2448, "{Name:'minecraft:nether_quartz_ore'}", "{Name:'minecraft:quartz_ore'}"); -+ register(2464, "{Name:'minecraft:hopper',Properties:{enabled:'true',facing:'down'}}", "{Name:'minecraft:hopper',Properties:{enabled:'true',facing:'down'}}"); -+ register(2466, "{Name:'minecraft:hopper',Properties:{enabled:'true',facing:'north'}}", "{Name:'minecraft:hopper',Properties:{enabled:'true',facing:'north'}}"); -+ register(2467, "{Name:'minecraft:hopper',Properties:{enabled:'true',facing:'south'}}", "{Name:'minecraft:hopper',Properties:{enabled:'true',facing:'south'}}"); -+ register(2468, "{Name:'minecraft:hopper',Properties:{enabled:'true',facing:'west'}}", "{Name:'minecraft:hopper',Properties:{enabled:'true',facing:'west'}}"); -+ register(2469, "{Name:'minecraft:hopper',Properties:{enabled:'true',facing:'east'}}", "{Name:'minecraft:hopper',Properties:{enabled:'true',facing:'east'}}"); -+ register(2472, "{Name:'minecraft:hopper',Properties:{enabled:'false',facing:'down'}}", "{Name:'minecraft:hopper',Properties:{enabled:'false',facing:'down'}}"); -+ register(2474, "{Name:'minecraft:hopper',Properties:{enabled:'false',facing:'north'}}", "{Name:'minecraft:hopper',Properties:{enabled:'false',facing:'north'}}"); -+ register(2475, "{Name:'minecraft:hopper',Properties:{enabled:'false',facing:'south'}}", "{Name:'minecraft:hopper',Properties:{enabled:'false',facing:'south'}}"); -+ register(2476, "{Name:'minecraft:hopper',Properties:{enabled:'false',facing:'west'}}", "{Name:'minecraft:hopper',Properties:{enabled:'false',facing:'west'}}"); -+ register(2477, "{Name:'minecraft:hopper',Properties:{enabled:'false',facing:'east'}}", "{Name:'minecraft:hopper',Properties:{enabled:'false',facing:'east'}}"); -+ register(2480, "{Name:'minecraft:quartz_block'}", "{Name:'minecraft:quartz_block',Properties:{variant:'default'}}"); -+ register(2481, "{Name:'minecraft:chiseled_quartz_block'}", "{Name:'minecraft:quartz_block',Properties:{variant:'chiseled'}}"); -+ register(2482, "{Name:'minecraft:quartz_pillar',Properties:{axis:'y'}}", "{Name:'minecraft:quartz_block',Properties:{variant:'lines_y'}}"); -+ register(2483, "{Name:'minecraft:quartz_pillar',Properties:{axis:'x'}}", "{Name:'minecraft:quartz_block',Properties:{variant:'lines_x'}}"); -+ register(2484, "{Name:'minecraft:quartz_pillar',Properties:{axis:'z'}}", "{Name:'minecraft:quartz_block',Properties:{variant:'lines_z'}}"); -+ register(2496, "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); -+ register(2497, "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); -+ register(2498, "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); -+ register(2499, "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); -+ register(2500, "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); -+ register(2501, "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); -+ register(2502, "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); -+ register(2503, "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:quartz_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); -+ register(2512, "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'north_south'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'north_south'}}"); -+ register(2513, "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'east_west'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'east_west'}}"); -+ register(2514, "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'ascending_east'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'ascending_east'}}"); -+ register(2515, "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'ascending_west'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'ascending_west'}}"); -+ register(2516, "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'ascending_north'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'ascending_north'}}"); -+ register(2517, "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'ascending_south'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'false',shape:'ascending_south'}}"); -+ register(2520, "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'north_south'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'north_south'}}"); -+ register(2521, "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'east_west'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'east_west'}}"); -+ register(2522, "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'ascending_east'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'ascending_east'}}"); -+ register(2523, "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'ascending_west'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'ascending_west'}}"); -+ register(2524, "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'ascending_north'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'ascending_north'}}"); -+ register(2525, "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'ascending_south'}}", "{Name:'minecraft:activator_rail',Properties:{powered:'true',shape:'ascending_south'}}"); -+ register(2528, "{Name:'minecraft:dropper',Properties:{facing:'down',triggered:'false'}}", "{Name:'minecraft:dropper',Properties:{facing:'down',triggered:'false'}}"); -+ register(2529, "{Name:'minecraft:dropper',Properties:{facing:'up',triggered:'false'}}", "{Name:'minecraft:dropper',Properties:{facing:'up',triggered:'false'}}"); -+ register(2530, "{Name:'minecraft:dropper',Properties:{facing:'north',triggered:'false'}}", "{Name:'minecraft:dropper',Properties:{facing:'north',triggered:'false'}}"); -+ register(2531, "{Name:'minecraft:dropper',Properties:{facing:'south',triggered:'false'}}", "{Name:'minecraft:dropper',Properties:{facing:'south',triggered:'false'}}"); -+ register(2532, "{Name:'minecraft:dropper',Properties:{facing:'west',triggered:'false'}}", "{Name:'minecraft:dropper',Properties:{facing:'west',triggered:'false'}}"); -+ register(2533, "{Name:'minecraft:dropper',Properties:{facing:'east',triggered:'false'}}", "{Name:'minecraft:dropper',Properties:{facing:'east',triggered:'false'}}"); -+ register(2536, "{Name:'minecraft:dropper',Properties:{facing:'down',triggered:'true'}}", "{Name:'minecraft:dropper',Properties:{facing:'down',triggered:'true'}}"); -+ register(2537, "{Name:'minecraft:dropper',Properties:{facing:'up',triggered:'true'}}", "{Name:'minecraft:dropper',Properties:{facing:'up',triggered:'true'}}"); -+ register(2538, "{Name:'minecraft:dropper',Properties:{facing:'north',triggered:'true'}}", "{Name:'minecraft:dropper',Properties:{facing:'north',triggered:'true'}}"); -+ register(2539, "{Name:'minecraft:dropper',Properties:{facing:'south',triggered:'true'}}", "{Name:'minecraft:dropper',Properties:{facing:'south',triggered:'true'}}"); -+ register(2540, "{Name:'minecraft:dropper',Properties:{facing:'west',triggered:'true'}}", "{Name:'minecraft:dropper',Properties:{facing:'west',triggered:'true'}}"); -+ register(2541, "{Name:'minecraft:dropper',Properties:{facing:'east',triggered:'true'}}", "{Name:'minecraft:dropper',Properties:{facing:'east',triggered:'true'}}"); -+ register(2544, "{Name:'minecraft:white_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'white'}}"); -+ register(2545, "{Name:'minecraft:orange_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'orange'}}"); -+ register(2546, "{Name:'minecraft:magenta_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'magenta'}}"); -+ register(2547, "{Name:'minecraft:light_blue_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'light_blue'}}"); -+ register(2548, "{Name:'minecraft:yellow_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'yellow'}}"); -+ register(2549, "{Name:'minecraft:lime_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'lime'}}"); -+ register(2550, "{Name:'minecraft:pink_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'pink'}}"); -+ register(2551, "{Name:'minecraft:gray_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'gray'}}"); -+ register(2552, "{Name:'minecraft:light_gray_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'silver'}}"); -+ register(2553, "{Name:'minecraft:cyan_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'cyan'}}"); -+ register(2554, "{Name:'minecraft:purple_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'purple'}}"); -+ register(2555, "{Name:'minecraft:blue_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'blue'}}"); -+ register(2556, "{Name:'minecraft:brown_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'brown'}}"); -+ register(2557, "{Name:'minecraft:green_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'green'}}"); -+ register(2558, "{Name:'minecraft:red_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'red'}}"); -+ register(2559, "{Name:'minecraft:black_terracotta'}", "{Name:'minecraft:stained_hardened_clay',Properties:{color:'black'}}"); -+ register(2560, "{Name:'minecraft:white_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'white',east:'true',north:'true',south:'true',west:'true'}}"); -+ register(2561, "{Name:'minecraft:orange_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'orange',east:'true',north:'true',south:'true',west:'true'}}"); -+ register(2562, "{Name:'minecraft:magenta_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'magenta',east:'true',north:'true',south:'true',west:'true'}}"); -+ register(2563, "{Name:'minecraft:light_blue_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'light_blue',east:'true',north:'true',south:'true',west:'true'}}"); -+ register(2564, "{Name:'minecraft:yellow_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'yellow',east:'true',north:'true',south:'true',west:'true'}}"); -+ register(2565, "{Name:'minecraft:lime_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'lime',east:'true',north:'true',south:'true',west:'true'}}"); -+ register(2566, "{Name:'minecraft:pink_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'pink',east:'true',north:'true',south:'true',west:'true'}}"); -+ register(2567, "{Name:'minecraft:gray_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'gray',east:'true',north:'true',south:'true',west:'true'}}"); -+ register(2568, "{Name:'minecraft:light_gray_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'silver',east:'true',north:'true',south:'true',west:'true'}}"); -+ register(2569, "{Name:'minecraft:cyan_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'cyan',east:'true',north:'true',south:'true',west:'true'}}"); -+ register(2570, "{Name:'minecraft:purple_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'purple',east:'true',north:'true',south:'true',west:'true'}}"); -+ register(2571, "{Name:'minecraft:blue_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'blue',east:'true',north:'true',south:'true',west:'true'}}"); -+ register(2572, "{Name:'minecraft:brown_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'brown',east:'true',north:'true',south:'true',west:'true'}}"); -+ register(2573, "{Name:'minecraft:green_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'green',east:'true',north:'true',south:'true',west:'true'}}"); -+ register(2574, "{Name:'minecraft:red_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'red',east:'true',north:'true',south:'true',west:'true'}}"); -+ register(2575, "{Name:'minecraft:black_stained_glass_pane',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:stained_glass_pane',Properties:{color:'black',east:'true',north:'true',south:'true',west:'true'}}"); -+ register(2576, "{Name:'minecraft:acacia_leaves',Properties:{check_decay:'false',decayable:'true'}}", "{Name:'minecraft:leaves2',Properties:{check_decay:'false',decayable:'true',variant:'acacia'}}"); -+ register(2577, "{Name:'minecraft:dark_oak_leaves',Properties:{check_decay:'false',decayable:'true'}}", "{Name:'minecraft:leaves2',Properties:{check_decay:'false',decayable:'true',variant:'dark_oak'}}"); -+ register(2580, "{Name:'minecraft:acacia_leaves',Properties:{check_decay:'false',decayable:'false'}}", "{Name:'minecraft:leaves2',Properties:{check_decay:'false',decayable:'false',variant:'acacia'}}"); -+ register(2581, "{Name:'minecraft:dark_oak_leaves',Properties:{check_decay:'false',decayable:'false'}}", "{Name:'minecraft:leaves2',Properties:{check_decay:'false',decayable:'false',variant:'dark_oak'}}"); -+ register(2584, "{Name:'minecraft:acacia_leaves',Properties:{check_decay:'true',decayable:'true'}}", "{Name:'minecraft:leaves2',Properties:{check_decay:'true',decayable:'true',variant:'acacia'}}"); -+ register(2585, "{Name:'minecraft:dark_oak_leaves',Properties:{check_decay:'true',decayable:'true'}}", "{Name:'minecraft:leaves2',Properties:{check_decay:'true',decayable:'true',variant:'dark_oak'}}"); -+ register(2588, "{Name:'minecraft:acacia_leaves',Properties:{check_decay:'true',decayable:'false'}}", "{Name:'minecraft:leaves2',Properties:{check_decay:'true',decayable:'false',variant:'acacia'}}"); -+ register(2589, "{Name:'minecraft:dark_oak_leaves',Properties:{check_decay:'true',decayable:'false'}}", "{Name:'minecraft:leaves2',Properties:{check_decay:'true',decayable:'false',variant:'dark_oak'}}"); -+ register(2592, "{Name:'minecraft:acacia_log',Properties:{axis:'y'}}", "{Name:'minecraft:log2',Properties:{axis:'y',variant:'acacia'}}"); -+ register(2593, "{Name:'minecraft:dark_oak_log',Properties:{axis:'y'}}", "{Name:'minecraft:log2',Properties:{axis:'y',variant:'dark_oak'}}"); -+ register(2596, "{Name:'minecraft:acacia_log',Properties:{axis:'x'}}", "{Name:'minecraft:log2',Properties:{axis:'x',variant:'acacia'}}"); -+ register(2597, "{Name:'minecraft:dark_oak_log',Properties:{axis:'x'}}", "{Name:'minecraft:log2',Properties:{axis:'x',variant:'dark_oak'}}"); -+ register(2600, "{Name:'minecraft:acacia_log',Properties:{axis:'z'}}", "{Name:'minecraft:log2',Properties:{axis:'z',variant:'acacia'}}"); -+ register(2601, "{Name:'minecraft:dark_oak_log',Properties:{axis:'z'}}", "{Name:'minecraft:log2',Properties:{axis:'z',variant:'dark_oak'}}"); -+ register(2604, "{Name:'minecraft:acacia_bark'}", "{Name:'minecraft:log2',Properties:{axis:'none',variant:'acacia'}}"); -+ register(2605, "{Name:'minecraft:dark_oak_bark'}", "{Name:'minecraft:log2',Properties:{axis:'none',variant:'dark_oak'}}"); -+ register(2608, "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); -+ register(2609, "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); -+ register(2610, "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); -+ register(2611, "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); -+ register(2612, "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); -+ register(2613, "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); -+ register(2614, "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); -+ register(2615, "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:acacia_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); -+ register(2624, "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); -+ register(2625, "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); -+ register(2626, "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); -+ register(2627, "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); -+ register(2628, "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); -+ register(2629, "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); -+ register(2630, "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); -+ register(2631, "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:dark_oak_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); -+ register(2640, "{Name:'minecraft:slime_block'}", "{Name:'minecraft:slime'}"); -+ register(2656, "{Name:'minecraft:barrier'}", "{Name:'minecraft:barrier'}"); -+ register(2672, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'north',half:'bottom',open:'false'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'north',half:'bottom',open:'false'}}"); -+ register(2673, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'south',half:'bottom',open:'false'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'south',half:'bottom',open:'false'}}"); -+ register(2674, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'west',half:'bottom',open:'false'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'west',half:'bottom',open:'false'}}"); -+ register(2675, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'east',half:'bottom',open:'false'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'east',half:'bottom',open:'false'}}"); -+ register(2676, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'north',half:'bottom',open:'true'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'north',half:'bottom',open:'true'}}"); -+ register(2677, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'south',half:'bottom',open:'true'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'south',half:'bottom',open:'true'}}"); -+ register(2678, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'west',half:'bottom',open:'true'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'west',half:'bottom',open:'true'}}"); -+ register(2679, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'east',half:'bottom',open:'true'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'east',half:'bottom',open:'true'}}"); -+ register(2680, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'north',half:'top',open:'false'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'north',half:'top',open:'false'}}"); -+ register(2681, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'south',half:'top',open:'false'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'south',half:'top',open:'false'}}"); -+ register(2682, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'west',half:'top',open:'false'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'west',half:'top',open:'false'}}"); -+ register(2683, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'east',half:'top',open:'false'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'east',half:'top',open:'false'}}"); -+ register(2684, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'north',half:'top',open:'true'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'north',half:'top',open:'true'}}"); -+ register(2685, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'south',half:'top',open:'true'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'south',half:'top',open:'true'}}"); -+ register(2686, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'west',half:'top',open:'true'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'west',half:'top',open:'true'}}"); -+ register(2687, "{Name:'minecraft:iron_trapdoor',Properties:{facing:'east',half:'top',open:'true'}}", "{Name:'minecraft:iron_trapdoor',Properties:{facing:'east',half:'top',open:'true'}}"); -+ register(2688, "{Name:'minecraft:prismarine'}", "{Name:'minecraft:prismarine',Properties:{variant:'prismarine'}}"); -+ register(2689, "{Name:'minecraft:prismarine_bricks'}", "{Name:'minecraft:prismarine',Properties:{variant:'prismarine_bricks'}}"); -+ register(2690, "{Name:'minecraft:dark_prismarine'}", "{Name:'minecraft:prismarine',Properties:{variant:'dark_prismarine'}}"); -+ register(2704, "{Name:'minecraft:sea_lantern'}", "{Name:'minecraft:sea_lantern'}"); -+ register(2720, "{Name:'minecraft:hay_block',Properties:{axis:'y'}}", "{Name:'minecraft:hay_block',Properties:{axis:'y'}}"); -+ register(2724, "{Name:'minecraft:hay_block',Properties:{axis:'x'}}", "{Name:'minecraft:hay_block',Properties:{axis:'x'}}"); -+ register(2728, "{Name:'minecraft:hay_block',Properties:{axis:'z'}}", "{Name:'minecraft:hay_block',Properties:{axis:'z'}}"); -+ register(2736, "{Name:'minecraft:white_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'white'}}"); -+ register(2737, "{Name:'minecraft:orange_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'orange'}}"); -+ register(2738, "{Name:'minecraft:magenta_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'magenta'}}"); -+ register(2739, "{Name:'minecraft:light_blue_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'light_blue'}}"); -+ register(2740, "{Name:'minecraft:yellow_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'yellow'}}"); -+ register(2741, "{Name:'minecraft:lime_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'lime'}}"); -+ register(2742, "{Name:'minecraft:pink_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'pink'}}"); -+ register(2743, "{Name:'minecraft:gray_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'gray'}}"); -+ register(2744, "{Name:'minecraft:light_gray_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'silver'}}"); -+ register(2745, "{Name:'minecraft:cyan_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'cyan'}}"); -+ register(2746, "{Name:'minecraft:purple_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'purple'}}"); -+ register(2747, "{Name:'minecraft:blue_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'blue'}}"); -+ register(2748, "{Name:'minecraft:brown_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'brown'}}"); -+ register(2749, "{Name:'minecraft:green_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'green'}}"); -+ register(2750, "{Name:'minecraft:red_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'red'}}"); -+ register(2751, "{Name:'minecraft:black_carpet'}", "{Name:'minecraft:carpet',Properties:{color:'black'}}"); -+ register(2752, "{Name:'minecraft:terracotta'}", "{Name:'minecraft:hardened_clay'}"); -+ register(2768, "{Name:'minecraft:coal_block'}", "{Name:'minecraft:coal_block'}"); -+ register(2784, "{Name:'minecraft:packed_ice'}", "{Name:'minecraft:packed_ice'}"); -+ register(2800, "{Name:'minecraft:sunflower',Properties:{half:'lower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'lower',variant:'sunflower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'lower',variant:'sunflower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'lower',variant:'sunflower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'lower',variant:'sunflower'}}"); -+ register(2801, "{Name:'minecraft:lilac',Properties:{half:'lower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'lower',variant:'syringa'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'lower',variant:'syringa'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'lower',variant:'syringa'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'lower',variant:'syringa'}}"); -+ register(2802, "{Name:'minecraft:tall_grass',Properties:{half:'lower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'lower',variant:'double_grass'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'lower',variant:'double_grass'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'lower',variant:'double_grass'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'lower',variant:'double_grass'}}"); -+ register(2803, "{Name:'minecraft:large_fern',Properties:{half:'lower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'lower',variant:'double_fern'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'lower',variant:'double_fern'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'lower',variant:'double_fern'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'lower',variant:'double_fern'}}"); -+ register(2804, "{Name:'minecraft:rose_bush',Properties:{half:'lower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'lower',variant:'double_rose'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'lower',variant:'double_rose'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'lower',variant:'double_rose'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'lower',variant:'double_rose'}}"); -+ register(2805, "{Name:'minecraft:peony',Properties:{half:'lower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'lower',variant:'paeonia'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'lower',variant:'paeonia'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'lower',variant:'paeonia'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'lower',variant:'paeonia'}}"); -+ register(2808, "{Name:'minecraft:peony',Properties:{half:'upper'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'upper',variant:'double_fern'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'upper',variant:'double_grass'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'upper',variant:'double_rose'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'upper',variant:'paeonia'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'upper',variant:'sunflower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'south',half:'upper',variant:'syringa'}}"); -+ register(2809, "{Name:'minecraft:peony',Properties:{half:'upper'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'upper',variant:'double_fern'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'upper',variant:'double_grass'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'upper',variant:'double_rose'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'upper',variant:'paeonia'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'upper',variant:'sunflower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'west',half:'upper',variant:'syringa'}}"); -+ register(2810, "{Name:'minecraft:peony',Properties:{half:'upper'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'upper',variant:'double_fern'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'upper',variant:'double_grass'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'upper',variant:'double_rose'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'upper',variant:'paeonia'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'upper',variant:'sunflower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'north',half:'upper',variant:'syringa'}}"); -+ register(2811, "{Name:'minecraft:peony',Properties:{half:'upper'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'upper',variant:'double_fern'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'upper',variant:'double_grass'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'upper',variant:'double_rose'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'upper',variant:'paeonia'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'upper',variant:'sunflower'}}", "{Name:'minecraft:double_plant',Properties:{facing:'east',half:'upper',variant:'syringa'}}"); -+ register(2816, "{Name:'minecraft:white_banner',Properties:{rotation:'0'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'0'}}"); -+ register(2817, "{Name:'minecraft:white_banner',Properties:{rotation:'1'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'1'}}"); -+ register(2818, "{Name:'minecraft:white_banner',Properties:{rotation:'2'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'2'}}"); -+ register(2819, "{Name:'minecraft:white_banner',Properties:{rotation:'3'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'3'}}"); -+ register(2820, "{Name:'minecraft:white_banner',Properties:{rotation:'4'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'4'}}"); -+ register(2821, "{Name:'minecraft:white_banner',Properties:{rotation:'5'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'5'}}"); -+ register(2822, "{Name:'minecraft:white_banner',Properties:{rotation:'6'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'6'}}"); -+ register(2823, "{Name:'minecraft:white_banner',Properties:{rotation:'7'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'7'}}"); -+ register(2824, "{Name:'minecraft:white_banner',Properties:{rotation:'8'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'8'}}"); -+ register(2825, "{Name:'minecraft:white_banner',Properties:{rotation:'9'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'9'}}"); -+ register(2826, "{Name:'minecraft:white_banner',Properties:{rotation:'10'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'10'}}"); -+ register(2827, "{Name:'minecraft:white_banner',Properties:{rotation:'11'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'11'}}"); -+ register(2828, "{Name:'minecraft:white_banner',Properties:{rotation:'12'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'12'}}"); -+ register(2829, "{Name:'minecraft:white_banner',Properties:{rotation:'13'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'13'}}"); -+ register(2830, "{Name:'minecraft:white_banner',Properties:{rotation:'14'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'14'}}"); -+ register(2831, "{Name:'minecraft:white_banner',Properties:{rotation:'15'}}", "{Name:'minecraft:standing_banner',Properties:{rotation:'15'}}"); -+ register(2834, "{Name:'minecraft:white_wall_banner',Properties:{facing:'north'}}", "{Name:'minecraft:wall_banner',Properties:{facing:'north'}}"); -+ register(2835, "{Name:'minecraft:white_wall_banner',Properties:{facing:'south'}}", "{Name:'minecraft:wall_banner',Properties:{facing:'south'}}"); -+ register(2836, "{Name:'minecraft:white_wall_banner',Properties:{facing:'west'}}", "{Name:'minecraft:wall_banner',Properties:{facing:'west'}}"); -+ register(2837, "{Name:'minecraft:white_wall_banner',Properties:{facing:'east'}}", "{Name:'minecraft:wall_banner',Properties:{facing:'east'}}"); -+ register(2848, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'0'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'0'}}"); -+ register(2849, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'1'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'1'}}"); -+ register(2850, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'2'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'2'}}"); -+ register(2851, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'3'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'3'}}"); -+ register(2852, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'4'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'4'}}"); -+ register(2853, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'5'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'5'}}"); -+ register(2854, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'6'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'6'}}"); -+ register(2855, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'7'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'7'}}"); -+ register(2856, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'8'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'8'}}"); -+ register(2857, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'9'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'9'}}"); -+ register(2858, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'10'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'10'}}"); -+ register(2859, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'11'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'11'}}"); -+ register(2860, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'12'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'12'}}"); -+ register(2861, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'13'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'13'}}"); -+ register(2862, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'14'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'14'}}"); -+ register(2863, "{Name:'minecraft:daylight_detector',Properties:{inverted:'true',power:'15'}}", "{Name:'minecraft:daylight_detector_inverted',Properties:{power:'15'}}"); -+ register(2864, "{Name:'minecraft:red_sandstone'}", "{Name:'minecraft:red_sandstone',Properties:{type:'red_sandstone'}}"); -+ register(2865, "{Name:'minecraft:chiseled_red_sandstone'}", "{Name:'minecraft:red_sandstone',Properties:{type:'chiseled_red_sandstone'}}"); -+ register(2866, "{Name:'minecraft:cut_red_sandstone'}", "{Name:'minecraft:red_sandstone',Properties:{type:'smooth_red_sandstone'}}"); -+ register(2880, "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); -+ register(2881, "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); -+ register(2882, "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); -+ register(2883, "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); -+ register(2884, "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); -+ register(2885, "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); -+ register(2886, "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); -+ register(2887, "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:red_sandstone_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); -+ register(2896, "{Name:'minecraft:red_sandstone_slab',Properties:{type:'double'}}", "{Name:'minecraft:double_stone_slab2',Properties:{seamless:'false',variant:'red_sandstone'}}"); -+ register(2904, "{Name:'minecraft:smooth_red_sandstone'}", "{Name:'minecraft:double_stone_slab2',Properties:{seamless:'true',variant:'red_sandstone'}}"); -+ register(2912, "{Name:'minecraft:red_sandstone_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:stone_slab2',Properties:{half:'bottom',variant:'red_sandstone'}}"); -+ register(2920, "{Name:'minecraft:red_sandstone_slab',Properties:{type:'top'}}", "{Name:'minecraft:stone_slab2',Properties:{half:'top',variant:'red_sandstone'}}"); -+ register(2928, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'false'}}"); -+ register(2929, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'false'}}"); -+ register(2930, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'false'}}"); -+ register(2931, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'false'}}"); -+ register(2932, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'false'}}"); -+ register(2933, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'false'}}"); -+ register(2934, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'false'}}"); -+ register(2935, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'false'}}"); -+ register(2936, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'true'}}"); -+ register(2937, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'true'}}"); -+ register(2938, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'true'}}"); -+ register(2939, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'true'}}"); -+ register(2940, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'true'}}"); -+ register(2941, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'true'}}"); -+ register(2942, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'true'}}"); -+ register(2943, "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'true'}}"); -+ register(2944, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'false'}}"); -+ register(2945, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'false'}}"); -+ register(2946, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'false'}}"); -+ register(2947, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'false'}}"); -+ register(2948, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'false'}}"); -+ register(2949, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'false'}}"); -+ register(2950, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'false'}}"); -+ register(2951, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'false'}}"); -+ register(2952, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'true'}}"); -+ register(2953, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'true'}}"); -+ register(2954, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'true'}}"); -+ register(2955, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'true'}}"); -+ register(2956, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'true'}}"); -+ register(2957, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'true'}}"); -+ register(2958, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'true'}}"); -+ register(2959, "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:birch_fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'true'}}"); -+ register(2960, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'false'}}"); -+ register(2961, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'false'}}"); -+ register(2962, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'false'}}"); -+ register(2963, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'false'}}"); -+ register(2964, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'false'}}"); -+ register(2965, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'false'}}"); -+ register(2966, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'false'}}"); -+ register(2967, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'false'}}"); -+ register(2968, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'true'}}"); -+ register(2969, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'true'}}"); -+ register(2970, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'true'}}"); -+ register(2971, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'true'}}"); -+ register(2972, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'true'}}"); -+ register(2973, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'true'}}"); -+ register(2974, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'true'}}"); -+ register(2975, "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'true'}}"); -+ register(2976, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'false'}}"); -+ register(2977, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'false'}}"); -+ register(2978, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'false'}}"); -+ register(2979, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'false'}}"); -+ register(2980, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'false'}}"); -+ register(2981, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'false'}}"); -+ register(2982, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'false'}}"); -+ register(2983, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'false'}}"); -+ register(2984, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'true'}}"); -+ register(2985, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'true'}}"); -+ register(2986, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'true'}}"); -+ register(2987, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'true'}}"); -+ register(2988, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'true'}}"); -+ register(2989, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'true'}}"); -+ register(2990, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'true'}}"); -+ register(2991, "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'true'}}"); -+ register(2992, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'false'}}"); -+ register(2993, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'false'}}"); -+ register(2994, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'false'}}"); -+ register(2995, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'false'}}"); -+ register(2996, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'false'}}"); -+ register(2997, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'false'}}"); -+ register(2998, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'false'}}"); -+ register(2999, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'false'}}"); -+ register(3000, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'true',open:'false',powered:'true'}}"); -+ register(3001, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'true',open:'false',powered:'true'}}"); -+ register(3002, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'true',open:'false',powered:'true'}}"); -+ register(3003, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'false',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'true',open:'false',powered:'true'}}"); -+ register(3004, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'south',in_wall:'true',open:'true',powered:'true'}}"); -+ register(3005, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'west',in_wall:'true',open:'true',powered:'true'}}"); -+ register(3006, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'north',in_wall:'true',open:'true',powered:'true'}}"); -+ register(3007, "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'false',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_fence_gate',Properties:{facing:'east',in_wall:'true',open:'true',powered:'true'}}"); -+ register(3008, "{Name:'minecraft:spruce_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:spruce_fence',Properties:{east:'true',north:'true',south:'true',west:'true'}}"); -+ register(3024, "{Name:'minecraft:birch_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:birch_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:birch_fence',Properties:{east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:birch_fence',Properties:{east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:birch_fence',Properties:{east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:birch_fence',Properties:{east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:birch_fence',Properties:{east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:birch_fence',Properties:{east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:birch_fence',Properties:{east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:birch_fence',Properties:{east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:birch_fence',Properties:{east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:birch_fence',Properties:{east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:birch_fence',Properties:{east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:birch_fence',Properties:{east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:birch_fence',Properties:{east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:birch_fence',Properties:{east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:birch_fence',Properties:{east:'true',north:'true',south:'true',west:'true'}}"); -+ register(3040, "{Name:'minecraft:jungle_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:jungle_fence',Properties:{east:'true',north:'true',south:'true',west:'true'}}"); -+ register(3056, "{Name:'minecraft:dark_oak_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:dark_oak_fence',Properties:{east:'true',north:'true',south:'true',west:'true'}}"); -+ register(3072, "{Name:'minecraft:acacia_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'false',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'false',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'false',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'false',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'false',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'false',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'false',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'false',north:'true',south:'true',west:'true'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'true',north:'false',south:'false',west:'false'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'true',north:'false',south:'false',west:'true'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'true',north:'false',south:'true',west:'false'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'true',north:'false',south:'true',west:'true'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'true',north:'true',south:'false',west:'false'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'true',north:'true',south:'false',west:'true'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'true',north:'true',south:'true',west:'false'}}", "{Name:'minecraft:acacia_fence',Properties:{east:'true',north:'true',south:'true',west:'true'}}"); -+ register(3088, "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(3089, "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(3090, "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(3091, "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(3092, "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(3093, "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(3094, "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(3095, "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(3096, "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'false'}}"); -+ register(3097, "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'false'}}"); -+ register(3098, "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'true'}}"); -+ register(3099, "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:spruce_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'true'}}"); -+ register(3104, "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(3105, "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(3106, "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(3107, "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(3108, "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(3109, "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(3110, "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(3111, "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(3112, "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'false'}}"); -+ register(3113, "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'false'}}"); -+ register(3114, "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'true'}}"); -+ register(3115, "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:birch_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'true'}}"); -+ register(3120, "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(3121, "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(3122, "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(3123, "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(3124, "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(3125, "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(3126, "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(3127, "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(3128, "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'false'}}"); -+ register(3129, "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'false'}}"); -+ register(3130, "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'true'}}"); -+ register(3131, "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:jungle_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'true'}}"); -+ register(3136, "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(3137, "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(3138, "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(3139, "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(3140, "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(3141, "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(3142, "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(3143, "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(3144, "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'false'}}"); -+ register(3145, "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'false'}}"); -+ register(3146, "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'true'}}"); -+ register(3147, "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:acacia_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'true'}}"); -+ register(3152, "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(3153, "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(3154, "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(3155, "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'false',powered:'true'}}"); -+ register(3156, "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(3157, "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(3158, "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(3159, "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'lower',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'lower',hinge:'right',open:'true',powered:'true'}}"); -+ register(3160, "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'false'}}"); -+ register(3161, "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'false'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'false'}}"); -+ register(3162, "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'upper',hinge:'left',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'upper',hinge:'left',open:'true',powered:'true'}}"); -+ register(3163, "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'east',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'north',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'south',half:'upper',hinge:'right',open:'true',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'false',powered:'true'}}", "{Name:'minecraft:dark_oak_door',Properties:{facing:'west',half:'upper',hinge:'right',open:'true',powered:'true'}}"); -+ register(3168, "{Name:'minecraft:end_rod',Properties:{facing:'down'}}", "{Name:'minecraft:end_rod',Properties:{facing:'down'}}"); -+ register(3169, "{Name:'minecraft:end_rod',Properties:{facing:'up'}}", "{Name:'minecraft:end_rod',Properties:{facing:'up'}}"); -+ register(3170, "{Name:'minecraft:end_rod',Properties:{facing:'north'}}", "{Name:'minecraft:end_rod',Properties:{facing:'north'}}"); -+ register(3171, "{Name:'minecraft:end_rod',Properties:{facing:'south'}}", "{Name:'minecraft:end_rod',Properties:{facing:'south'}}"); -+ register(3172, "{Name:'minecraft:end_rod',Properties:{facing:'west'}}", "{Name:'minecraft:end_rod',Properties:{facing:'west'}}"); -+ register(3173, "{Name:'minecraft:end_rod',Properties:{facing:'east'}}", "{Name:'minecraft:end_rod',Properties:{facing:'east'}}"); -+ register(3184, "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'false',east:'true',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'false',north:'true',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'false',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'false',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'false',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'false',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'false',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'false',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'false',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'false',south:'true',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'true',south:'false',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'true',south:'false',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'true',south:'false',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'true',south:'false',up:'true',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'true',south:'true',up:'false',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'true',south:'true',up:'false',west:'true'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'true',south:'true',up:'true',west:'false'}}", "{Name:'minecraft:chorus_plant',Properties:{down:'true',east:'true',north:'true',south:'true',up:'true',west:'true'}}"); -+ register(3200, "{Name:'minecraft:chorus_flower',Properties:{age:'0'}}", "{Name:'minecraft:chorus_flower',Properties:{age:'0'}}"); -+ register(3201, "{Name:'minecraft:chorus_flower',Properties:{age:'1'}}", "{Name:'minecraft:chorus_flower',Properties:{age:'1'}}"); -+ register(3202, "{Name:'minecraft:chorus_flower',Properties:{age:'2'}}", "{Name:'minecraft:chorus_flower',Properties:{age:'2'}}"); -+ register(3203, "{Name:'minecraft:chorus_flower',Properties:{age:'3'}}", "{Name:'minecraft:chorus_flower',Properties:{age:'3'}}"); -+ register(3204, "{Name:'minecraft:chorus_flower',Properties:{age:'4'}}", "{Name:'minecraft:chorus_flower',Properties:{age:'4'}}"); -+ register(3205, "{Name:'minecraft:chorus_flower',Properties:{age:'5'}}", "{Name:'minecraft:chorus_flower',Properties:{age:'5'}}"); -+ register(3216, "{Name:'minecraft:purpur_block'}", "{Name:'minecraft:purpur_block'}"); -+ register(3232, "{Name:'minecraft:purpur_pillar',Properties:{axis:'y'}}", "{Name:'minecraft:purpur_pillar',Properties:{axis:'y'}}"); -+ register(3236, "{Name:'minecraft:purpur_pillar',Properties:{axis:'x'}}", "{Name:'minecraft:purpur_pillar',Properties:{axis:'x'}}"); -+ register(3240, "{Name:'minecraft:purpur_pillar',Properties:{axis:'z'}}", "{Name:'minecraft:purpur_pillar',Properties:{axis:'z'}}"); -+ register(3248, "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'bottom',shape:'straight'}}"); -+ register(3249, "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'bottom',shape:'straight'}}"); -+ register(3250, "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'bottom',shape:'straight'}}"); -+ register(3251, "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'bottom',shape:'inner_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'bottom',shape:'outer_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'bottom',shape:'straight'}}"); -+ register(3252, "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'top',shape:'inner_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'top',shape:'inner_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'top',shape:'outer_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'top',shape:'outer_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'east',half:'top',shape:'straight'}}"); -+ register(3253, "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'top',shape:'inner_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'top',shape:'inner_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'top',shape:'outer_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'top',shape:'outer_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'west',half:'top',shape:'straight'}}"); -+ register(3254, "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'top',shape:'inner_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'top',shape:'inner_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'top',shape:'outer_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'top',shape:'outer_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'south',half:'top',shape:'straight'}}"); -+ register(3255, "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'top',shape:'inner_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'top',shape:'inner_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'top',shape:'outer_left'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'top',shape:'outer_right'}}", "{Name:'minecraft:purpur_stairs',Properties:{facing:'north',half:'top',shape:'straight'}}"); -+ register(3264, "{Name:'minecraft:purpur_slab',Properties:{type:'double'}}", "{Name:'minecraft:purpur_double_slab',Properties:{variant:'default'}}"); -+ register(3280, "{Name:'minecraft:purpur_slab',Properties:{type:'bottom'}}", "{Name:'minecraft:purpur_slab',Properties:{half:'bottom',variant:'default'}}"); -+ register(3288, "{Name:'minecraft:purpur_slab',Properties:{type:'top'}}", "{Name:'minecraft:purpur_slab',Properties:{half:'top',variant:'default'}}"); -+ register(3296, "{Name:'minecraft:end_stone_bricks'}", "{Name:'minecraft:end_bricks'}"); -+ register(3312, "{Name:'minecraft:beetroots',Properties:{age:'0'}}", "{Name:'minecraft:beetroots',Properties:{age:'0'}}"); -+ register(3313, "{Name:'minecraft:beetroots',Properties:{age:'1'}}", "{Name:'minecraft:beetroots',Properties:{age:'1'}}"); -+ register(3314, "{Name:'minecraft:beetroots',Properties:{age:'2'}}", "{Name:'minecraft:beetroots',Properties:{age:'2'}}"); -+ register(3315, "{Name:'minecraft:beetroots',Properties:{age:'3'}}", "{Name:'minecraft:beetroots',Properties:{age:'3'}}"); -+ register(3328, "{Name:'minecraft:grass_path'}", "{Name:'minecraft:grass_path'}"); -+ register(3344, "{Name:'minecraft:end_gateway'}", "{Name:'minecraft:end_gateway'}"); -+ register(3360, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'down'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'down'}}"); -+ register(3361, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'up'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'up'}}"); -+ register(3362, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'north'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'north'}}"); -+ register(3363, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'south'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'south'}}"); -+ register(3364, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'west'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'west'}}"); -+ register(3365, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'east'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'false',facing:'east'}}"); -+ register(3368, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'down'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'down'}}"); -+ register(3369, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'up'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'up'}}"); -+ register(3370, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'north'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'north'}}"); -+ register(3371, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'south'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'south'}}"); -+ register(3372, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'west'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'west'}}"); -+ register(3373, "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'east'}}", "{Name:'minecraft:repeating_command_block',Properties:{conditional:'true',facing:'east'}}"); -+ register(3376, "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'down'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'down'}}"); -+ register(3377, "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'up'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'up'}}"); -+ register(3378, "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'north'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'north'}}"); -+ register(3379, "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'south'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'south'}}"); -+ register(3380, "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'west'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'west'}}"); -+ register(3381, "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'east'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'false',facing:'east'}}"); -+ register(3384, "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'down'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'down'}}"); -+ register(3385, "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'up'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'up'}}"); -+ register(3386, "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'north'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'north'}}"); -+ register(3387, "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'south'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'south'}}"); -+ register(3388, "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'west'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'west'}}"); -+ register(3389, "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'east'}}", "{Name:'minecraft:chain_command_block',Properties:{conditional:'true',facing:'east'}}"); -+ register(3392, "{Name:'minecraft:frosted_ice',Properties:{age:'0'}}", "{Name:'minecraft:frosted_ice',Properties:{age:'0'}}"); -+ register(3393, "{Name:'minecraft:frosted_ice',Properties:{age:'1'}}", "{Name:'minecraft:frosted_ice',Properties:{age:'1'}}"); -+ register(3394, "{Name:'minecraft:frosted_ice',Properties:{age:'2'}}", "{Name:'minecraft:frosted_ice',Properties:{age:'2'}}"); -+ register(3395, "{Name:'minecraft:frosted_ice',Properties:{age:'3'}}", "{Name:'minecraft:frosted_ice',Properties:{age:'3'}}"); -+ register(3408, "{Name:'minecraft:magma_block'}", "{Name:'minecraft:magma'}"); -+ register(3424, "{Name:'minecraft:nether_wart_block'}", "{Name:'minecraft:nether_wart_block'}"); -+ register(3440, "{Name:'minecraft:red_nether_bricks'}", "{Name:'minecraft:red_nether_brick'}"); -+ register(3456, "{Name:'minecraft:bone_block',Properties:{axis:'y'}}", "{Name:'minecraft:bone_block',Properties:{axis:'y'}}"); -+ register(3460, "{Name:'minecraft:bone_block',Properties:{axis:'x'}}", "{Name:'minecraft:bone_block',Properties:{axis:'x'}}"); -+ register(3464, "{Name:'minecraft:bone_block',Properties:{axis:'z'}}", "{Name:'minecraft:bone_block',Properties:{axis:'z'}}"); -+ register(3472, "{Name:'minecraft:structure_void'}", "{Name:'minecraft:structure_void'}"); -+ register(3488, "{Name:'minecraft:observer',Properties:{facing:'down',powered:'false'}}", "{Name:'minecraft:observer',Properties:{facing:'down',powered:'false'}}"); -+ register(3489, "{Name:'minecraft:observer',Properties:{facing:'up',powered:'false'}}", "{Name:'minecraft:observer',Properties:{facing:'up',powered:'false'}}"); -+ register(3490, "{Name:'minecraft:observer',Properties:{facing:'north',powered:'false'}}", "{Name:'minecraft:observer',Properties:{facing:'north',powered:'false'}}"); -+ register(3491, "{Name:'minecraft:observer',Properties:{facing:'south',powered:'false'}}", "{Name:'minecraft:observer',Properties:{facing:'south',powered:'false'}}"); -+ register(3492, "{Name:'minecraft:observer',Properties:{facing:'west',powered:'false'}}", "{Name:'minecraft:observer',Properties:{facing:'west',powered:'false'}}"); -+ register(3493, "{Name:'minecraft:observer',Properties:{facing:'east',powered:'false'}}", "{Name:'minecraft:observer',Properties:{facing:'east',powered:'false'}}"); -+ register(3496, "{Name:'minecraft:observer',Properties:{facing:'down',powered:'true'}}", "{Name:'minecraft:observer',Properties:{facing:'down',powered:'true'}}"); -+ register(3497, "{Name:'minecraft:observer',Properties:{facing:'up',powered:'true'}}", "{Name:'minecraft:observer',Properties:{facing:'up',powered:'true'}}"); -+ register(3498, "{Name:'minecraft:observer',Properties:{facing:'north',powered:'true'}}", "{Name:'minecraft:observer',Properties:{facing:'north',powered:'true'}}"); -+ register(3499, "{Name:'minecraft:observer',Properties:{facing:'south',powered:'true'}}", "{Name:'minecraft:observer',Properties:{facing:'south',powered:'true'}}"); -+ register(3500, "{Name:'minecraft:observer',Properties:{facing:'west',powered:'true'}}", "{Name:'minecraft:observer',Properties:{facing:'west',powered:'true'}}"); -+ register(3501, "{Name:'minecraft:observer',Properties:{facing:'east',powered:'true'}}", "{Name:'minecraft:observer',Properties:{facing:'east',powered:'true'}}"); -+ register(3504, "{Name:'minecraft:white_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:white_shulker_box',Properties:{facing:'down'}}"); -+ register(3505, "{Name:'minecraft:white_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:white_shulker_box',Properties:{facing:'up'}}"); -+ register(3506, "{Name:'minecraft:white_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:white_shulker_box',Properties:{facing:'north'}}"); -+ register(3507, "{Name:'minecraft:white_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:white_shulker_box',Properties:{facing:'south'}}"); -+ register(3508, "{Name:'minecraft:white_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:white_shulker_box',Properties:{facing:'west'}}"); -+ register(3509, "{Name:'minecraft:white_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:white_shulker_box',Properties:{facing:'east'}}"); -+ register(3520, "{Name:'minecraft:orange_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:orange_shulker_box',Properties:{facing:'down'}}"); -+ register(3521, "{Name:'minecraft:orange_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:orange_shulker_box',Properties:{facing:'up'}}"); -+ register(3522, "{Name:'minecraft:orange_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:orange_shulker_box',Properties:{facing:'north'}}"); -+ register(3523, "{Name:'minecraft:orange_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:orange_shulker_box',Properties:{facing:'south'}}"); -+ register(3524, "{Name:'minecraft:orange_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:orange_shulker_box',Properties:{facing:'west'}}"); -+ register(3525, "{Name:'minecraft:orange_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:orange_shulker_box',Properties:{facing:'east'}}"); -+ register(3536, "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'down'}}"); -+ register(3537, "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'up'}}"); -+ register(3538, "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'north'}}"); -+ register(3539, "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'south'}}"); -+ register(3540, "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'west'}}"); -+ register(3541, "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:magenta_shulker_box',Properties:{facing:'east'}}"); -+ register(3552, "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'down'}}"); -+ register(3553, "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'up'}}"); -+ register(3554, "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'north'}}"); -+ register(3555, "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'south'}}"); -+ register(3556, "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'west'}}"); -+ register(3557, "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:light_blue_shulker_box',Properties:{facing:'east'}}"); -+ register(3568, "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'down'}}"); -+ register(3569, "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'up'}}"); -+ register(3570, "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'north'}}"); -+ register(3571, "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'south'}}"); -+ register(3572, "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'west'}}"); -+ register(3573, "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:yellow_shulker_box',Properties:{facing:'east'}}"); -+ register(3584, "{Name:'minecraft:lime_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:lime_shulker_box',Properties:{facing:'down'}}"); -+ register(3585, "{Name:'minecraft:lime_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:lime_shulker_box',Properties:{facing:'up'}}"); -+ register(3586, "{Name:'minecraft:lime_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:lime_shulker_box',Properties:{facing:'north'}}"); -+ register(3587, "{Name:'minecraft:lime_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:lime_shulker_box',Properties:{facing:'south'}}"); -+ register(3588, "{Name:'minecraft:lime_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:lime_shulker_box',Properties:{facing:'west'}}"); -+ register(3589, "{Name:'minecraft:lime_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:lime_shulker_box',Properties:{facing:'east'}}"); -+ register(3600, "{Name:'minecraft:pink_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:pink_shulker_box',Properties:{facing:'down'}}"); -+ register(3601, "{Name:'minecraft:pink_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:pink_shulker_box',Properties:{facing:'up'}}"); -+ register(3602, "{Name:'minecraft:pink_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:pink_shulker_box',Properties:{facing:'north'}}"); -+ register(3603, "{Name:'minecraft:pink_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:pink_shulker_box',Properties:{facing:'south'}}"); -+ register(3604, "{Name:'minecraft:pink_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:pink_shulker_box',Properties:{facing:'west'}}"); -+ register(3605, "{Name:'minecraft:pink_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:pink_shulker_box',Properties:{facing:'east'}}"); -+ register(3616, "{Name:'minecraft:gray_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:gray_shulker_box',Properties:{facing:'down'}}"); -+ register(3617, "{Name:'minecraft:gray_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:gray_shulker_box',Properties:{facing:'up'}}"); -+ register(3618, "{Name:'minecraft:gray_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:gray_shulker_box',Properties:{facing:'north'}}"); -+ register(3619, "{Name:'minecraft:gray_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:gray_shulker_box',Properties:{facing:'south'}}"); -+ register(3620, "{Name:'minecraft:gray_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:gray_shulker_box',Properties:{facing:'west'}}"); -+ register(3621, "{Name:'minecraft:gray_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:gray_shulker_box',Properties:{facing:'east'}}"); -+ register(3632, "{Name:'minecraft:light_gray_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:silver_shulker_box',Properties:{facing:'down'}}"); -+ register(3633, "{Name:'minecraft:light_gray_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:silver_shulker_box',Properties:{facing:'up'}}"); -+ register(3634, "{Name:'minecraft:light_gray_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:silver_shulker_box',Properties:{facing:'north'}}"); -+ register(3635, "{Name:'minecraft:light_gray_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:silver_shulker_box',Properties:{facing:'south'}}"); -+ register(3636, "{Name:'minecraft:light_gray_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:silver_shulker_box',Properties:{facing:'west'}}"); -+ register(3637, "{Name:'minecraft:light_gray_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:silver_shulker_box',Properties:{facing:'east'}}"); -+ register(3648, "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'down'}}"); -+ register(3649, "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'up'}}"); -+ register(3650, "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'north'}}"); -+ register(3651, "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'south'}}"); -+ register(3652, "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'west'}}"); -+ register(3653, "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:cyan_shulker_box',Properties:{facing:'east'}}"); -+ register(3664, "{Name:'minecraft:purple_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:purple_shulker_box',Properties:{facing:'down'}}"); -+ register(3665, "{Name:'minecraft:purple_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:purple_shulker_box',Properties:{facing:'up'}}"); -+ register(3666, "{Name:'minecraft:purple_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:purple_shulker_box',Properties:{facing:'north'}}"); -+ register(3667, "{Name:'minecraft:purple_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:purple_shulker_box',Properties:{facing:'south'}}"); -+ register(3668, "{Name:'minecraft:purple_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:purple_shulker_box',Properties:{facing:'west'}}"); -+ register(3669, "{Name:'minecraft:purple_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:purple_shulker_box',Properties:{facing:'east'}}"); -+ register(3680, "{Name:'minecraft:blue_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:blue_shulker_box',Properties:{facing:'down'}}"); -+ register(3681, "{Name:'minecraft:blue_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:blue_shulker_box',Properties:{facing:'up'}}"); -+ register(3682, "{Name:'minecraft:blue_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:blue_shulker_box',Properties:{facing:'north'}}"); -+ register(3683, "{Name:'minecraft:blue_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:blue_shulker_box',Properties:{facing:'south'}}"); -+ register(3684, "{Name:'minecraft:blue_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:blue_shulker_box',Properties:{facing:'west'}}"); -+ register(3685, "{Name:'minecraft:blue_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:blue_shulker_box',Properties:{facing:'east'}}"); -+ register(3696, "{Name:'minecraft:brown_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:brown_shulker_box',Properties:{facing:'down'}}"); -+ register(3697, "{Name:'minecraft:brown_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:brown_shulker_box',Properties:{facing:'up'}}"); -+ register(3698, "{Name:'minecraft:brown_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:brown_shulker_box',Properties:{facing:'north'}}"); -+ register(3699, "{Name:'minecraft:brown_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:brown_shulker_box',Properties:{facing:'south'}}"); -+ register(3700, "{Name:'minecraft:brown_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:brown_shulker_box',Properties:{facing:'west'}}"); -+ register(3701, "{Name:'minecraft:brown_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:brown_shulker_box',Properties:{facing:'east'}}"); -+ register(3712, "{Name:'minecraft:green_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:green_shulker_box',Properties:{facing:'down'}}"); -+ register(3713, "{Name:'minecraft:green_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:green_shulker_box',Properties:{facing:'up'}}"); -+ register(3714, "{Name:'minecraft:green_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:green_shulker_box',Properties:{facing:'north'}}"); -+ register(3715, "{Name:'minecraft:green_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:green_shulker_box',Properties:{facing:'south'}}"); -+ register(3716, "{Name:'minecraft:green_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:green_shulker_box',Properties:{facing:'west'}}"); -+ register(3717, "{Name:'minecraft:green_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:green_shulker_box',Properties:{facing:'east'}}"); -+ register(3728, "{Name:'minecraft:red_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:red_shulker_box',Properties:{facing:'down'}}"); -+ register(3729, "{Name:'minecraft:red_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:red_shulker_box',Properties:{facing:'up'}}"); -+ register(3730, "{Name:'minecraft:red_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:red_shulker_box',Properties:{facing:'north'}}"); -+ register(3731, "{Name:'minecraft:red_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:red_shulker_box',Properties:{facing:'south'}}"); -+ register(3732, "{Name:'minecraft:red_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:red_shulker_box',Properties:{facing:'west'}}"); -+ register(3733, "{Name:'minecraft:red_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:red_shulker_box',Properties:{facing:'east'}}"); -+ register(3744, "{Name:'minecraft:black_shulker_box',Properties:{facing:'down'}}", "{Name:'minecraft:black_shulker_box',Properties:{facing:'down'}}"); -+ register(3745, "{Name:'minecraft:black_shulker_box',Properties:{facing:'up'}}", "{Name:'minecraft:black_shulker_box',Properties:{facing:'up'}}"); -+ register(3746, "{Name:'minecraft:black_shulker_box',Properties:{facing:'north'}}", "{Name:'minecraft:black_shulker_box',Properties:{facing:'north'}}"); -+ register(3747, "{Name:'minecraft:black_shulker_box',Properties:{facing:'south'}}", "{Name:'minecraft:black_shulker_box',Properties:{facing:'south'}}"); -+ register(3748, "{Name:'minecraft:black_shulker_box',Properties:{facing:'west'}}", "{Name:'minecraft:black_shulker_box',Properties:{facing:'west'}}"); -+ register(3749, "{Name:'minecraft:black_shulker_box',Properties:{facing:'east'}}", "{Name:'minecraft:black_shulker_box',Properties:{facing:'east'}}"); -+ register(3760, "{Name:'minecraft:white_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:white_glazed_terracotta',Properties:{facing:'south'}}"); -+ register(3761, "{Name:'minecraft:white_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:white_glazed_terracotta',Properties:{facing:'west'}}"); -+ register(3762, "{Name:'minecraft:white_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:white_glazed_terracotta',Properties:{facing:'north'}}"); -+ register(3763, "{Name:'minecraft:white_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:white_glazed_terracotta',Properties:{facing:'east'}}"); -+ register(3776, "{Name:'minecraft:orange_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:orange_glazed_terracotta',Properties:{facing:'south'}}"); -+ register(3777, "{Name:'minecraft:orange_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:orange_glazed_terracotta',Properties:{facing:'west'}}"); -+ register(3778, "{Name:'minecraft:orange_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:orange_glazed_terracotta',Properties:{facing:'north'}}"); -+ register(3779, "{Name:'minecraft:orange_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:orange_glazed_terracotta',Properties:{facing:'east'}}"); -+ register(3792, "{Name:'minecraft:magenta_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:magenta_glazed_terracotta',Properties:{facing:'south'}}"); -+ register(3793, "{Name:'minecraft:magenta_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:magenta_glazed_terracotta',Properties:{facing:'west'}}"); -+ register(3794, "{Name:'minecraft:magenta_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:magenta_glazed_terracotta',Properties:{facing:'north'}}"); -+ register(3795, "{Name:'minecraft:magenta_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:magenta_glazed_terracotta',Properties:{facing:'east'}}"); -+ register(3808, "{Name:'minecraft:light_blue_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:light_blue_glazed_terracotta',Properties:{facing:'south'}}"); -+ register(3809, "{Name:'minecraft:light_blue_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:light_blue_glazed_terracotta',Properties:{facing:'west'}}"); -+ register(3810, "{Name:'minecraft:light_blue_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:light_blue_glazed_terracotta',Properties:{facing:'north'}}"); -+ register(3811, "{Name:'minecraft:light_blue_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:light_blue_glazed_terracotta',Properties:{facing:'east'}}"); -+ register(3824, "{Name:'minecraft:yellow_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:yellow_glazed_terracotta',Properties:{facing:'south'}}"); -+ register(3825, "{Name:'minecraft:yellow_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:yellow_glazed_terracotta',Properties:{facing:'west'}}"); -+ register(3826, "{Name:'minecraft:yellow_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:yellow_glazed_terracotta',Properties:{facing:'north'}}"); -+ register(3827, "{Name:'minecraft:yellow_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:yellow_glazed_terracotta',Properties:{facing:'east'}}"); -+ register(3840, "{Name:'minecraft:lime_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:lime_glazed_terracotta',Properties:{facing:'south'}}"); -+ register(3841, "{Name:'minecraft:lime_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:lime_glazed_terracotta',Properties:{facing:'west'}}"); -+ register(3842, "{Name:'minecraft:lime_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:lime_glazed_terracotta',Properties:{facing:'north'}}"); -+ register(3843, "{Name:'minecraft:lime_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:lime_glazed_terracotta',Properties:{facing:'east'}}"); -+ register(3856, "{Name:'minecraft:pink_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:pink_glazed_terracotta',Properties:{facing:'south'}}"); -+ register(3857, "{Name:'minecraft:pink_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:pink_glazed_terracotta',Properties:{facing:'west'}}"); -+ register(3858, "{Name:'minecraft:pink_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:pink_glazed_terracotta',Properties:{facing:'north'}}"); -+ register(3859, "{Name:'minecraft:pink_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:pink_glazed_terracotta',Properties:{facing:'east'}}"); -+ register(3872, "{Name:'minecraft:gray_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:gray_glazed_terracotta',Properties:{facing:'south'}}"); -+ register(3873, "{Name:'minecraft:gray_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:gray_glazed_terracotta',Properties:{facing:'west'}}"); -+ register(3874, "{Name:'minecraft:gray_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:gray_glazed_terracotta',Properties:{facing:'north'}}"); -+ register(3875, "{Name:'minecraft:gray_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:gray_glazed_terracotta',Properties:{facing:'east'}}"); -+ register(3888, "{Name:'minecraft:light_gray_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:silver_glazed_terracotta',Properties:{facing:'south'}}"); -+ register(3889, "{Name:'minecraft:light_gray_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:silver_glazed_terracotta',Properties:{facing:'west'}}"); -+ register(3890, "{Name:'minecraft:light_gray_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:silver_glazed_terracotta',Properties:{facing:'north'}}"); -+ register(3891, "{Name:'minecraft:light_gray_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:silver_glazed_terracotta',Properties:{facing:'east'}}"); -+ register(3904, "{Name:'minecraft:cyan_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:cyan_glazed_terracotta',Properties:{facing:'south'}}"); -+ register(3905, "{Name:'minecraft:cyan_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:cyan_glazed_terracotta',Properties:{facing:'west'}}"); -+ register(3906, "{Name:'minecraft:cyan_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:cyan_glazed_terracotta',Properties:{facing:'north'}}"); -+ register(3907, "{Name:'minecraft:cyan_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:cyan_glazed_terracotta',Properties:{facing:'east'}}"); -+ register(3920, "{Name:'minecraft:purple_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:purple_glazed_terracotta',Properties:{facing:'south'}}"); -+ register(3921, "{Name:'minecraft:purple_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:purple_glazed_terracotta',Properties:{facing:'west'}}"); -+ register(3922, "{Name:'minecraft:purple_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:purple_glazed_terracotta',Properties:{facing:'north'}}"); -+ register(3923, "{Name:'minecraft:purple_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:purple_glazed_terracotta',Properties:{facing:'east'}}"); -+ register(3936, "{Name:'minecraft:blue_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:blue_glazed_terracotta',Properties:{facing:'south'}}"); -+ register(3937, "{Name:'minecraft:blue_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:blue_glazed_terracotta',Properties:{facing:'west'}}"); -+ register(3938, "{Name:'minecraft:blue_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:blue_glazed_terracotta',Properties:{facing:'north'}}"); -+ register(3939, "{Name:'minecraft:blue_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:blue_glazed_terracotta',Properties:{facing:'east'}}"); -+ register(3952, "{Name:'minecraft:brown_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:brown_glazed_terracotta',Properties:{facing:'south'}}"); -+ register(3953, "{Name:'minecraft:brown_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:brown_glazed_terracotta',Properties:{facing:'west'}}"); -+ register(3954, "{Name:'minecraft:brown_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:brown_glazed_terracotta',Properties:{facing:'north'}}"); -+ register(3955, "{Name:'minecraft:brown_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:brown_glazed_terracotta',Properties:{facing:'east'}}"); -+ register(3968, "{Name:'minecraft:green_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:green_glazed_terracotta',Properties:{facing:'south'}}"); -+ register(3969, "{Name:'minecraft:green_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:green_glazed_terracotta',Properties:{facing:'west'}}"); -+ register(3970, "{Name:'minecraft:green_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:green_glazed_terracotta',Properties:{facing:'north'}}"); -+ register(3971, "{Name:'minecraft:green_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:green_glazed_terracotta',Properties:{facing:'east'}}"); -+ register(3984, "{Name:'minecraft:red_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:red_glazed_terracotta',Properties:{facing:'south'}}"); -+ register(3985, "{Name:'minecraft:red_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:red_glazed_terracotta',Properties:{facing:'west'}}"); -+ register(3986, "{Name:'minecraft:red_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:red_glazed_terracotta',Properties:{facing:'north'}}"); -+ register(3987, "{Name:'minecraft:red_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:red_glazed_terracotta',Properties:{facing:'east'}}"); -+ register(4000, "{Name:'minecraft:black_glazed_terracotta',Properties:{facing:'south'}}", "{Name:'minecraft:black_glazed_terracotta',Properties:{facing:'south'}}"); -+ register(4001, "{Name:'minecraft:black_glazed_terracotta',Properties:{facing:'west'}}", "{Name:'minecraft:black_glazed_terracotta',Properties:{facing:'west'}}"); -+ register(4002, "{Name:'minecraft:black_glazed_terracotta',Properties:{facing:'north'}}", "{Name:'minecraft:black_glazed_terracotta',Properties:{facing:'north'}}"); -+ register(4003, "{Name:'minecraft:black_glazed_terracotta',Properties:{facing:'east'}}", "{Name:'minecraft:black_glazed_terracotta',Properties:{facing:'east'}}"); -+ register(4016, "{Name:'minecraft:white_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'white'}}"); -+ register(4017, "{Name:'minecraft:orange_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'orange'}}"); -+ register(4018, "{Name:'minecraft:magenta_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'magenta'}}"); -+ register(4019, "{Name:'minecraft:light_blue_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'light_blue'}}"); -+ register(4020, "{Name:'minecraft:yellow_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'yellow'}}"); -+ register(4021, "{Name:'minecraft:lime_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'lime'}}"); -+ register(4022, "{Name:'minecraft:pink_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'pink'}}"); -+ register(4023, "{Name:'minecraft:gray_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'gray'}}"); -+ register(4024, "{Name:'minecraft:light_gray_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'silver'}}"); -+ register(4025, "{Name:'minecraft:cyan_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'cyan'}}"); -+ register(4026, "{Name:'minecraft:purple_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'purple'}}"); -+ register(4027, "{Name:'minecraft:blue_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'blue'}}"); -+ register(4028, "{Name:'minecraft:brown_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'brown'}}"); -+ register(4029, "{Name:'minecraft:green_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'green'}}"); -+ register(4030, "{Name:'minecraft:red_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'red'}}"); -+ register(4031, "{Name:'minecraft:black_concrete'}", "{Name:'minecraft:concrete',Properties:{color:'black'}}"); -+ register(4032, "{Name:'minecraft:white_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'white'}}"); -+ register(4033, "{Name:'minecraft:orange_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'orange'}}"); -+ register(4034, "{Name:'minecraft:magenta_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'magenta'}}"); -+ register(4035, "{Name:'minecraft:light_blue_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'light_blue'}}"); -+ register(4036, "{Name:'minecraft:yellow_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'yellow'}}"); -+ register(4037, "{Name:'minecraft:lime_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'lime'}}"); -+ register(4038, "{Name:'minecraft:pink_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'pink'}}"); -+ register(4039, "{Name:'minecraft:gray_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'gray'}}"); -+ register(4040, "{Name:'minecraft:light_gray_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'silver'}}"); -+ register(4041, "{Name:'minecraft:cyan_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'cyan'}}"); -+ register(4042, "{Name:'minecraft:purple_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'purple'}}"); -+ register(4043, "{Name:'minecraft:blue_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'blue'}}"); -+ register(4044, "{Name:'minecraft:brown_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'brown'}}"); -+ register(4045, "{Name:'minecraft:green_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'green'}}"); -+ register(4046, "{Name:'minecraft:red_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'red'}}"); -+ register(4047, "{Name:'minecraft:black_concrete_powder'}", "{Name:'minecraft:concrete_powder',Properties:{color:'black'}}"); -+ register(4080, "{Name:'minecraft:structure_block',Properties:{mode:'save'}}", "{Name:'minecraft:structure_block',Properties:{mode:'save'}}"); -+ register(4081, "{Name:'minecraft:structure_block',Properties:{mode:'load'}}", "{Name:'minecraft:structure_block',Properties:{mode:'load'}}"); -+ register(4082, "{Name:'minecraft:structure_block',Properties:{mode:'corner'}}", "{Name:'minecraft:structure_block',Properties:{mode:'corner'}}"); -+ register(4083, "{Name:'minecraft:structure_block',Properties:{mode:'data'}}", "{Name:'minecraft:structure_block',Properties:{mode:'data'}}"); -+ finalizeMaps(); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperItemNameV102.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperItemNameV102.java -new file mode 100644 -index 0000000000000000000000000000000000000000..86f6aa3e3fa886976809f350fc5eb16f6a026ed9 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperItemNameV102.java -@@ -0,0 +1,533 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.helpers; -+ -+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -+ -+public final class HelperItemNameV102 { -+ -+ // This class is responsible for mapping the id -> string update in itemstacks and potions -+ -+ private static final Int2ObjectOpenHashMap ITEM_NAMES = new Int2ObjectOpenHashMap() { -+ @Override -+ public String put(final int k, final String o) { -+ final String ret = super.put(k, o); -+ -+ if (ret != null) { -+ throw new IllegalStateException("Mapping already exists for " + k + ": prev: " + ret + ", new: " + o); -+ } -+ -+ return ret; -+ } -+ }; -+ -+ static { -+ ITEM_NAMES.put(0, "minecraft:air"); -+ ITEM_NAMES.put(1, "minecraft:stone"); -+ ITEM_NAMES.put(2, "minecraft:grass"); -+ ITEM_NAMES.put(3, "minecraft:dirt"); -+ ITEM_NAMES.put(4, "minecraft:cobblestone"); -+ ITEM_NAMES.put(5, "minecraft:planks"); -+ ITEM_NAMES.put(6, "minecraft:sapling"); -+ ITEM_NAMES.put(7, "minecraft:bedrock"); -+ ITEM_NAMES.put(8, "minecraft:flowing_water"); -+ ITEM_NAMES.put(9, "minecraft:water"); -+ ITEM_NAMES.put(10, "minecraft:flowing_lava"); -+ ITEM_NAMES.put(11, "minecraft:lava"); -+ ITEM_NAMES.put(12, "minecraft:sand"); -+ ITEM_NAMES.put(13, "minecraft:gravel"); -+ ITEM_NAMES.put(14, "minecraft:gold_ore"); -+ ITEM_NAMES.put(15, "minecraft:iron_ore"); -+ ITEM_NAMES.put(16, "minecraft:coal_ore"); -+ ITEM_NAMES.put(17, "minecraft:log"); -+ ITEM_NAMES.put(18, "minecraft:leaves"); -+ ITEM_NAMES.put(19, "minecraft:sponge"); -+ ITEM_NAMES.put(20, "minecraft:glass"); -+ ITEM_NAMES.put(21, "minecraft:lapis_ore"); -+ ITEM_NAMES.put(22, "minecraft:lapis_block"); -+ ITEM_NAMES.put(23, "minecraft:dispenser"); -+ ITEM_NAMES.put(24, "minecraft:sandstone"); -+ ITEM_NAMES.put(25, "minecraft:noteblock"); -+ ITEM_NAMES.put(27, "minecraft:golden_rail"); -+ ITEM_NAMES.put(28, "minecraft:detector_rail"); -+ ITEM_NAMES.put(29, "minecraft:sticky_piston"); -+ ITEM_NAMES.put(30, "minecraft:web"); -+ ITEM_NAMES.put(31, "minecraft:tallgrass"); -+ ITEM_NAMES.put(32, "minecraft:deadbush"); -+ ITEM_NAMES.put(33, "minecraft:piston"); -+ ITEM_NAMES.put(35, "minecraft:wool"); -+ ITEM_NAMES.put(37, "minecraft:yellow_flower"); -+ ITEM_NAMES.put(38, "minecraft:red_flower"); -+ ITEM_NAMES.put(39, "minecraft:brown_mushroom"); -+ ITEM_NAMES.put(40, "minecraft:red_mushroom"); -+ ITEM_NAMES.put(41, "minecraft:gold_block"); -+ ITEM_NAMES.put(42, "minecraft:iron_block"); -+ ITEM_NAMES.put(43, "minecraft:double_stone_slab"); -+ ITEM_NAMES.put(44, "minecraft:stone_slab"); -+ ITEM_NAMES.put(45, "minecraft:brick_block"); -+ ITEM_NAMES.put(46, "minecraft:tnt"); -+ ITEM_NAMES.put(47, "minecraft:bookshelf"); -+ ITEM_NAMES.put(48, "minecraft:mossy_cobblestone"); -+ ITEM_NAMES.put(49, "minecraft:obsidian"); -+ ITEM_NAMES.put(50, "minecraft:torch"); -+ ITEM_NAMES.put(51, "minecraft:fire"); -+ ITEM_NAMES.put(52, "minecraft:mob_spawner"); -+ ITEM_NAMES.put(53, "minecraft:oak_stairs"); -+ ITEM_NAMES.put(54, "minecraft:chest"); -+ ITEM_NAMES.put(56, "minecraft:diamond_ore"); -+ ITEM_NAMES.put(57, "minecraft:diamond_block"); -+ ITEM_NAMES.put(58, "minecraft:crafting_table"); -+ ITEM_NAMES.put(60, "minecraft:farmland"); -+ ITEM_NAMES.put(61, "minecraft:furnace"); -+ ITEM_NAMES.put(62, "minecraft:lit_furnace"); -+ ITEM_NAMES.put(65, "minecraft:ladder"); -+ ITEM_NAMES.put(66, "minecraft:rail"); -+ ITEM_NAMES.put(67, "minecraft:stone_stairs"); -+ ITEM_NAMES.put(69, "minecraft:lever"); -+ ITEM_NAMES.put(70, "minecraft:stone_pressure_plate"); -+ ITEM_NAMES.put(72, "minecraft:wooden_pressure_plate"); -+ ITEM_NAMES.put(73, "minecraft:redstone_ore"); -+ ITEM_NAMES.put(76, "minecraft:redstone_torch"); -+ ITEM_NAMES.put(77, "minecraft:stone_button"); -+ ITEM_NAMES.put(78, "minecraft:snow_layer"); -+ ITEM_NAMES.put(79, "minecraft:ice"); -+ ITEM_NAMES.put(80, "minecraft:snow"); -+ ITEM_NAMES.put(81, "minecraft:cactus"); -+ ITEM_NAMES.put(82, "minecraft:clay"); -+ ITEM_NAMES.put(84, "minecraft:jukebox"); -+ ITEM_NAMES.put(85, "minecraft:fence"); -+ ITEM_NAMES.put(86, "minecraft:pumpkin"); -+ ITEM_NAMES.put(87, "minecraft:netherrack"); -+ ITEM_NAMES.put(88, "minecraft:soul_sand"); -+ ITEM_NAMES.put(89, "minecraft:glowstone"); -+ ITEM_NAMES.put(90, "minecraft:portal"); -+ ITEM_NAMES.put(91, "minecraft:lit_pumpkin"); -+ ITEM_NAMES.put(95, "minecraft:stained_glass"); -+ ITEM_NAMES.put(96, "minecraft:trapdoor"); -+ ITEM_NAMES.put(97, "minecraft:monster_egg"); -+ ITEM_NAMES.put(98, "minecraft:stonebrick"); -+ ITEM_NAMES.put(99, "minecraft:brown_mushroom_block"); -+ ITEM_NAMES.put(100, "minecraft:red_mushroom_block"); -+ ITEM_NAMES.put(101, "minecraft:iron_bars"); -+ ITEM_NAMES.put(102, "minecraft:glass_pane"); -+ ITEM_NAMES.put(103, "minecraft:melon_block"); -+ ITEM_NAMES.put(106, "minecraft:vine"); -+ ITEM_NAMES.put(107, "minecraft:fence_gate"); -+ ITEM_NAMES.put(108, "minecraft:brick_stairs"); -+ ITEM_NAMES.put(109, "minecraft:stone_brick_stairs"); -+ ITEM_NAMES.put(110, "minecraft:mycelium"); -+ ITEM_NAMES.put(111, "minecraft:waterlily"); -+ ITEM_NAMES.put(112, "minecraft:nether_brick"); -+ ITEM_NAMES.put(113, "minecraft:nether_brick_fence"); -+ ITEM_NAMES.put(114, "minecraft:nether_brick_stairs"); -+ ITEM_NAMES.put(116, "minecraft:enchanting_table"); -+ ITEM_NAMES.put(119, "minecraft:end_portal"); -+ ITEM_NAMES.put(120, "minecraft:end_portal_frame"); -+ ITEM_NAMES.put(121, "minecraft:end_stone"); -+ ITEM_NAMES.put(122, "minecraft:dragon_egg"); -+ ITEM_NAMES.put(123, "minecraft:redstone_lamp"); -+ ITEM_NAMES.put(125, "minecraft:double_wooden_slab"); -+ ITEM_NAMES.put(126, "minecraft:wooden_slab"); -+ ITEM_NAMES.put(127, "minecraft:cocoa"); -+ ITEM_NAMES.put(128, "minecraft:sandstone_stairs"); -+ ITEM_NAMES.put(129, "minecraft:emerald_ore"); -+ ITEM_NAMES.put(130, "minecraft:ender_chest"); -+ ITEM_NAMES.put(131, "minecraft:tripwire_hook"); -+ ITEM_NAMES.put(133, "minecraft:emerald_block"); -+ ITEM_NAMES.put(134, "minecraft:spruce_stairs"); -+ ITEM_NAMES.put(135, "minecraft:birch_stairs"); -+ ITEM_NAMES.put(136, "minecraft:jungle_stairs"); -+ ITEM_NAMES.put(137, "minecraft:command_block"); -+ ITEM_NAMES.put(138, "minecraft:beacon"); -+ ITEM_NAMES.put(139, "minecraft:cobblestone_wall"); -+ ITEM_NAMES.put(141, "minecraft:carrots"); -+ ITEM_NAMES.put(142, "minecraft:potatoes"); -+ ITEM_NAMES.put(143, "minecraft:wooden_button"); -+ ITEM_NAMES.put(145, "minecraft:anvil"); -+ ITEM_NAMES.put(146, "minecraft:trapped_chest"); -+ ITEM_NAMES.put(147, "minecraft:light_weighted_pressure_plate"); -+ ITEM_NAMES.put(148, "minecraft:heavy_weighted_pressure_plate"); -+ ITEM_NAMES.put(151, "minecraft:daylight_detector"); -+ ITEM_NAMES.put(152, "minecraft:redstone_block"); -+ ITEM_NAMES.put(153, "minecraft:quartz_ore"); -+ ITEM_NAMES.put(154, "minecraft:hopper"); -+ ITEM_NAMES.put(155, "minecraft:quartz_block"); -+ ITEM_NAMES.put(156, "minecraft:quartz_stairs"); -+ ITEM_NAMES.put(157, "minecraft:activator_rail"); -+ ITEM_NAMES.put(158, "minecraft:dropper"); -+ ITEM_NAMES.put(159, "minecraft:stained_hardened_clay"); -+ ITEM_NAMES.put(160, "minecraft:stained_glass_pane"); -+ ITEM_NAMES.put(161, "minecraft:leaves2"); -+ ITEM_NAMES.put(162, "minecraft:log2"); -+ ITEM_NAMES.put(163, "minecraft:acacia_stairs"); -+ ITEM_NAMES.put(164, "minecraft:dark_oak_stairs"); -+ ITEM_NAMES.put(170, "minecraft:hay_block"); -+ ITEM_NAMES.put(171, "minecraft:carpet"); -+ ITEM_NAMES.put(172, "minecraft:hardened_clay"); -+ ITEM_NAMES.put(173, "minecraft:coal_block"); -+ ITEM_NAMES.put(174, "minecraft:packed_ice"); -+ ITEM_NAMES.put(175, "minecraft:double_plant"); -+ ITEM_NAMES.put(256, "minecraft:iron_shovel"); -+ ITEM_NAMES.put(257, "minecraft:iron_pickaxe"); -+ ITEM_NAMES.put(258, "minecraft:iron_axe"); -+ ITEM_NAMES.put(259, "minecraft:flint_and_steel"); -+ ITEM_NAMES.put(260, "minecraft:apple"); -+ ITEM_NAMES.put(261, "minecraft:bow"); -+ ITEM_NAMES.put(262, "minecraft:arrow"); -+ ITEM_NAMES.put(263, "minecraft:coal"); -+ ITEM_NAMES.put(264, "minecraft:diamond"); -+ ITEM_NAMES.put(265, "minecraft:iron_ingot"); -+ ITEM_NAMES.put(266, "minecraft:gold_ingot"); -+ ITEM_NAMES.put(267, "minecraft:iron_sword"); -+ ITEM_NAMES.put(268, "minecraft:wooden_sword"); -+ ITEM_NAMES.put(269, "minecraft:wooden_shovel"); -+ ITEM_NAMES.put(270, "minecraft:wooden_pickaxe"); -+ ITEM_NAMES.put(271, "minecraft:wooden_axe"); -+ ITEM_NAMES.put(272, "minecraft:stone_sword"); -+ ITEM_NAMES.put(273, "minecraft:stone_shovel"); -+ ITEM_NAMES.put(274, "minecraft:stone_pickaxe"); -+ ITEM_NAMES.put(275, "minecraft:stone_axe"); -+ ITEM_NAMES.put(276, "minecraft:diamond_sword"); -+ ITEM_NAMES.put(277, "minecraft:diamond_shovel"); -+ ITEM_NAMES.put(278, "minecraft:diamond_pickaxe"); -+ ITEM_NAMES.put(279, "minecraft:diamond_axe"); -+ ITEM_NAMES.put(280, "minecraft:stick"); -+ ITEM_NAMES.put(281, "minecraft:bowl"); -+ ITEM_NAMES.put(282, "minecraft:mushroom_stew"); -+ ITEM_NAMES.put(283, "minecraft:golden_sword"); -+ ITEM_NAMES.put(284, "minecraft:golden_shovel"); -+ ITEM_NAMES.put(285, "minecraft:golden_pickaxe"); -+ ITEM_NAMES.put(286, "minecraft:golden_axe"); -+ ITEM_NAMES.put(287, "minecraft:string"); -+ ITEM_NAMES.put(288, "minecraft:feather"); -+ ITEM_NAMES.put(289, "minecraft:gunpowder"); -+ ITEM_NAMES.put(290, "minecraft:wooden_hoe"); -+ ITEM_NAMES.put(291, "minecraft:stone_hoe"); -+ ITEM_NAMES.put(292, "minecraft:iron_hoe"); -+ ITEM_NAMES.put(293, "minecraft:diamond_hoe"); -+ ITEM_NAMES.put(294, "minecraft:golden_hoe"); -+ ITEM_NAMES.put(295, "minecraft:wheat_seeds"); -+ ITEM_NAMES.put(296, "minecraft:wheat"); -+ ITEM_NAMES.put(297, "minecraft:bread"); -+ ITEM_NAMES.put(298, "minecraft:leather_helmet"); -+ ITEM_NAMES.put(299, "minecraft:leather_chestplate"); -+ ITEM_NAMES.put(300, "minecraft:leather_leggings"); -+ ITEM_NAMES.put(301, "minecraft:leather_boots"); -+ ITEM_NAMES.put(302, "minecraft:chainmail_helmet"); -+ ITEM_NAMES.put(303, "minecraft:chainmail_chestplate"); -+ ITEM_NAMES.put(304, "minecraft:chainmail_leggings"); -+ ITEM_NAMES.put(305, "minecraft:chainmail_boots"); -+ ITEM_NAMES.put(306, "minecraft:iron_helmet"); -+ ITEM_NAMES.put(307, "minecraft:iron_chestplate"); -+ ITEM_NAMES.put(308, "minecraft:iron_leggings"); -+ ITEM_NAMES.put(309, "minecraft:iron_boots"); -+ ITEM_NAMES.put(310, "minecraft:diamond_helmet"); -+ ITEM_NAMES.put(311, "minecraft:diamond_chestplate"); -+ ITEM_NAMES.put(312, "minecraft:diamond_leggings"); -+ ITEM_NAMES.put(313, "minecraft:diamond_boots"); -+ ITEM_NAMES.put(314, "minecraft:golden_helmet"); -+ ITEM_NAMES.put(315, "minecraft:golden_chestplate"); -+ ITEM_NAMES.put(316, "minecraft:golden_leggings"); -+ ITEM_NAMES.put(317, "minecraft:golden_boots"); -+ ITEM_NAMES.put(318, "minecraft:flint"); -+ ITEM_NAMES.put(319, "minecraft:porkchop"); -+ ITEM_NAMES.put(320, "minecraft:cooked_porkchop"); -+ ITEM_NAMES.put(321, "minecraft:painting"); -+ ITEM_NAMES.put(322, "minecraft:golden_apple"); -+ ITEM_NAMES.put(323, "minecraft:sign"); -+ ITEM_NAMES.put(324, "minecraft:wooden_door"); -+ ITEM_NAMES.put(325, "minecraft:bucket"); -+ ITEM_NAMES.put(326, "minecraft:water_bucket"); -+ ITEM_NAMES.put(327, "minecraft:lava_bucket"); -+ ITEM_NAMES.put(328, "minecraft:minecart"); -+ ITEM_NAMES.put(329, "minecraft:saddle"); -+ ITEM_NAMES.put(330, "minecraft:iron_door"); -+ ITEM_NAMES.put(331, "minecraft:redstone"); -+ ITEM_NAMES.put(332, "minecraft:snowball"); -+ ITEM_NAMES.put(333, "minecraft:boat"); -+ ITEM_NAMES.put(334, "minecraft:leather"); -+ ITEM_NAMES.put(335, "minecraft:milk_bucket"); -+ ITEM_NAMES.put(336, "minecraft:brick"); -+ ITEM_NAMES.put(337, "minecraft:clay_ball"); -+ ITEM_NAMES.put(338, "minecraft:reeds"); -+ ITEM_NAMES.put(339, "minecraft:paper"); -+ ITEM_NAMES.put(340, "minecraft:book"); -+ ITEM_NAMES.put(341, "minecraft:slime_ball"); -+ ITEM_NAMES.put(342, "minecraft:chest_minecart"); -+ ITEM_NAMES.put(343, "minecraft:furnace_minecart"); -+ ITEM_NAMES.put(344, "minecraft:egg"); -+ ITEM_NAMES.put(345, "minecraft:compass"); -+ ITEM_NAMES.put(346, "minecraft:fishing_rod"); -+ ITEM_NAMES.put(347, "minecraft:clock"); -+ ITEM_NAMES.put(348, "minecraft:glowstone_dust"); -+ ITEM_NAMES.put(349, "minecraft:fish"); -+ ITEM_NAMES.put(350, "minecraft:cooked_fish"); // Fix typo, the game never recognized cooked_fished -+ ITEM_NAMES.put(351, "minecraft:dye"); -+ ITEM_NAMES.put(352, "minecraft:bone"); -+ ITEM_NAMES.put(353, "minecraft:sugar"); -+ ITEM_NAMES.put(354, "minecraft:cake"); -+ ITEM_NAMES.put(355, "minecraft:bed"); -+ ITEM_NAMES.put(356, "minecraft:repeater"); -+ ITEM_NAMES.put(357, "minecraft:cookie"); -+ ITEM_NAMES.put(358, "minecraft:filled_map"); -+ ITEM_NAMES.put(359, "minecraft:shears"); -+ ITEM_NAMES.put(360, "minecraft:melon"); -+ ITEM_NAMES.put(361, "minecraft:pumpkin_seeds"); -+ ITEM_NAMES.put(362, "minecraft:melon_seeds"); -+ ITEM_NAMES.put(363, "minecraft:beef"); -+ ITEM_NAMES.put(364, "minecraft:cooked_beef"); -+ ITEM_NAMES.put(365, "minecraft:chicken"); -+ ITEM_NAMES.put(366, "minecraft:cooked_chicken"); -+ ITEM_NAMES.put(367, "minecraft:rotten_flesh"); -+ ITEM_NAMES.put(368, "minecraft:ender_pearl"); -+ ITEM_NAMES.put(369, "minecraft:blaze_rod"); -+ ITEM_NAMES.put(370, "minecraft:ghast_tear"); -+ ITEM_NAMES.put(371, "minecraft:gold_nugget"); -+ ITEM_NAMES.put(372, "minecraft:nether_wart"); -+ ITEM_NAMES.put(373, "minecraft:potion"); -+ ITEM_NAMES.put(374, "minecraft:glass_bottle"); -+ ITEM_NAMES.put(375, "minecraft:spider_eye"); -+ ITEM_NAMES.put(376, "minecraft:fermented_spider_eye"); -+ ITEM_NAMES.put(377, "minecraft:blaze_powder"); -+ ITEM_NAMES.put(378, "minecraft:magma_cream"); -+ ITEM_NAMES.put(379, "minecraft:brewing_stand"); -+ ITEM_NAMES.put(380, "minecraft:cauldron"); -+ ITEM_NAMES.put(381, "minecraft:ender_eye"); -+ ITEM_NAMES.put(382, "minecraft:speckled_melon"); -+ ITEM_NAMES.put(383, "minecraft:spawn_egg"); -+ ITEM_NAMES.put(384, "minecraft:experience_bottle"); -+ ITEM_NAMES.put(385, "minecraft:fire_charge"); -+ ITEM_NAMES.put(386, "minecraft:writable_book"); -+ ITEM_NAMES.put(387, "minecraft:written_book"); -+ ITEM_NAMES.put(388, "minecraft:emerald"); -+ ITEM_NAMES.put(389, "minecraft:item_frame"); -+ ITEM_NAMES.put(390, "minecraft:flower_pot"); -+ ITEM_NAMES.put(391, "minecraft:carrot"); -+ ITEM_NAMES.put(392, "minecraft:potato"); -+ ITEM_NAMES.put(393, "minecraft:baked_potato"); -+ ITEM_NAMES.put(394, "minecraft:poisonous_potato"); -+ ITEM_NAMES.put(395, "minecraft:map"); -+ ITEM_NAMES.put(396, "minecraft:golden_carrot"); -+ ITEM_NAMES.put(397, "minecraft:skull"); -+ ITEM_NAMES.put(398, "minecraft:carrot_on_a_stick"); -+ ITEM_NAMES.put(399, "minecraft:nether_star"); -+ ITEM_NAMES.put(400, "minecraft:pumpkin_pie"); -+ ITEM_NAMES.put(401, "minecraft:fireworks"); -+ ITEM_NAMES.put(402, "minecraft:firework_charge"); -+ ITEM_NAMES.put(403, "minecraft:enchanted_book"); -+ ITEM_NAMES.put(404, "minecraft:comparator"); -+ ITEM_NAMES.put(405, "minecraft:netherbrick"); -+ ITEM_NAMES.put(406, "minecraft:quartz"); -+ ITEM_NAMES.put(407, "minecraft:tnt_minecart"); -+ ITEM_NAMES.put(408, "minecraft:hopper_minecart"); -+ ITEM_NAMES.put(417, "minecraft:iron_horse_armor"); -+ ITEM_NAMES.put(418, "minecraft:golden_horse_armor"); -+ ITEM_NAMES.put(419, "minecraft:diamond_horse_armor"); -+ ITEM_NAMES.put(420, "minecraft:lead"); -+ ITEM_NAMES.put(421, "minecraft:name_tag"); -+ ITEM_NAMES.put(422, "minecraft:command_block_minecart"); -+ ITEM_NAMES.put(2256, "minecraft:record_13"); -+ ITEM_NAMES.put(2257, "minecraft:record_cat"); -+ ITEM_NAMES.put(2258, "minecraft:record_blocks"); -+ ITEM_NAMES.put(2259, "minecraft:record_chirp"); -+ ITEM_NAMES.put(2260, "minecraft:record_far"); -+ ITEM_NAMES.put(2261, "minecraft:record_mall"); -+ ITEM_NAMES.put(2262, "minecraft:record_mellohi"); -+ ITEM_NAMES.put(2263, "minecraft:record_stal"); -+ ITEM_NAMES.put(2264, "minecraft:record_strad"); -+ ITEM_NAMES.put(2265, "minecraft:record_ward"); -+ ITEM_NAMES.put(2266, "minecraft:record_11"); -+ ITEM_NAMES.put(2267, "minecraft:record_wait"); -+ // https://github.com/starlis/empirecraft/commit/2da59d1901407fc0c135ef0458c0fe9b016570b3 -+ // It's likely that this is a result of old CB/Spigot behavior still writing ids into items as ints. -+ // These ids do not appear to be used by regular MC anyways, so I do not see the harm of porting it here. -+ // Extras can be added if needed -+ String[] extra = new String[4_000]; -+ // EMC start -+ extra[409] = "minecraft:prismarine_shard"; -+ extra[410] = "minecraft:prismarine_crystals"; -+ extra[411] = "minecraft:rabbit"; -+ extra[412] = "minecraft:cooked_rabbit"; -+ extra[413] = "minecraft:rabbit_stew"; -+ extra[414] = "minecraft:rabbit_foot"; -+ extra[415] = "minecraft:rabbit_hide"; -+ extra[416] = "minecraft:armor_stand"; -+ extra[423] = "minecraft:mutton"; -+ extra[424] = "minecraft:cooked_mutton"; -+ extra[425] = "minecraft:banner"; -+ extra[426] = "minecraft:end_crystal"; -+ extra[427] = "minecraft:spruce_door"; -+ extra[428] = "minecraft:birch_door"; -+ extra[429] = "minecraft:jungle_door"; -+ extra[430] = "minecraft:acacia_door"; -+ extra[431] = "minecraft:dark_oak_door"; -+ extra[432] = "minecraft:chorus_fruit"; -+ extra[433] = "minecraft:chorus_fruit_popped"; -+ extra[434] = "minecraft:beetroot"; -+ extra[435] = "minecraft:beetroot_seeds"; -+ extra[436] = "minecraft:beetroot_soup"; -+ extra[437] = "minecraft:dragon_breath"; -+ extra[438] = "minecraft:splash_potion"; -+ extra[439] = "minecraft:spectral_arrow"; -+ extra[440] = "minecraft:tipped_arrow"; -+ extra[441] = "minecraft:lingering_potion"; -+ extra[442] = "minecraft:shield"; -+ extra[443] = "minecraft:elytra"; -+ extra[444] = "minecraft:spruce_boat"; -+ extra[445] = "minecraft:birch_boat"; -+ extra[446] = "minecraft:jungle_boat"; -+ extra[447] = "minecraft:acacia_boat"; -+ extra[448] = "minecraft:dark_oak_boat"; -+ extra[449] = "minecraft:totem_of_undying"; -+ extra[450] = "minecraft:shulker_shell"; -+ extra[452] = "minecraft:iron_nugget"; -+ extra[453] = "minecraft:knowledge_book"; -+ // EMC end -+ -+ // dump extra into map -+ for (int i = 0; i < extra.length; ++i) { -+ if (extra[i] != null) { -+ ITEM_NAMES.put(i, extra[i]); -+ } -+ } -+ } -+ -+ private static final String[] POTION_NAMES = new String[128]; -+ static { -+ POTION_NAMES[0] = "minecraft:water"; -+ POTION_NAMES[1] = "minecraft:regeneration"; -+ POTION_NAMES[2] = "minecraft:swiftness"; -+ POTION_NAMES[3] = "minecraft:fire_resistance"; -+ POTION_NAMES[4] = "minecraft:poison"; -+ POTION_NAMES[5] = "minecraft:healing"; -+ POTION_NAMES[6] = "minecraft:night_vision"; -+ POTION_NAMES[7] = null; -+ POTION_NAMES[8] = "minecraft:weakness"; -+ POTION_NAMES[9] = "minecraft:strength"; -+ POTION_NAMES[10] = "minecraft:slowness"; -+ POTION_NAMES[11] = "minecraft:leaping"; -+ POTION_NAMES[12] = "minecraft:harming"; -+ POTION_NAMES[13] = "minecraft:water_breathing"; -+ POTION_NAMES[14] = "minecraft:invisibility"; -+ POTION_NAMES[15] = null; -+ POTION_NAMES[16] = "minecraft:awkward"; -+ POTION_NAMES[17] = "minecraft:regeneration"; -+ POTION_NAMES[18] = "minecraft:swiftness"; -+ POTION_NAMES[19] = "minecraft:fire_resistance"; -+ POTION_NAMES[20] = "minecraft:poison"; -+ POTION_NAMES[21] = "minecraft:healing"; -+ POTION_NAMES[22] = "minecraft:night_vision"; -+ POTION_NAMES[23] = null; -+ POTION_NAMES[24] = "minecraft:weakness"; -+ POTION_NAMES[25] = "minecraft:strength"; -+ POTION_NAMES[26] = "minecraft:slowness"; -+ POTION_NAMES[27] = "minecraft:leaping"; -+ POTION_NAMES[28] = "minecraft:harming"; -+ POTION_NAMES[29] = "minecraft:water_breathing"; -+ POTION_NAMES[30] = "minecraft:invisibility"; -+ POTION_NAMES[31] = null; -+ POTION_NAMES[32] = "minecraft:thick"; -+ POTION_NAMES[33] = "minecraft:strong_regeneration"; -+ POTION_NAMES[34] = "minecraft:strong_swiftness"; -+ POTION_NAMES[35] = "minecraft:fire_resistance"; -+ POTION_NAMES[36] = "minecraft:strong_poison"; -+ POTION_NAMES[37] = "minecraft:strong_healing"; -+ POTION_NAMES[38] = "minecraft:night_vision"; -+ POTION_NAMES[39] = null; -+ POTION_NAMES[40] = "minecraft:weakness"; -+ POTION_NAMES[41] = "minecraft:strong_strength"; -+ POTION_NAMES[42] = "minecraft:slowness"; -+ POTION_NAMES[43] = "minecraft:strong_leaping"; -+ POTION_NAMES[44] = "minecraft:strong_harming"; -+ POTION_NAMES[45] = "minecraft:water_breathing"; -+ POTION_NAMES[46] = "minecraft:invisibility"; -+ POTION_NAMES[47] = null; -+ POTION_NAMES[48] = null; -+ POTION_NAMES[49] = "minecraft:strong_regeneration"; -+ POTION_NAMES[50] = "minecraft:strong_swiftness"; -+ POTION_NAMES[51] = "minecraft:fire_resistance"; -+ POTION_NAMES[52] = "minecraft:strong_poison"; -+ POTION_NAMES[53] = "minecraft:strong_healing"; -+ POTION_NAMES[54] = "minecraft:night_vision"; -+ POTION_NAMES[55] = null; -+ POTION_NAMES[56] = "minecraft:weakness"; -+ POTION_NAMES[57] = "minecraft:strong_strength"; -+ POTION_NAMES[58] = "minecraft:slowness"; -+ POTION_NAMES[59] = "minecraft:strong_leaping"; -+ POTION_NAMES[60] = "minecraft:strong_harming"; -+ POTION_NAMES[61] = "minecraft:water_breathing"; -+ POTION_NAMES[62] = "minecraft:invisibility"; -+ POTION_NAMES[63] = null; -+ POTION_NAMES[64] = "minecraft:mundane"; -+ POTION_NAMES[65] = "minecraft:long_regeneration"; -+ POTION_NAMES[66] = "minecraft:long_swiftness"; -+ POTION_NAMES[67] = "minecraft:long_fire_resistance"; -+ POTION_NAMES[68] = "minecraft:long_poison"; -+ POTION_NAMES[69] = "minecraft:healing"; -+ POTION_NAMES[70] = "minecraft:long_night_vision"; -+ POTION_NAMES[71] = null; -+ POTION_NAMES[72] = "minecraft:long_weakness"; -+ POTION_NAMES[73] = "minecraft:long_strength"; -+ POTION_NAMES[74] = "minecraft:long_slowness"; -+ POTION_NAMES[75] = "minecraft:long_leaping"; -+ POTION_NAMES[76] = "minecraft:harming"; -+ POTION_NAMES[77] = "minecraft:long_water_breathing"; -+ POTION_NAMES[78] = "minecraft:long_invisibility"; -+ POTION_NAMES[79] = null; -+ POTION_NAMES[80] = "minecraft:awkward"; -+ POTION_NAMES[81] = "minecraft:long_regeneration"; -+ POTION_NAMES[82] = "minecraft:long_swiftness"; -+ POTION_NAMES[83] = "minecraft:long_fire_resistance"; -+ POTION_NAMES[84] = "minecraft:long_poison"; -+ POTION_NAMES[85] = "minecraft:healing"; -+ POTION_NAMES[86] = "minecraft:long_night_vision"; -+ POTION_NAMES[87] = null; -+ POTION_NAMES[88] = "minecraft:long_weakness"; -+ POTION_NAMES[89] = "minecraft:long_strength"; -+ POTION_NAMES[90] = "minecraft:long_slowness"; -+ POTION_NAMES[91] = "minecraft:long_leaping"; -+ POTION_NAMES[92] = "minecraft:harming"; -+ POTION_NAMES[93] = "minecraft:long_water_breathing"; -+ POTION_NAMES[94] = "minecraft:long_invisibility"; -+ POTION_NAMES[95] = null; -+ POTION_NAMES[96] = "minecraft:thick"; -+ POTION_NAMES[97] = "minecraft:regeneration"; -+ POTION_NAMES[98] = "minecraft:swiftness"; -+ POTION_NAMES[99] = "minecraft:long_fire_resistance"; -+ POTION_NAMES[100] = "minecraft:poison"; -+ POTION_NAMES[101] = "minecraft:strong_healing"; -+ POTION_NAMES[102] = "minecraft:long_night_vision"; -+ POTION_NAMES[103] = null; -+ POTION_NAMES[104] = "minecraft:long_weakness"; -+ POTION_NAMES[105] = "minecraft:strength"; -+ POTION_NAMES[106] = "minecraft:long_slowness"; -+ POTION_NAMES[107] = "minecraft:leaping"; -+ POTION_NAMES[108] = "minecraft:strong_harming"; -+ POTION_NAMES[109] = "minecraft:long_water_breathing"; -+ POTION_NAMES[110] = "minecraft:long_invisibility"; -+ POTION_NAMES[111] = null; -+ POTION_NAMES[112] = null; -+ POTION_NAMES[113] = "minecraft:regeneration"; -+ POTION_NAMES[114] = "minecraft:swiftness"; -+ POTION_NAMES[115] = "minecraft:long_fire_resistance"; -+ POTION_NAMES[116] = "minecraft:poison"; -+ POTION_NAMES[117] = "minecraft:strong_healing"; -+ POTION_NAMES[118] = "minecraft:long_night_vision"; -+ POTION_NAMES[119] = null; -+ POTION_NAMES[120] = "minecraft:long_weakness"; -+ POTION_NAMES[121] = "minecraft:strength"; -+ POTION_NAMES[122] = "minecraft:long_slowness"; -+ POTION_NAMES[123] = "minecraft:leaping"; -+ POTION_NAMES[124] = "minecraft:strong_harming"; -+ POTION_NAMES[125] = "minecraft:long_water_breathing"; -+ POTION_NAMES[126] = "minecraft:long_invisibility"; -+ POTION_NAMES[127] = null; -+ } -+ -+ // ret is nullable, you are supposed to log when it does not exist, NOT HIDE IT! -+ public static String getNameFromId(final int id) { -+ return ITEM_NAMES.get(id); -+ } -+ -+ public static String getPotionNameFromId(final short id) { -+ return POTION_NAMES[id & 127]; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperSpawnEggNameV105.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperSpawnEggNameV105.java -new file mode 100644 -index 0000000000000000000000000000000000000000..bcc586cb68148fd960dd685eecce853169a92ed5 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/HelperSpawnEggNameV105.java -@@ -0,0 +1,77 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.helpers; -+ -+public final class HelperSpawnEggNameV105 { -+ -+ private static final String[] ID_TO_STRING = new String[256]; -+ static { -+ ID_TO_STRING[1] = "Item"; -+ ID_TO_STRING[2] = "XPOrb"; -+ ID_TO_STRING[7] = "ThrownEgg"; -+ ID_TO_STRING[8] = "LeashKnot"; -+ ID_TO_STRING[9] = "Painting"; -+ ID_TO_STRING[10] = "Arrow"; -+ ID_TO_STRING[11] = "Snowball"; -+ ID_TO_STRING[12] = "Fireball"; -+ ID_TO_STRING[13] = "SmallFireball"; -+ ID_TO_STRING[14] = "ThrownEnderpearl"; -+ ID_TO_STRING[15] = "EyeOfEnderSignal"; -+ ID_TO_STRING[16] = "ThrownPotion"; -+ ID_TO_STRING[17] = "ThrownExpBottle"; -+ ID_TO_STRING[18] = "ItemFrame"; -+ ID_TO_STRING[19] = "WitherSkull"; -+ ID_TO_STRING[20] = "PrimedTnt"; -+ ID_TO_STRING[21] = "FallingSand"; -+ ID_TO_STRING[22] = "FireworksRocketEntity"; -+ ID_TO_STRING[23] = "TippedArrow"; -+ ID_TO_STRING[24] = "SpectralArrow"; -+ ID_TO_STRING[25] = "ShulkerBullet"; -+ ID_TO_STRING[26] = "DragonFireball"; -+ ID_TO_STRING[30] = "ArmorStand"; -+ ID_TO_STRING[41] = "Boat"; -+ ID_TO_STRING[42] = "MinecartRideable"; -+ ID_TO_STRING[43] = "MinecartChest"; -+ ID_TO_STRING[44] = "MinecartFurnace"; -+ ID_TO_STRING[45] = "MinecartTNT"; -+ ID_TO_STRING[46] = "MinecartHopper"; -+ ID_TO_STRING[47] = "MinecartSpawner"; -+ ID_TO_STRING[40] = "MinecartCommandBlock"; -+ ID_TO_STRING[50] = "Creeper"; -+ ID_TO_STRING[51] = "Skeleton"; -+ ID_TO_STRING[52] = "Spider"; -+ ID_TO_STRING[53] = "Giant"; -+ ID_TO_STRING[54] = "Zombie"; -+ ID_TO_STRING[55] = "Slime"; -+ ID_TO_STRING[56] = "Ghast"; -+ ID_TO_STRING[57] = "PigZombie"; -+ ID_TO_STRING[58] = "Enderman"; -+ ID_TO_STRING[59] = "CaveSpider"; -+ ID_TO_STRING[60] = "Silverfish"; -+ ID_TO_STRING[61] = "Blaze"; -+ ID_TO_STRING[62] = "LavaSlime"; -+ ID_TO_STRING[63] = "EnderDragon"; -+ ID_TO_STRING[64] = "WitherBoss"; -+ ID_TO_STRING[65] = "Bat"; -+ ID_TO_STRING[66] = "Witch"; -+ ID_TO_STRING[67] = "Endermite"; -+ ID_TO_STRING[68] = "Guardian"; -+ ID_TO_STRING[69] = "Shulker"; -+ ID_TO_STRING[90] = "Pig"; -+ ID_TO_STRING[91] = "Sheep"; -+ ID_TO_STRING[92] = "Cow"; -+ ID_TO_STRING[93] = "Chicken"; -+ ID_TO_STRING[94] = "Squid"; -+ ID_TO_STRING[95] = "Wolf"; -+ ID_TO_STRING[96] = "MushroomCow"; -+ ID_TO_STRING[97] = "SnowMan"; -+ ID_TO_STRING[98] = "Ozelot"; -+ ID_TO_STRING[99] = "VillagerGolem"; -+ ID_TO_STRING[100] = "EntityHorse"; -+ ID_TO_STRING[101] = "Rabbit"; -+ ID_TO_STRING[120] = "Villager"; -+ ID_TO_STRING[200] = "EnderCrystal"; -+ } -+ -+ public static String getSpawnNameFromId(final short id) { -+ return ID_TO_STRING[id & 255]; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/RenameHelper.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/RenameHelper.java -new file mode 100644 -index 0000000000000000000000000000000000000000..28dcc6f1425a46c6c76dd16a67aeab0ec72d1d6a ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/helpers/RenameHelper.java -@@ -0,0 +1,106 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.helpers; -+ -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import java.util.ArrayList; -+import java.util.List; -+import java.util.function.Function; -+ -+public final class RenameHelper { -+ -+ // assumes no two or more entries are renamed to a single value, otherwise result will be only one of them will win -+ // and there is no defined winner in such a case -+ public static void renameKeys(final MapType data, final Function renamer) { -+ if (data == null) { -+ return; -+ } -+ -+ List newKeys = null; -+ List newValues = null; -+ boolean needsRename = false; -+ for (final String key : data.keys()) { -+ final String renamed = renamer.apply(key); -+ if (renamed != null) { -+ newKeys = new ArrayList<>(); -+ newValues = new ArrayList<>(); -+ newValues.add(data.getGeneric(key)); -+ newKeys.add(renamed); -+ data.remove(key); -+ needsRename = true; -+ break; -+ } -+ } -+ -+ if (!needsRename) { -+ return; -+ } -+ -+ for (final String key : new ArrayList<>(data.keys())) { -+ final String renamed = renamer.apply(key); -+ -+ if (renamed != null) { -+ newValues.add(data.getGeneric(key)); -+ newKeys.add(renamed); -+ data.remove(key); -+ } -+ } -+ -+ // insert new keys -+ for (int i = 0, len = newKeys.size(); i < len; ++i) { -+ final String key = newKeys.get(i); -+ final Object value = newValues.get(i); -+ -+ data.setGeneric(key, value); -+ } -+ } -+ -+ // Clobbers anything in toKey if fromKey exists -+ public static void renameSingle(final MapType data, final String fromKey, final String toKey) { -+ if (data == null) { -+ return; -+ } -+ -+ final Object value = data.getGeneric(fromKey); -+ if (value != null) { -+ data.remove(fromKey); -+ data.setGeneric(toKey, value); -+ } -+ } -+ -+ public static void renameString(final MapType data, final String key, final Function renamer) { -+ if (data == null) { -+ return; -+ } -+ -+ final String value = data.getString(key); -+ if (value == null) { -+ return; -+ } -+ -+ final String renamed = renamer.apply(value); -+ if (renamed == null) { -+ return; -+ } -+ -+ data.setString(key, renamed); -+ } -+ -+ public static void renameListMapItems(final MapType data, final String listPath, final String mapPath, -+ final Function renamer) { -+ if (data == null) { -+ return; -+ } -+ -+ final ListType list = data.getList(listPath, ObjectType.MAP); -+ if (list == null) { -+ return; -+ } -+ -+ for (int i = 0, len = list.size(); i < len; ++i) { -+ RenameHelper.renameString(list.getMap(i), mapPath, renamer); -+ } -+ } -+ -+ private RenameHelper() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemname/ConverterAbstractItemRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemname/ConverterAbstractItemRename.java -new file mode 100644 -index 0000000000000000000000000000000000000000..94569f0ccff0d3a09eafd4ba73572d9db0a0ac5b ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemname/ConverterAbstractItemRename.java -@@ -0,0 +1,18 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.itemname; -+ -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import java.util.function.Function; -+ -+public final class ConverterAbstractItemRename { -+ -+ private ConverterAbstractItemRename() {} -+ -+ public static void register(final int version, final Function renamer) { -+ register(version, 0, renamer); -+ } -+ public static void register(final int version, final int subVersion, final Function renamer) { -+ ConverterAbstractStringValueTypeRename.register(version, subVersion, MCTypeRegistry.ITEM_NAME, renamer); -+ } -+ -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterEnchantmentsRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterEnchantmentsRename.java -new file mode 100644 -index 0000000000000000000000000000000000000000..154ba98bfb587fb4b880e39cd573701b1b049cfe ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterEnchantmentsRename.java -@@ -0,0 +1,35 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.itemstack; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.types.MapType; -+import java.util.function.Function; -+ -+public final class ConverterEnchantmentsRename extends DataConverter, MapType> { -+ -+ private final Function renamer; -+ -+ public ConverterEnchantmentsRename(final int toVersion, final Function renamer) { -+ this(toVersion, 0, renamer); -+ } -+ -+ public ConverterEnchantmentsRename(final int toVersion, final int versionStep, final Function renamer) { -+ super(toVersion, versionStep); -+ -+ this.renamer = renamer; -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType tag = data.getMap("tag"); -+ -+ if (tag == null) { -+ return null; -+ } -+ -+ RenameHelper.renameListMapItems(tag, "Enchantments", "id", this.renamer); -+ RenameHelper.renameListMapItems(tag, "StoredEnchantments", "id", this.renamer); -+ -+ return null; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenItemStack.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenItemStack.java -new file mode 100644 -index 0000000000000000000000000000000000000000..21176b8b96be6cb93d3dc1a74ae9f53f1ad4740c ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenItemStack.java -@@ -0,0 +1,460 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.itemstack; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.Types; -+import com.mojang.logging.LogUtils; -+import org.slf4j.Logger; -+import java.util.Arrays; -+import java.util.HashMap; -+import java.util.HashSet; -+import java.util.Map; -+import java.util.Set; -+ -+public final class ConverterFlattenItemStack extends DataConverter, MapType> { -+ -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ -+ // Map of "id.damage" -> "flattened id" -+ private static final Map FLATTEN_MAP = new HashMap<>(); -+ static { -+ FLATTEN_MAP.put("minecraft:stone.0", "minecraft:stone"); -+ FLATTEN_MAP.put("minecraft:stone.1", "minecraft:granite"); -+ FLATTEN_MAP.put("minecraft:stone.2", "minecraft:polished_granite"); -+ FLATTEN_MAP.put("minecraft:stone.3", "minecraft:diorite"); -+ FLATTEN_MAP.put("minecraft:stone.4", "minecraft:polished_diorite"); -+ FLATTEN_MAP.put("minecraft:stone.5", "minecraft:andesite"); -+ FLATTEN_MAP.put("minecraft:stone.6", "minecraft:polished_andesite"); -+ FLATTEN_MAP.put("minecraft:dirt.0", "minecraft:dirt"); -+ FLATTEN_MAP.put("minecraft:dirt.1", "minecraft:coarse_dirt"); -+ FLATTEN_MAP.put("minecraft:dirt.2", "minecraft:podzol"); -+ FLATTEN_MAP.put("minecraft:leaves.0", "minecraft:oak_leaves"); -+ FLATTEN_MAP.put("minecraft:leaves.1", "minecraft:spruce_leaves"); -+ FLATTEN_MAP.put("minecraft:leaves.2", "minecraft:birch_leaves"); -+ FLATTEN_MAP.put("minecraft:leaves.3", "minecraft:jungle_leaves"); -+ FLATTEN_MAP.put("minecraft:leaves2.0", "minecraft:acacia_leaves"); -+ FLATTEN_MAP.put("minecraft:leaves2.1", "minecraft:dark_oak_leaves"); -+ FLATTEN_MAP.put("minecraft:log.0", "minecraft:oak_log"); -+ FLATTEN_MAP.put("minecraft:log.1", "minecraft:spruce_log"); -+ FLATTEN_MAP.put("minecraft:log.2", "minecraft:birch_log"); -+ FLATTEN_MAP.put("minecraft:log.3", "minecraft:jungle_log"); -+ FLATTEN_MAP.put("minecraft:log2.0", "minecraft:acacia_log"); -+ FLATTEN_MAP.put("minecraft:log2.1", "minecraft:dark_oak_log"); -+ FLATTEN_MAP.put("minecraft:sapling.0", "minecraft:oak_sapling"); -+ FLATTEN_MAP.put("minecraft:sapling.1", "minecraft:spruce_sapling"); -+ FLATTEN_MAP.put("minecraft:sapling.2", "minecraft:birch_sapling"); -+ FLATTEN_MAP.put("minecraft:sapling.3", "minecraft:jungle_sapling"); -+ FLATTEN_MAP.put("minecraft:sapling.4", "minecraft:acacia_sapling"); -+ FLATTEN_MAP.put("minecraft:sapling.5", "minecraft:dark_oak_sapling"); -+ FLATTEN_MAP.put("minecraft:planks.0", "minecraft:oak_planks"); -+ FLATTEN_MAP.put("minecraft:planks.1", "minecraft:spruce_planks"); -+ FLATTEN_MAP.put("minecraft:planks.2", "minecraft:birch_planks"); -+ FLATTEN_MAP.put("minecraft:planks.3", "minecraft:jungle_planks"); -+ FLATTEN_MAP.put("minecraft:planks.4", "minecraft:acacia_planks"); -+ FLATTEN_MAP.put("minecraft:planks.5", "minecraft:dark_oak_planks"); -+ FLATTEN_MAP.put("minecraft:sand.0", "minecraft:sand"); -+ FLATTEN_MAP.put("minecraft:sand.1", "minecraft:red_sand"); -+ FLATTEN_MAP.put("minecraft:quartz_block.0", "minecraft:quartz_block"); -+ FLATTEN_MAP.put("minecraft:quartz_block.1", "minecraft:chiseled_quartz_block"); -+ FLATTEN_MAP.put("minecraft:quartz_block.2", "minecraft:quartz_pillar"); -+ FLATTEN_MAP.put("minecraft:anvil.0", "minecraft:anvil"); -+ FLATTEN_MAP.put("minecraft:anvil.1", "minecraft:chipped_anvil"); -+ FLATTEN_MAP.put("minecraft:anvil.2", "minecraft:damaged_anvil"); -+ FLATTEN_MAP.put("minecraft:wool.0", "minecraft:white_wool"); -+ FLATTEN_MAP.put("minecraft:wool.1", "minecraft:orange_wool"); -+ FLATTEN_MAP.put("minecraft:wool.2", "minecraft:magenta_wool"); -+ FLATTEN_MAP.put("minecraft:wool.3", "minecraft:light_blue_wool"); -+ FLATTEN_MAP.put("minecraft:wool.4", "minecraft:yellow_wool"); -+ FLATTEN_MAP.put("minecraft:wool.5", "minecraft:lime_wool"); -+ FLATTEN_MAP.put("minecraft:wool.6", "minecraft:pink_wool"); -+ FLATTEN_MAP.put("minecraft:wool.7", "minecraft:gray_wool"); -+ FLATTEN_MAP.put("minecraft:wool.8", "minecraft:light_gray_wool"); -+ FLATTEN_MAP.put("minecraft:wool.9", "minecraft:cyan_wool"); -+ FLATTEN_MAP.put("minecraft:wool.10", "minecraft:purple_wool"); -+ FLATTEN_MAP.put("minecraft:wool.11", "minecraft:blue_wool"); -+ FLATTEN_MAP.put("minecraft:wool.12", "minecraft:brown_wool"); -+ FLATTEN_MAP.put("minecraft:wool.13", "minecraft:green_wool"); -+ FLATTEN_MAP.put("minecraft:wool.14", "minecraft:red_wool"); -+ FLATTEN_MAP.put("minecraft:wool.15", "minecraft:black_wool"); -+ FLATTEN_MAP.put("minecraft:carpet.0", "minecraft:white_carpet"); -+ FLATTEN_MAP.put("minecraft:carpet.1", "minecraft:orange_carpet"); -+ FLATTEN_MAP.put("minecraft:carpet.2", "minecraft:magenta_carpet"); -+ FLATTEN_MAP.put("minecraft:carpet.3", "minecraft:light_blue_carpet"); -+ FLATTEN_MAP.put("minecraft:carpet.4", "minecraft:yellow_carpet"); -+ FLATTEN_MAP.put("minecraft:carpet.5", "minecraft:lime_carpet"); -+ FLATTEN_MAP.put("minecraft:carpet.6", "minecraft:pink_carpet"); -+ FLATTEN_MAP.put("minecraft:carpet.7", "minecraft:gray_carpet"); -+ FLATTEN_MAP.put("minecraft:carpet.8", "minecraft:light_gray_carpet"); -+ FLATTEN_MAP.put("minecraft:carpet.9", "minecraft:cyan_carpet"); -+ FLATTEN_MAP.put("minecraft:carpet.10", "minecraft:purple_carpet"); -+ FLATTEN_MAP.put("minecraft:carpet.11", "minecraft:blue_carpet"); -+ FLATTEN_MAP.put("minecraft:carpet.12", "minecraft:brown_carpet"); -+ FLATTEN_MAP.put("minecraft:carpet.13", "minecraft:green_carpet"); -+ FLATTEN_MAP.put("minecraft:carpet.14", "minecraft:red_carpet"); -+ FLATTEN_MAP.put("minecraft:carpet.15", "minecraft:black_carpet"); -+ FLATTEN_MAP.put("minecraft:hardened_clay.0", "minecraft:terracotta"); -+ FLATTEN_MAP.put("minecraft:stained_hardened_clay.0", "minecraft:white_terracotta"); -+ FLATTEN_MAP.put("minecraft:stained_hardened_clay.1", "minecraft:orange_terracotta"); -+ FLATTEN_MAP.put("minecraft:stained_hardened_clay.2", "minecraft:magenta_terracotta"); -+ FLATTEN_MAP.put("minecraft:stained_hardened_clay.3", "minecraft:light_blue_terracotta"); -+ FLATTEN_MAP.put("minecraft:stained_hardened_clay.4", "minecraft:yellow_terracotta"); -+ FLATTEN_MAP.put("minecraft:stained_hardened_clay.5", "minecraft:lime_terracotta"); -+ FLATTEN_MAP.put("minecraft:stained_hardened_clay.6", "minecraft:pink_terracotta"); -+ FLATTEN_MAP.put("minecraft:stained_hardened_clay.7", "minecraft:gray_terracotta"); -+ FLATTEN_MAP.put("minecraft:stained_hardened_clay.8", "minecraft:light_gray_terracotta"); -+ FLATTEN_MAP.put("minecraft:stained_hardened_clay.9", "minecraft:cyan_terracotta"); -+ FLATTEN_MAP.put("minecraft:stained_hardened_clay.10", "minecraft:purple_terracotta"); -+ FLATTEN_MAP.put("minecraft:stained_hardened_clay.11", "minecraft:blue_terracotta"); -+ FLATTEN_MAP.put("minecraft:stained_hardened_clay.12", "minecraft:brown_terracotta"); -+ FLATTEN_MAP.put("minecraft:stained_hardened_clay.13", "minecraft:green_terracotta"); -+ FLATTEN_MAP.put("minecraft:stained_hardened_clay.14", "minecraft:red_terracotta"); -+ FLATTEN_MAP.put("minecraft:stained_hardened_clay.15", "minecraft:black_terracotta"); -+ FLATTEN_MAP.put("minecraft:silver_glazed_terracotta.0", "minecraft:light_gray_glazed_terracotta"); -+ FLATTEN_MAP.put("minecraft:stained_glass.0", "minecraft:white_stained_glass"); -+ FLATTEN_MAP.put("minecraft:stained_glass.1", "minecraft:orange_stained_glass"); -+ FLATTEN_MAP.put("minecraft:stained_glass.2", "minecraft:magenta_stained_glass"); -+ FLATTEN_MAP.put("minecraft:stained_glass.3", "minecraft:light_blue_stained_glass"); -+ FLATTEN_MAP.put("minecraft:stained_glass.4", "minecraft:yellow_stained_glass"); -+ FLATTEN_MAP.put("minecraft:stained_glass.5", "minecraft:lime_stained_glass"); -+ FLATTEN_MAP.put("minecraft:stained_glass.6", "minecraft:pink_stained_glass"); -+ FLATTEN_MAP.put("minecraft:stained_glass.7", "minecraft:gray_stained_glass"); -+ FLATTEN_MAP.put("minecraft:stained_glass.8", "minecraft:light_gray_stained_glass"); -+ FLATTEN_MAP.put("minecraft:stained_glass.9", "minecraft:cyan_stained_glass"); -+ FLATTEN_MAP.put("minecraft:stained_glass.10", "minecraft:purple_stained_glass"); -+ FLATTEN_MAP.put("minecraft:stained_glass.11", "minecraft:blue_stained_glass"); -+ FLATTEN_MAP.put("minecraft:stained_glass.12", "minecraft:brown_stained_glass"); -+ FLATTEN_MAP.put("minecraft:stained_glass.13", "minecraft:green_stained_glass"); -+ FLATTEN_MAP.put("minecraft:stained_glass.14", "minecraft:red_stained_glass"); -+ FLATTEN_MAP.put("minecraft:stained_glass.15", "minecraft:black_stained_glass"); -+ FLATTEN_MAP.put("minecraft:stained_glass_pane.0", "minecraft:white_stained_glass_pane"); -+ FLATTEN_MAP.put("minecraft:stained_glass_pane.1", "minecraft:orange_stained_glass_pane"); -+ FLATTEN_MAP.put("minecraft:stained_glass_pane.2", "minecraft:magenta_stained_glass_pane"); -+ FLATTEN_MAP.put("minecraft:stained_glass_pane.3", "minecraft:light_blue_stained_glass_pane"); -+ FLATTEN_MAP.put("minecraft:stained_glass_pane.4", "minecraft:yellow_stained_glass_pane"); -+ FLATTEN_MAP.put("minecraft:stained_glass_pane.5", "minecraft:lime_stained_glass_pane"); -+ FLATTEN_MAP.put("minecraft:stained_glass_pane.6", "minecraft:pink_stained_glass_pane"); -+ FLATTEN_MAP.put("minecraft:stained_glass_pane.7", "minecraft:gray_stained_glass_pane"); -+ FLATTEN_MAP.put("minecraft:stained_glass_pane.8", "minecraft:light_gray_stained_glass_pane"); -+ FLATTEN_MAP.put("minecraft:stained_glass_pane.9", "minecraft:cyan_stained_glass_pane"); -+ FLATTEN_MAP.put("minecraft:stained_glass_pane.10", "minecraft:purple_stained_glass_pane"); -+ FLATTEN_MAP.put("minecraft:stained_glass_pane.11", "minecraft:blue_stained_glass_pane"); -+ FLATTEN_MAP.put("minecraft:stained_glass_pane.12", "minecraft:brown_stained_glass_pane"); -+ FLATTEN_MAP.put("minecraft:stained_glass_pane.13", "minecraft:green_stained_glass_pane"); -+ FLATTEN_MAP.put("minecraft:stained_glass_pane.14", "minecraft:red_stained_glass_pane"); -+ FLATTEN_MAP.put("minecraft:stained_glass_pane.15", "minecraft:black_stained_glass_pane"); -+ FLATTEN_MAP.put("minecraft:prismarine.0", "minecraft:prismarine"); -+ FLATTEN_MAP.put("minecraft:prismarine.1", "minecraft:prismarine_bricks"); -+ FLATTEN_MAP.put("minecraft:prismarine.2", "minecraft:dark_prismarine"); -+ FLATTEN_MAP.put("minecraft:concrete.0", "minecraft:white_concrete"); -+ FLATTEN_MAP.put("minecraft:concrete.1", "minecraft:orange_concrete"); -+ FLATTEN_MAP.put("minecraft:concrete.2", "minecraft:magenta_concrete"); -+ FLATTEN_MAP.put("minecraft:concrete.3", "minecraft:light_blue_concrete"); -+ FLATTEN_MAP.put("minecraft:concrete.4", "minecraft:yellow_concrete"); -+ FLATTEN_MAP.put("minecraft:concrete.5", "minecraft:lime_concrete"); -+ FLATTEN_MAP.put("minecraft:concrete.6", "minecraft:pink_concrete"); -+ FLATTEN_MAP.put("minecraft:concrete.7", "minecraft:gray_concrete"); -+ FLATTEN_MAP.put("minecraft:concrete.8", "minecraft:light_gray_concrete"); -+ FLATTEN_MAP.put("minecraft:concrete.9", "minecraft:cyan_concrete"); -+ FLATTEN_MAP.put("minecraft:concrete.10", "minecraft:purple_concrete"); -+ FLATTEN_MAP.put("minecraft:concrete.11", "minecraft:blue_concrete"); -+ FLATTEN_MAP.put("minecraft:concrete.12", "minecraft:brown_concrete"); -+ FLATTEN_MAP.put("minecraft:concrete.13", "minecraft:green_concrete"); -+ FLATTEN_MAP.put("minecraft:concrete.14", "minecraft:red_concrete"); -+ FLATTEN_MAP.put("minecraft:concrete.15", "minecraft:black_concrete"); -+ FLATTEN_MAP.put("minecraft:concrete_powder.0", "minecraft:white_concrete_powder"); -+ FLATTEN_MAP.put("minecraft:concrete_powder.1", "minecraft:orange_concrete_powder"); -+ FLATTEN_MAP.put("minecraft:concrete_powder.2", "minecraft:magenta_concrete_powder"); -+ FLATTEN_MAP.put("minecraft:concrete_powder.3", "minecraft:light_blue_concrete_powder"); -+ FLATTEN_MAP.put("minecraft:concrete_powder.4", "minecraft:yellow_concrete_powder"); -+ FLATTEN_MAP.put("minecraft:concrete_powder.5", "minecraft:lime_concrete_powder"); -+ FLATTEN_MAP.put("minecraft:concrete_powder.6", "minecraft:pink_concrete_powder"); -+ FLATTEN_MAP.put("minecraft:concrete_powder.7", "minecraft:gray_concrete_powder"); -+ FLATTEN_MAP.put("minecraft:concrete_powder.8", "minecraft:light_gray_concrete_powder"); -+ FLATTEN_MAP.put("minecraft:concrete_powder.9", "minecraft:cyan_concrete_powder"); -+ FLATTEN_MAP.put("minecraft:concrete_powder.10", "minecraft:purple_concrete_powder"); -+ FLATTEN_MAP.put("minecraft:concrete_powder.11", "minecraft:blue_concrete_powder"); -+ FLATTEN_MAP.put("minecraft:concrete_powder.12", "minecraft:brown_concrete_powder"); -+ FLATTEN_MAP.put("minecraft:concrete_powder.13", "minecraft:green_concrete_powder"); -+ FLATTEN_MAP.put("minecraft:concrete_powder.14", "minecraft:red_concrete_powder"); -+ FLATTEN_MAP.put("minecraft:concrete_powder.15", "minecraft:black_concrete_powder"); -+ FLATTEN_MAP.put("minecraft:cobblestone_wall.0", "minecraft:cobblestone_wall"); -+ FLATTEN_MAP.put("minecraft:cobblestone_wall.1", "minecraft:mossy_cobblestone_wall"); -+ FLATTEN_MAP.put("minecraft:sandstone.0", "minecraft:sandstone"); -+ FLATTEN_MAP.put("minecraft:sandstone.1", "minecraft:chiseled_sandstone"); -+ FLATTEN_MAP.put("minecraft:sandstone.2", "minecraft:cut_sandstone"); -+ FLATTEN_MAP.put("minecraft:red_sandstone.0", "minecraft:red_sandstone"); -+ FLATTEN_MAP.put("minecraft:red_sandstone.1", "minecraft:chiseled_red_sandstone"); -+ FLATTEN_MAP.put("minecraft:red_sandstone.2", "minecraft:cut_red_sandstone"); -+ FLATTEN_MAP.put("minecraft:stonebrick.0", "minecraft:stone_bricks"); -+ FLATTEN_MAP.put("minecraft:stonebrick.1", "minecraft:mossy_stone_bricks"); -+ FLATTEN_MAP.put("minecraft:stonebrick.2", "minecraft:cracked_stone_bricks"); -+ FLATTEN_MAP.put("minecraft:stonebrick.3", "minecraft:chiseled_stone_bricks"); -+ FLATTEN_MAP.put("minecraft:monster_egg.0", "minecraft:infested_stone"); -+ FLATTEN_MAP.put("minecraft:monster_egg.1", "minecraft:infested_cobblestone"); -+ FLATTEN_MAP.put("minecraft:monster_egg.2", "minecraft:infested_stone_bricks"); -+ FLATTEN_MAP.put("minecraft:monster_egg.3", "minecraft:infested_mossy_stone_bricks"); -+ FLATTEN_MAP.put("minecraft:monster_egg.4", "minecraft:infested_cracked_stone_bricks"); -+ FLATTEN_MAP.put("minecraft:monster_egg.5", "minecraft:infested_chiseled_stone_bricks"); -+ FLATTEN_MAP.put("minecraft:yellow_flower.0", "minecraft:dandelion"); -+ FLATTEN_MAP.put("minecraft:red_flower.0", "minecraft:poppy"); -+ FLATTEN_MAP.put("minecraft:red_flower.1", "minecraft:blue_orchid"); -+ FLATTEN_MAP.put("minecraft:red_flower.2", "minecraft:allium"); -+ FLATTEN_MAP.put("minecraft:red_flower.3", "minecraft:azure_bluet"); -+ FLATTEN_MAP.put("minecraft:red_flower.4", "minecraft:red_tulip"); -+ FLATTEN_MAP.put("minecraft:red_flower.5", "minecraft:orange_tulip"); -+ FLATTEN_MAP.put("minecraft:red_flower.6", "minecraft:white_tulip"); -+ FLATTEN_MAP.put("minecraft:red_flower.7", "minecraft:pink_tulip"); -+ FLATTEN_MAP.put("minecraft:red_flower.8", "minecraft:oxeye_daisy"); -+ FLATTEN_MAP.put("minecraft:double_plant.0", "minecraft:sunflower"); -+ FLATTEN_MAP.put("minecraft:double_plant.1", "minecraft:lilac"); -+ FLATTEN_MAP.put("minecraft:double_plant.2", "minecraft:tall_grass"); -+ FLATTEN_MAP.put("minecraft:double_plant.3", "minecraft:large_fern"); -+ FLATTEN_MAP.put("minecraft:double_plant.4", "minecraft:rose_bush"); -+ FLATTEN_MAP.put("minecraft:double_plant.5", "minecraft:peony"); -+ FLATTEN_MAP.put("minecraft:deadbush.0", "minecraft:dead_bush"); -+ FLATTEN_MAP.put("minecraft:tallgrass.0", "minecraft:dead_bush"); -+ FLATTEN_MAP.put("minecraft:tallgrass.1", "minecraft:grass"); -+ FLATTEN_MAP.put("minecraft:tallgrass.2", "minecraft:fern"); -+ FLATTEN_MAP.put("minecraft:sponge.0", "minecraft:sponge"); -+ FLATTEN_MAP.put("minecraft:sponge.1", "minecraft:wet_sponge"); -+ FLATTEN_MAP.put("minecraft:purpur_slab.0", "minecraft:purpur_slab"); -+ FLATTEN_MAP.put("minecraft:stone_slab.0", "minecraft:stone_slab"); -+ FLATTEN_MAP.put("minecraft:stone_slab.1", "minecraft:sandstone_slab"); -+ FLATTEN_MAP.put("minecraft:stone_slab.2", "minecraft:petrified_oak_slab"); -+ FLATTEN_MAP.put("minecraft:stone_slab.3", "minecraft:cobblestone_slab"); -+ FLATTEN_MAP.put("minecraft:stone_slab.4", "minecraft:brick_slab"); -+ FLATTEN_MAP.put("minecraft:stone_slab.5", "minecraft:stone_brick_slab"); -+ FLATTEN_MAP.put("minecraft:stone_slab.6", "minecraft:nether_brick_slab"); -+ FLATTEN_MAP.put("minecraft:stone_slab.7", "minecraft:quartz_slab"); -+ FLATTEN_MAP.put("minecraft:stone_slab2.0", "minecraft:red_sandstone_slab"); -+ FLATTEN_MAP.put("minecraft:wooden_slab.0", "minecraft:oak_slab"); -+ FLATTEN_MAP.put("minecraft:wooden_slab.1", "minecraft:spruce_slab"); -+ FLATTEN_MAP.put("minecraft:wooden_slab.2", "minecraft:birch_slab"); -+ FLATTEN_MAP.put("minecraft:wooden_slab.3", "minecraft:jungle_slab"); -+ FLATTEN_MAP.put("minecraft:wooden_slab.4", "minecraft:acacia_slab"); -+ FLATTEN_MAP.put("minecraft:wooden_slab.5", "minecraft:dark_oak_slab"); -+ FLATTEN_MAP.put("minecraft:coal.0", "minecraft:coal"); -+ FLATTEN_MAP.put("minecraft:coal.1", "minecraft:charcoal"); -+ FLATTEN_MAP.put("minecraft:fish.0", "minecraft:cod"); -+ FLATTEN_MAP.put("minecraft:fish.1", "minecraft:salmon"); -+ FLATTEN_MAP.put("minecraft:fish.2", "minecraft:clownfish"); -+ FLATTEN_MAP.put("minecraft:fish.3", "minecraft:pufferfish"); -+ FLATTEN_MAP.put("minecraft:cooked_fish.0", "minecraft:cooked_cod"); -+ FLATTEN_MAP.put("minecraft:cooked_fish.1", "minecraft:cooked_salmon"); -+ FLATTEN_MAP.put("minecraft:skull.0", "minecraft:skeleton_skull"); -+ FLATTEN_MAP.put("minecraft:skull.1", "minecraft:wither_skeleton_skull"); -+ FLATTEN_MAP.put("minecraft:skull.2", "minecraft:zombie_head"); -+ FLATTEN_MAP.put("minecraft:skull.3", "minecraft:player_head"); -+ FLATTEN_MAP.put("minecraft:skull.4", "minecraft:creeper_head"); -+ FLATTEN_MAP.put("minecraft:skull.5", "minecraft:dragon_head"); -+ FLATTEN_MAP.put("minecraft:golden_apple.0", "minecraft:golden_apple"); -+ FLATTEN_MAP.put("minecraft:golden_apple.1", "minecraft:enchanted_golden_apple"); -+ FLATTEN_MAP.put("minecraft:fireworks.0", "minecraft:firework_rocket"); -+ FLATTEN_MAP.put("minecraft:firework_charge.0", "minecraft:firework_star"); -+ FLATTEN_MAP.put("minecraft:dye.0", "minecraft:ink_sac"); -+ FLATTEN_MAP.put("minecraft:dye.1", "minecraft:rose_red"); -+ FLATTEN_MAP.put("minecraft:dye.2", "minecraft:cactus_green"); -+ FLATTEN_MAP.put("minecraft:dye.3", "minecraft:cocoa_beans"); -+ FLATTEN_MAP.put("minecraft:dye.4", "minecraft:lapis_lazuli"); -+ FLATTEN_MAP.put("minecraft:dye.5", "minecraft:purple_dye"); -+ FLATTEN_MAP.put("minecraft:dye.6", "minecraft:cyan_dye"); -+ FLATTEN_MAP.put("minecraft:dye.7", "minecraft:light_gray_dye"); -+ FLATTEN_MAP.put("minecraft:dye.8", "minecraft:gray_dye"); -+ FLATTEN_MAP.put("minecraft:dye.9", "minecraft:pink_dye"); -+ FLATTEN_MAP.put("minecraft:dye.10", "minecraft:lime_dye"); -+ FLATTEN_MAP.put("minecraft:dye.11", "minecraft:dandelion_yellow"); -+ FLATTEN_MAP.put("minecraft:dye.12", "minecraft:light_blue_dye"); -+ FLATTEN_MAP.put("minecraft:dye.13", "minecraft:magenta_dye"); -+ FLATTEN_MAP.put("minecraft:dye.14", "minecraft:orange_dye"); -+ FLATTEN_MAP.put("minecraft:dye.15", "minecraft:bone_meal"); -+ FLATTEN_MAP.put("minecraft:silver_shulker_box.0", "minecraft:light_gray_shulker_box"); -+ FLATTEN_MAP.put("minecraft:fence.0", "minecraft:oak_fence"); -+ FLATTEN_MAP.put("minecraft:fence_gate.0", "minecraft:oak_fence_gate"); -+ FLATTEN_MAP.put("minecraft:wooden_door.0", "minecraft:oak_door"); -+ FLATTEN_MAP.put("minecraft:boat.0", "minecraft:oak_boat"); -+ FLATTEN_MAP.put("minecraft:lit_pumpkin.0", "minecraft:jack_o_lantern"); -+ FLATTEN_MAP.put("minecraft:pumpkin.0", "minecraft:carved_pumpkin"); -+ FLATTEN_MAP.put("minecraft:trapdoor.0", "minecraft:oak_trapdoor"); -+ FLATTEN_MAP.put("minecraft:nether_brick.0", "minecraft:nether_bricks"); -+ FLATTEN_MAP.put("minecraft:red_nether_brick.0", "minecraft:red_nether_bricks"); -+ FLATTEN_MAP.put("minecraft:netherbrick.0", "minecraft:nether_brick"); -+ FLATTEN_MAP.put("minecraft:wooden_button.0", "minecraft:oak_button"); -+ FLATTEN_MAP.put("minecraft:wooden_pressure_plate.0", "minecraft:oak_pressure_plate"); -+ FLATTEN_MAP.put("minecraft:noteblock.0", "minecraft:note_block"); -+ FLATTEN_MAP.put("minecraft:bed.0", "minecraft:white_bed"); -+ FLATTEN_MAP.put("minecraft:bed.1", "minecraft:orange_bed"); -+ FLATTEN_MAP.put("minecraft:bed.2", "minecraft:magenta_bed"); -+ FLATTEN_MAP.put("minecraft:bed.3", "minecraft:light_blue_bed"); -+ FLATTEN_MAP.put("minecraft:bed.4", "minecraft:yellow_bed"); -+ FLATTEN_MAP.put("minecraft:bed.5", "minecraft:lime_bed"); -+ FLATTEN_MAP.put("minecraft:bed.6", "minecraft:pink_bed"); -+ FLATTEN_MAP.put("minecraft:bed.7", "minecraft:gray_bed"); -+ FLATTEN_MAP.put("minecraft:bed.8", "minecraft:light_gray_bed"); -+ FLATTEN_MAP.put("minecraft:bed.9", "minecraft:cyan_bed"); -+ FLATTEN_MAP.put("minecraft:bed.10", "minecraft:purple_bed"); -+ FLATTEN_MAP.put("minecraft:bed.11", "minecraft:blue_bed"); -+ FLATTEN_MAP.put("minecraft:bed.12", "minecraft:brown_bed"); -+ FLATTEN_MAP.put("minecraft:bed.13", "minecraft:green_bed"); -+ FLATTEN_MAP.put("minecraft:bed.14", "minecraft:red_bed"); -+ FLATTEN_MAP.put("minecraft:bed.15", "minecraft:black_bed"); -+ FLATTEN_MAP.put("minecraft:banner.15", "minecraft:white_banner"); -+ FLATTEN_MAP.put("minecraft:banner.14", "minecraft:orange_banner"); -+ FLATTEN_MAP.put("minecraft:banner.13", "minecraft:magenta_banner"); -+ FLATTEN_MAP.put("minecraft:banner.12", "minecraft:light_blue_banner"); -+ FLATTEN_MAP.put("minecraft:banner.11", "minecraft:yellow_banner"); -+ FLATTEN_MAP.put("minecraft:banner.10", "minecraft:lime_banner"); -+ FLATTEN_MAP.put("minecraft:banner.9", "minecraft:pink_banner"); -+ FLATTEN_MAP.put("minecraft:banner.8", "minecraft:gray_banner"); -+ FLATTEN_MAP.put("minecraft:banner.7", "minecraft:light_gray_banner"); -+ FLATTEN_MAP.put("minecraft:banner.6", "minecraft:cyan_banner"); -+ FLATTEN_MAP.put("minecraft:banner.5", "minecraft:purple_banner"); -+ FLATTEN_MAP.put("minecraft:banner.4", "minecraft:blue_banner"); -+ FLATTEN_MAP.put("minecraft:banner.3", "minecraft:brown_banner"); -+ FLATTEN_MAP.put("minecraft:banner.2", "minecraft:green_banner"); -+ FLATTEN_MAP.put("minecraft:banner.1", "minecraft:red_banner"); -+ FLATTEN_MAP.put("minecraft:banner.0", "minecraft:black_banner"); -+ FLATTEN_MAP.put("minecraft:grass.0", "minecraft:grass_block"); -+ FLATTEN_MAP.put("minecraft:brick_block.0", "minecraft:bricks"); -+ FLATTEN_MAP.put("minecraft:end_bricks.0", "minecraft:end_stone_bricks"); -+ FLATTEN_MAP.put("minecraft:golden_rail.0", "minecraft:powered_rail"); -+ FLATTEN_MAP.put("minecraft:magma.0", "minecraft:magma_block"); -+ FLATTEN_MAP.put("minecraft:quartz_ore.0", "minecraft:nether_quartz_ore"); -+ FLATTEN_MAP.put("minecraft:reeds.0", "minecraft:sugar_cane"); -+ FLATTEN_MAP.put("minecraft:slime.0", "minecraft:slime_block"); -+ FLATTEN_MAP.put("minecraft:stone_stairs.0", "minecraft:cobblestone_stairs"); -+ FLATTEN_MAP.put("minecraft:waterlily.0", "minecraft:lily_pad"); -+ FLATTEN_MAP.put("minecraft:web.0", "minecraft:cobweb"); -+ FLATTEN_MAP.put("minecraft:snow.0", "minecraft:snow_block"); -+ FLATTEN_MAP.put("minecraft:snow_layer.0", "minecraft:snow"); -+ FLATTEN_MAP.put("minecraft:record_11.0", "minecraft:music_disc_11"); -+ FLATTEN_MAP.put("minecraft:record_13.0", "minecraft:music_disc_13"); -+ FLATTEN_MAP.put("minecraft:record_blocks.0", "minecraft:music_disc_blocks"); -+ FLATTEN_MAP.put("minecraft:record_cat.0", "minecraft:music_disc_cat"); -+ FLATTEN_MAP.put("minecraft:record_chirp.0", "minecraft:music_disc_chirp"); -+ FLATTEN_MAP.put("minecraft:record_far.0", "minecraft:music_disc_far"); -+ FLATTEN_MAP.put("minecraft:record_mall.0", "minecraft:music_disc_mall"); -+ FLATTEN_MAP.put("minecraft:record_mellohi.0", "minecraft:music_disc_mellohi"); -+ FLATTEN_MAP.put("minecraft:record_stal.0", "minecraft:music_disc_stal"); -+ FLATTEN_MAP.put("minecraft:record_strad.0", "minecraft:music_disc_strad"); -+ FLATTEN_MAP.put("minecraft:record_wait.0", "minecraft:music_disc_wait"); -+ FLATTEN_MAP.put("minecraft:record_ward.0", "minecraft:music_disc_ward"); -+ } -+ -+ // maps out ids requiring flattening -+ private static final Set IDS_REQUIRING_FLATTENING = new HashSet<>(); -+ static { -+ for (final String key : FLATTEN_MAP.keySet()) { -+ IDS_REQUIRING_FLATTENING.add(key.substring(0, key.indexOf('.'))); -+ } -+ } -+ -+ // Damage tag is moved from the ItemStack base tag to the ItemStack tag, and we only want to migrate that -+ // for items that actually require it for damage purposes (Remember, old damage was used to differentiate item types) -+ // It should be noted that this ID set should not be included in the flattening map, because damage for these items -+ // is actual damage and not a subtype specifier -+ private static final Set ITEMS_WITH_DAMAGE = new HashSet<>(Arrays.asList( -+ "minecraft:bow", -+ "minecraft:carrot_on_a_stick", -+ "minecraft:chainmail_boots", -+ "minecraft:chainmail_chestplate", -+ "minecraft:chainmail_helmet", -+ "minecraft:chainmail_leggings", -+ "minecraft:diamond_axe", -+ "minecraft:diamond_boots", -+ "minecraft:diamond_chestplate", -+ "minecraft:diamond_helmet", -+ "minecraft:diamond_hoe", -+ "minecraft:diamond_leggings", -+ "minecraft:diamond_pickaxe", -+ "minecraft:diamond_shovel", -+ "minecraft:diamond_sword", -+ "minecraft:elytra", -+ "minecraft:fishing_rod", -+ "minecraft:flint_and_steel", -+ "minecraft:golden_axe", -+ "minecraft:golden_boots", -+ "minecraft:golden_chestplate", -+ "minecraft:golden_helmet", -+ "minecraft:golden_hoe", -+ "minecraft:golden_leggings", -+ "minecraft:golden_pickaxe", -+ "minecraft:golden_shovel", -+ "minecraft:golden_sword", -+ "minecraft:iron_axe", -+ "minecraft:iron_boots", -+ "minecraft:iron_chestplate", -+ "minecraft:iron_helmet", -+ "minecraft:iron_hoe", -+ "minecraft:iron_leggings", -+ "minecraft:iron_pickaxe", -+ "minecraft:iron_shovel", -+ "minecraft:iron_sword", -+ "minecraft:leather_boots", -+ "minecraft:leather_chestplate", -+ "minecraft:leather_helmet", -+ "minecraft:leather_leggings", -+ "minecraft:shears", -+ "minecraft:shield", -+ "minecraft:stone_axe", -+ "minecraft:stone_hoe", -+ "minecraft:stone_pickaxe", -+ "minecraft:stone_shovel", -+ "minecraft:stone_sword", -+ "minecraft:wooden_axe", -+ "minecraft:wooden_hoe", -+ "minecraft:wooden_pickaxe", -+ "minecraft:wooden_shovel", -+ "minecraft:wooden_sword" -+ )); -+ -+ public ConverterFlattenItemStack() { -+ super(MCVersions.V17W47A, 4); -+ } -+ -+ public static String flattenItem(final String oldName, final int data) { -+ if (IDS_REQUIRING_FLATTENING.contains(oldName)) { -+ final String flattened = FLATTEN_MAP.get(oldName + '.' + data); -+ return flattened == null ? FLATTEN_MAP.get(oldName.concat(".0")) : flattened; -+ } else { -+ return null; -+ } -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final String id = data.getString("id"); -+ -+ if (id == null) { -+ return null; -+ } -+ -+ final int damage = data.getInt("Damage"); -+ data.remove("Damage"); -+ -+ if (IDS_REQUIRING_FLATTENING.contains(id)) { -+ String remap = FLATTEN_MAP.get(id + '.' + damage); -+ if (remap == null) { -+ remap = FLATTEN_MAP.get(id.concat(".0")); -+ // this shouldn't be null -+ } -+ if (remap != null) { -+ data.setString("id", remap); -+ } else { -+ LOGGER.warn("Item '" + id + "' requires flattening but found no mapping for it! (ConverterFlattenItemStack)"); -+ } -+ } -+ -+ if (damage != 0 && ITEMS_WITH_DAMAGE.contains(id)) { -+ // migrate damage -+ MapType tag = data.getMap("tag"); -+ if (tag == null) { -+ tag = Types.NBT.createEmptyMap(); -+ data.setMap("tag", tag); -+ } -+ tag.setInt("Damage", damage); -+ } -+ -+ return null; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenSpawnEgg.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenSpawnEgg.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4fa31e40b0a6f571a853299b4e242de921ccbda0 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterFlattenSpawnEgg.java -@@ -0,0 +1,87 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.itemstack; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.types.MapType; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class ConverterFlattenSpawnEgg extends DataConverter, MapType> { -+ -+ private static final Map ENTITY_ID_TO_NEW_EGG_ID = new HashMap<>(); -+ static { -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:bat", "minecraft:bat_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:blaze", "minecraft:blaze_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:cave_spider", "minecraft:cave_spider_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:chicken", "minecraft:chicken_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:cow", "minecraft:cow_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:creeper", "minecraft:creeper_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:donkey", "minecraft:donkey_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:elder_guardian", "minecraft:elder_guardian_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:enderman", "minecraft:enderman_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:endermite", "minecraft:endermite_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:evocation_illager", "minecraft:evocation_illager_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:ghast", "minecraft:ghast_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:guardian", "minecraft:guardian_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:ender_dragon", "minecraft:ender_dragon_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:horse", "minecraft:horse_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:husk", "minecraft:husk_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:iron_golem", "minecraft:iron_golem_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:llama", "minecraft:llama_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:magma_cube", "minecraft:magma_cube_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:mooshroom", "minecraft:mooshroom_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:mule", "minecraft:mule_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:ocelot", "minecraft:ocelot_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:pufferfish", "minecraft:pufferfish_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:parrot", "minecraft:parrot_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:pig", "minecraft:pig_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:polar_bear", "minecraft:polar_bear_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:rabbit", "minecraft:rabbit_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:sheep", "minecraft:sheep_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:shulker", "minecraft:shulker_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:silverfish", "minecraft:silverfish_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:skeleton", "minecraft:skeleton_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:skeleton_horse", "minecraft:skeleton_horse_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:slime", "minecraft:slime_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:snow_golem", "minecraft:snow_golem_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:spider", "minecraft:spider_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:squid", "minecraft:squid_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:stray", "minecraft:stray_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:turtle", "minecraft:turtle_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:vex", "minecraft:vex_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:villager", "minecraft:villager_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:vindication_illager", "minecraft:vindication_illager_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:witch", "minecraft:witch_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:wither", "minecraft:wither_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:wither_skeleton", "minecraft:wither_skeleton_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:wolf", "minecraft:wolf_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:zombie", "minecraft:zombie_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:zombie_horse", "minecraft:zombie_horse_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:zombie_pigman", "minecraft:zombie_pigman_spawn_egg"); -+ ENTITY_ID_TO_NEW_EGG_ID.put("minecraft:zombie_villager", "minecraft:zombie_villager_spawn_egg"); -+ } -+ -+ public ConverterFlattenSpawnEgg(final int version, final int versionStep) { -+ super(version, versionStep); -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType tag = data.getMap("tag"); -+ if (tag == null) { -+ return null; -+ } -+ -+ final MapType entityTag = tag.getMap("EntityTag"); -+ if (entityTag == null) { -+ return null; -+ } -+ -+ final String id = entityTag.getString("id"); -+ if (id != null) { -+ data.setString("id", ENTITY_ID_TO_NEW_EGG_ID.getOrDefault(id, "minecraft:pig_spawn_egg")); -+ } -+ -+ return null; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterItemStackToDataComponents.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterItemStackToDataComponents.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1354b139f929010bdb090764d6151ac9569df753 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/itemstack/ConverterItemStackToDataComponents.java -@@ -0,0 +1,1244 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.itemstack; -+ -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.minecraft.util.ComponentUtils; -+import ca.spottedleaf.dataconverter.minecraft.versions.V3818; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.TypeUtil; -+import ca.spottedleaf.dataconverter.util.NamespaceUtil; -+import net.minecraft.util.Mth; -+import java.util.Arrays; -+import java.util.HashSet; -+import java.util.Set; -+ -+public final class ConverterItemStackToDataComponents { -+ -+ private static final int TOOLTIP_FLAG_HIDE_ENCHANTMENTS = 1 << 0; -+ private static final int TOOLTIP_FLAG_HIDE_MODIFIERS = 1 << 1; -+ private static final int TOOLTIP_FLAG_HIDE_UNBREAKABLE = 1 << 2; -+ private static final int TOOLTIP_FLAG_HIDE_CAN_DESTROY = 1 << 3; -+ private static final int TOOLTIP_FLAG_HIDE_CAN_PLACE = 1 << 4; -+ private static final int TOOLTIP_FLAG_HIDE_ADDITIONAL = 1 << 5; -+ private static final int TOOLTIP_FLAG_HIDE_DYE = 1 << 6; -+ private static final int TOOLTIP_FLAG_HIDE_UPGRADES = 1 << 7; -+ -+ private static final int DEFAULT_LEATHER_COLOUR = (160 << 16) | (101 << 8) | (64 << 0); // r, g, b -+ -+ private static final String[] BUCKETED_MOB_TAGS = new String[] { -+ "NoAI", -+ "Silent", -+ "NoGravity", -+ "Glowing", -+ "Invulnerable", -+ "Health", -+ "Age", -+ "Variant", -+ "HuntingCooldown", -+ "BucketVariantTag" -+ }; -+ private static final Set BOOLEAN_BLOCK_STATE_PROPERTIES = new HashSet<>( -+ Arrays.asList( -+ "attached", -+ "bottom", -+ "conditional", -+ "disarmed", -+ "drag", -+ "enabled", -+ "extended", -+ "eye", -+ "falling", -+ "hanging", -+ "has_bottle_0", -+ "has_bottle_1", -+ "has_bottle_2", -+ "has_record", -+ "has_book", -+ "inverted", -+ "in_wall", -+ "lit", -+ "locked", -+ "occupied", -+ "open", -+ "persistent", -+ "powered", -+ "short", -+ "signal_fire", -+ "snowy", -+ "triggered", -+ "unstable", -+ "waterlogged", -+ "berries", -+ "bloom", -+ "shrieking", -+ "can_summon", -+ "up", -+ "down", -+ "north", -+ "east", -+ "south", -+ "west", -+ "slot_0_occupied", -+ "slot_1_occupied", -+ "slot_2_occupied", -+ "slot_3_occupied", -+ "slot_4_occupied", -+ "slot_5_occupied", -+ "cracked", -+ "crafting" -+ ) -+ ); -+ private static final String[] MAP_DECORATION_CONVERSION_TABLE = new String[34]; -+ static { -+ MAP_DECORATION_CONVERSION_TABLE[0] = "player"; -+ MAP_DECORATION_CONVERSION_TABLE[1] = "frame"; -+ MAP_DECORATION_CONVERSION_TABLE[2] = "red_marker"; -+ MAP_DECORATION_CONVERSION_TABLE[3] = "blue_marker"; -+ MAP_DECORATION_CONVERSION_TABLE[4] = "target_x"; -+ MAP_DECORATION_CONVERSION_TABLE[5] = "target_point"; -+ MAP_DECORATION_CONVERSION_TABLE[6] = "player_off_map"; -+ MAP_DECORATION_CONVERSION_TABLE[7] = "player_off_limits"; -+ MAP_DECORATION_CONVERSION_TABLE[8] = "mansion"; -+ MAP_DECORATION_CONVERSION_TABLE[9] = "monument"; -+ MAP_DECORATION_CONVERSION_TABLE[10] = "banner_white"; -+ MAP_DECORATION_CONVERSION_TABLE[11] = "banner_orange"; -+ MAP_DECORATION_CONVERSION_TABLE[12] = "banner_magenta"; -+ MAP_DECORATION_CONVERSION_TABLE[13] = "banner_light_blue"; -+ MAP_DECORATION_CONVERSION_TABLE[14] = "banner_yellow"; -+ MAP_DECORATION_CONVERSION_TABLE[15] = "banner_lime"; -+ MAP_DECORATION_CONVERSION_TABLE[16] = "banner_pink"; -+ MAP_DECORATION_CONVERSION_TABLE[17] = "banner_gray"; -+ MAP_DECORATION_CONVERSION_TABLE[18] = "banner_light_gray"; -+ MAP_DECORATION_CONVERSION_TABLE[19] = "banner_cyan"; -+ MAP_DECORATION_CONVERSION_TABLE[20] = "banner_purple"; -+ MAP_DECORATION_CONVERSION_TABLE[21] = "banner_blue"; -+ MAP_DECORATION_CONVERSION_TABLE[22] = "banner_brown"; -+ MAP_DECORATION_CONVERSION_TABLE[23] = "banner_green"; -+ MAP_DECORATION_CONVERSION_TABLE[24] = "banner_red"; -+ MAP_DECORATION_CONVERSION_TABLE[25] = "banner_black"; -+ MAP_DECORATION_CONVERSION_TABLE[26] = "red_x"; -+ MAP_DECORATION_CONVERSION_TABLE[27] = "village_desert"; -+ MAP_DECORATION_CONVERSION_TABLE[28] = "village_plains"; -+ MAP_DECORATION_CONVERSION_TABLE[29] = "village_savanna"; -+ MAP_DECORATION_CONVERSION_TABLE[30] = "village_snowy"; -+ MAP_DECORATION_CONVERSION_TABLE[31] = "village_taiga"; -+ MAP_DECORATION_CONVERSION_TABLE[32] = "jungle_temple"; -+ MAP_DECORATION_CONVERSION_TABLE[33] = "swamp_hut"; -+ } -+ -+ private static String convertMapDecorationId(final int type) { -+ return type >= 0 && type < MAP_DECORATION_CONVERSION_TABLE.length ? MAP_DECORATION_CONVERSION_TABLE[type] : MAP_DECORATION_CONVERSION_TABLE[0]; -+ } -+ -+ private static void convertBlockStateProperties(final MapType properties) { -+ // convert values stored as boolean/integer to string -+ for (final String key : properties.keys()) { -+ final Object value = properties.getGeneric(key); -+ if (value instanceof Number number) { -+ if (BOOLEAN_BLOCK_STATE_PROPERTIES.contains(key)) { -+ properties.setString(key, Boolean.toString(number.byteValue() != (byte)0)); -+ } else { -+ properties.setString(key, number.toString()); -+ } -+ } -+ } -+ } -+ -+ private static void convertTileEntity(final MapType tileEntity, final TransientItemStack transientItem) { -+ final Object lock = tileEntity.getGeneric("Lock"); -+ if (lock != null) { -+ tileEntity.remove("Lock"); -+ transientItem.componentSetGeneric("minecraft:lock", lock); -+ } -+ -+ final Object lootTable = tileEntity.getGeneric("LootTable"); -+ if (lootTable != null) { -+ final MapType containerLoot = tileEntity.getTypeUtil().createEmptyMap(); -+ transientItem.componentSetMap("minecraft:container_loot", containerLoot); -+ -+ containerLoot.setGeneric("loot_table", lootTable); -+ -+ final long seed = tileEntity.getLong("LootTableSeed", 0L); -+ if (seed != 0L) { -+ containerLoot.setLong("seed", seed); -+ } -+ -+ tileEntity.remove("LootTable"); -+ tileEntity.remove("LootTableSeed"); -+ } -+ -+ final String id = NamespaceUtil.correctNamespace(tileEntity.getString("id", "")); -+ -+ switch (id) { -+ case "minecraft:skull": { -+ final Object noteBlockSound = tileEntity.getGeneric("note_block_sound"); -+ if (noteBlockSound != null) { -+ tileEntity.remove("note_block_sound"); -+ transientItem.componentSetGeneric("minecraft:note_block_sound", noteBlockSound); -+ } -+ -+ break; -+ } -+ case "minecraft:decorated_pot": { -+ final Object sherds = tileEntity.getGeneric("sherds"); -+ if (sherds != null) { -+ tileEntity.remove("sherds"); -+ transientItem.componentSetGeneric("minecraft:pot_decorations", sherds); -+ } -+ -+ final Object item = tileEntity.getGeneric("item"); -+ if (item != null) { -+ tileEntity.remove("item"); -+ -+ final ListType container = tileEntity.getTypeUtil().createEmptyList(); -+ transientItem.componentSetList("minecraft:container", container); -+ -+ final MapType wrappedItem = tileEntity.getTypeUtil().createEmptyMap(); -+ container.addMap(wrappedItem); -+ -+ wrappedItem.setInt("slot", 0); -+ wrappedItem.setGeneric("item", item); -+ } -+ -+ break; -+ } -+ case "minecraft:banner": { -+ final Object patterns = tileEntity.getGeneric("patterns"); -+ if (patterns != null) { -+ tileEntity.remove("patterns"); -+ -+ transientItem.componentSetGeneric("minecraft:banner_patterns", patterns); -+ } -+ -+ final Number base = tileEntity.getNumber("Base"); -+ if (base != null) { -+ tileEntity.remove("Base"); -+ -+ transientItem.componentSetString("minecraft:base_color", V3818.getBannerColour(base.intValue())); -+ } -+ -+ break; -+ } -+ -+ case "minecraft:shulker_box": -+ case "minecraft:chest": -+ case "minecraft:trapped_chest": -+ case "minecraft:furnace": -+ case "minecraft:ender_chest": -+ case "minecraft:dispenser": -+ case "minecraft:dropper": -+ case "minecraft:brewing_stand": -+ case "minecraft:hopper": -+ case "minecraft:barrel": -+ case "minecraft:smoker": -+ case "minecraft:blast_furnace": -+ case "minecraft:campfire": -+ case "minecraft:chiseled_bookshelf": -+ case "minecraft:crafter": { -+ final ListType items = tileEntity.getList("Items", ObjectType.MAP); -+ tileEntity.remove("Items"); -+ if (items != null && items.size() > 1) { -+ transientItem.componentSetList("minecraft:container", items); -+ -+ for (int i = 0, len = items.size(); i < len; ++i) { -+ final MapType item = items.getMap(i); -+ final int slot = (int)item.getByte("Slot", (byte)0) & 0xFF; -+ item.remove("Slot"); -+ -+ final MapType wrappedItem = item.getTypeUtil().createEmptyMap(); -+ items.setMap(i, wrappedItem); -+ -+ wrappedItem.setInt("slot", slot); -+ wrappedItem.setMap("item", item); -+ } -+ } -+ -+ break; -+ } -+ -+ case "minecraft:beehive": { -+ final Object bees = tileEntity.getGeneric("bees"); -+ if (bees != null) { -+ tileEntity.remove("bees"); -+ -+ transientItem.componentSetGeneric("minecraft:bees", bees); -+ } -+ break; -+ } -+ } -+ } -+ -+ private static void convertEnchantments(final TransientItemStack transientItem, final TypeUtil type, -+ final String tagKey, final String componentKey, -+ final boolean hideToolTip) { -+ final ListType enchantments = transientItem.tagRemoveList(tagKey, ObjectType.MAP); -+ if (enchantments == null || enchantments.size() == 0) { -+ if (hideToolTip) { -+ final MapType newEnchants = type.createEmptyMap(); -+ transientItem.componentSetMap(componentKey, newEnchants); -+ -+ newEnchants.setMap("levels", type.createEmptyMap()); -+ newEnchants.setBoolean("show_in_tooltip", false); -+ } -+ } else { -+ final MapType newEnchantments = type.createEmptyMap(); -+ -+ for (int i = 0, len = enchantments.size(); i < len; ++i) { -+ final MapType enchantment = enchantments.getMap(i); -+ -+ final String id = enchantment.getString("id"); -+ final Number level = enchantment.getNumber("lvl"); -+ -+ if (id == null || level == null) { -+ continue; -+ } -+ -+ newEnchantments.setInt(id, Mth.clamp(level.intValue(), 0, 0xFF)); -+ } -+ -+ if (!newEnchantments.isEmpty() || hideToolTip) { -+ final MapType newEnchants = type.createEmptyMap(); -+ transientItem.componentSetMap(componentKey, newEnchants); -+ -+ newEnchants.setMap("levels", newEnchantments); -+ if (hideToolTip) { -+ newEnchants.setBoolean("show_in_tooltip", false); -+ } -+ } -+ } -+ -+ if (enchantments != null && enchantments.size() == 0) { -+ transientItem.componentSetBoolean("minecraft:enchantment_glint_override", true); -+ } -+ } -+ -+ private static void convertDisplay(final TransientItemStack transientItem, final TypeUtil type, final int flags) { -+ final MapType display = transientItem.tag.getMap("display"); -+ -+ if (display != null) { -+ final Object name = display.getGeneric("Name"); -+ if (name != null) { -+ display.remove("Name"); -+ -+ transientItem.componentSetGeneric("minecraft:custom_name", name); -+ } -+ -+ final Object lore = display.getGeneric("Lore"); -+ if (lore != null) { -+ display.remove("Lore"); -+ -+ transientItem.componentSetGeneric("minecraft:lore", lore); -+ } -+ } -+ -+ final Number color = display == null ? null : display.getNumber("color"); -+ final boolean hideDye = (flags & TOOLTIP_FLAG_HIDE_DYE) != 0; -+ -+ if (hideDye || color != null) { -+ if (color != null) { -+ display.remove("color"); -+ } -+ -+ final MapType dyedColor = type.createEmptyMap(); -+ transientItem.componentSetMap("minecraft:dyed_color", dyedColor); -+ -+ dyedColor.setInt("rgb", color == null ? DEFAULT_LEATHER_COLOUR : color.intValue()); -+ if (hideDye) { -+ dyedColor.setBoolean("show_in_tooltip", false); -+ } -+ } -+ -+ final Object locName = display == null ? null : display.getGeneric("LocName"); -+ if (locName != null) { -+ display.remove("LocName"); -+ -+ if (locName instanceof String locNameString) { -+ transientItem.componentSetString("minecraft:item_name", ComponentUtils.createTranslatableComponent(locNameString)); -+ } -+ } -+ -+ if (display != null && "minecraft:filled_map".equals(transientItem.id)) { -+ final Object mapColor = display.getGeneric("MapColor"); -+ if (mapColor != null) { -+ display.remove("MapColor"); -+ -+ transientItem.componentSetGeneric("minecraft:map_color", mapColor); -+ } -+ } -+ -+ // mirror behavior of fixSubTag -+ if (display != null && display.isEmpty()) { -+ transientItem.tagRemoveMap("display"); -+ } -+ } -+ -+ public static MapType convertBlockStatePredicate(final String value, final TypeUtil type) { -+ final int propertyStart = value.indexOf('['); -+ final int nbtStart = value.indexOf('{'); -+ int blockNameEnd = value.length(); -+ -+ if (propertyStart != -1) { -+ blockNameEnd = propertyStart; -+ } -+ if (nbtStart != -1) { -+ blockNameEnd = Math.min(blockNameEnd, nbtStart); -+ } -+ -+ final MapType ret = type.createEmptyMap(); -+ -+ final String blockName = value.substring(0, blockNameEnd); -+ -+ // string is fine here, the underlying type accepts string AND list under the same name... -+ ret.setString("blocks", blockName.trim()); -+ -+ if (propertyStart != -1) { -+ // unlike DFU, set the fromIndex so that on malformed data we do not IOOBE -+ final int propertyEnd = value.indexOf(']', propertyStart + 1); -+ if (propertyEnd != -1) { -+ final MapType state = type.createEmptyMap(); -+ ret.setMap("state", state); -+ -+ for (final String property : value.substring(propertyStart + 1, propertyEnd).split(",")) { -+ final int separatorIdx = property.indexOf('='); -+ if (separatorIdx == -1) { -+ continue; -+ } -+ -+ final String propertyKey = property.substring(0, separatorIdx).trim(); -+ final String propertyValue = property.substring(separatorIdx + 1); -+ -+ state.setString(propertyKey, propertyValue); -+ } -+ } -+ } -+ -+ if (nbtStart != -1) { -+ // unlike DFU, set the fromIndex so that on malformed data we do not IOOBE -+ final int nbtEnd = value.indexOf('}', nbtStart + 1); -+ if (nbtEnd != -1) { -+ // note: want to include { and } -+ ret.setString("nbt", value.substring(nbtStart, nbtEnd + 1)); -+ } -+ } -+ -+ return ret; -+ } -+ -+ private static void convertBlockStatePredicates(final TransientItemStack item, final TypeUtil type, -+ final String tagKey, final String componentKey, -+ final boolean hideInTooltip) { -+ final ListType blocks = item.tagRemoveListUnchecked(tagKey); -+ if (blocks == null) { -+ return; -+ } -+ -+ final MapType blockPredicates = type.createEmptyMap(); -+ item.componentSetMap(componentKey, blockPredicates); -+ -+ if (hideInTooltip) { -+ blockPredicates.setBoolean("show_in_tooltip", false); -+ } -+ -+ final ListType predicates = type.createEmptyList(); -+ blockPredicates.setList("predicates", predicates); -+ -+ for (int i = 0, len = blocks.size(); i < len; ++i) { -+ final Object block = blocks.getGeneric(i); -+ if (!(block instanceof String blockString)) { -+ // cannot type error here, if block is not a string then nothing in `blocks` is as they have the same type -+ predicates.addGeneric(block); -+ continue; -+ } -+ -+ final MapType predicate = convertBlockStatePredicate(blockString, type); -+ -+ predicates.addMap(predicate); -+ } -+ } -+ -+ private static void convertAdventureMode(final TransientItemStack item, final TypeUtil type, final int flags) { -+ convertBlockStatePredicates( -+ item, type, "CanDestroy", "minecraft:can_break", -+ (flags & TOOLTIP_FLAG_HIDE_CAN_DESTROY) != 0 -+ ); -+ convertBlockStatePredicates( -+ item, type, "CanPlaceOn", "minecraft:can_place_on", -+ (flags & TOOLTIP_FLAG_HIDE_CAN_PLACE) != 0 -+ ); -+ } -+ -+ private static void copy(final MapType src, final String srcKey, final MapType dst, final String dstKey) { -+ if (src == null || dst == null) { -+ return; -+ } -+ -+ final Object srcValue = src.getGeneric(srcKey); -+ if (srcValue != null) { -+ dst.setGeneric(dstKey, srcValue); -+ } -+ } -+ -+ private static MapType convertAttribute(final Object inputGeneric, final TypeUtil type) { -+ final MapType input = inputGeneric instanceof MapType casted ? (MapType)casted : null; -+ -+ final MapType ret = type.createEmptyMap(); -+ ret.setString("name", ""); -+ ret.setDouble("amount", 0.0); -+ ret.setString("operation", "add_value"); -+ -+ copy(input, "AttributeName", ret, "type"); -+ copy(input, "Slot", ret, "slot"); -+ copy(input, "UUID", ret, "uuid"); -+ copy(input, "Name", ret, "name"); -+ copy(input, "Amount", ret, "amount"); -+ -+ // note: no type check on hasKey -+ if (input != null && input.hasKey("Operation")) { -+ final String operation; -+ switch (input.getInt("Operation", 0)) { -+ case 1: { -+ operation = "add_multiplied_base"; -+ break; -+ } -+ case 2: { -+ operation = "add_multiplied_total"; -+ break; -+ } -+ default: { -+ operation = "add_value"; -+ break; -+ } -+ } -+ ret.setString("operation", operation); -+ } -+ -+ return ret; -+ } -+ -+ private static void convertAttributes(final TransientItemStack item, final TypeUtil type, final int flags) { -+ final ListType attributes = item.tagRemoveListUnchecked("AttributeModifiers"); -+ final ListType newAttributes = type.createEmptyList(); -+ -+ if (attributes != null) { -+ for (int i = 0, len = attributes.size(); i < len; ++i) { -+ newAttributes.addMap(convertAttribute(attributes.getGeneric(i), type)); -+ } -+ } -+ -+ final boolean hideModifiers = (flags & TOOLTIP_FLAG_HIDE_MODIFIERS) != 0; -+ if (newAttributes.size() > 0 || hideModifiers) { -+ final MapType newModifiers = type.createEmptyMap(); -+ item.componentSetMap("minecraft:attribute_modifiers", newModifiers); -+ -+ newModifiers.setList("modifiers", newAttributes); -+ if (hideModifiers) { -+ newModifiers.setBoolean("show_in_tooltip", false); -+ } -+ } -+ } -+ -+ private static void convertMap(final TransientItemStack item, final TypeUtil type) { -+ item.tagMigrateToComponent("map", "minecraft:map_id"); -+ -+ final ListType decorations = item.tagRemoveListUnchecked("Decorations"); -+ if (decorations != null) { -+ final MapType newDecorations = type.createEmptyMap(); -+ -+ for (int i = 0, len = decorations.size(); i < len; ++i) { -+ final Object decorationGeneric = decorations.getGeneric(i); -+ -+ final MapType decoration = decorationGeneric instanceof MapType casted ? (MapType)casted : null; -+ -+ // note: getForcedString mirrors DFU converting to string for key -+ final String id = decoration == null ? "" : decoration.getForcedString("id", ""); -+ if (newDecorations.hasKey(id)) { -+ // note: never replace existing decorations by id -+ continue; -+ } -+ -+ final int typeId = decoration == null ? 0 : decoration.getInt("type", 0); -+ final double x = decoration == null ? 0.0 : decoration.getDouble("x", 0.0); -+ final double z = decoration == null ? 0.0 : decoration.getDouble("z", 0.0); -+ final float rot = decoration == null ? 0.0f : (float)decoration.getDouble("rot", 0.0); -+ -+ final MapType newDecoration = type.createEmptyMap(); -+ newDecorations.setMap(id, newDecoration); -+ -+ newDecoration.setString("type", convertMapDecorationId(typeId)); -+ newDecoration.setDouble("x", x); -+ newDecoration.setDouble("z", z); -+ newDecoration.setFloat("rotation", rot); -+ } -+ -+ if (!newDecorations.isEmpty()) { -+ item.componentSetMap("minecraft:map_decorations", newDecorations); -+ } -+ } -+ } -+ -+ private static void convertPotion(final TransientItemStack item, final TypeUtil type) { -+ final MapType potionContents = type.createEmptyMap(); -+ -+ final String potion = item.tagRemoveString("Potion"); -+ -+ if (potion != null && !"minecraft:empty".equals(potion)) { -+ potionContents.setString("potion", potion); -+ } -+ -+ item.migrateTagTo("CustomPotionColor", potionContents, "custom_color"); -+ item.migrateTagTo("custom_potion_effects", potionContents, "custom_effects"); -+ -+ if (!potionContents.isEmpty()) { -+ item.componentSetMap("minecraft:potion_contents", potionContents); -+ } -+ } -+ -+ private static MapType makeFilteredText(final String raw, final String filtered, final TypeUtil type) { -+ final MapType ret = type.createEmptyMap(); -+ -+ ret.setString("raw", raw); -+ if (filtered != null) { -+ ret.setString("filtered", filtered); -+ } -+ -+ return ret; -+ } -+ -+ private static ListType convertBookPages(final TransientItemStack item, final TypeUtil type) { -+ final ListType oldPages = item.tagRemoveListUnchecked("pages"); -+ -+ final MapType filteredPages = item.tagRemoveMap("filtered_pages"); -+ -+ if (oldPages == null || oldPages.size() == 0) { -+ return null; -+ } -+ -+ final ListType ret = type.createEmptyList(); -+ -+ for (int i = 0, len = oldPages.size(); i < len; ++i) { -+ final String page = oldPages.getGeneric(i) instanceof String str ? str : ""; -+ final String filtered = filteredPages == null ? null : filteredPages.getString(Integer.toString(i)); -+ -+ ret.addMap(makeFilteredText(page, filtered, type)); -+ } -+ -+ return ret; -+ } -+ -+ private static void convertWritableBook(final TransientItemStack item, final TypeUtil type) { -+ final ListType pages = convertBookPages(item, type); -+ if (pages != null) { -+ final MapType bookContent = type.createEmptyMap(); -+ item.componentSetMap("minecraft:writable_book_content", bookContent); -+ -+ bookContent.setList("pages", pages); -+ } -+ } -+ -+ private static void convertWrittenBook(final TransientItemStack item, final TypeUtil type) { -+ final ListType pages = convertBookPages(item, type); -+ -+ final MapType bookContent = type.createEmptyMap(); -+ item.componentSetMap("minecraft:written_book_content", bookContent); -+ if (pages != null) { -+ bookContent.setList("pages", pages); -+ } -+ -+ final String title = item.tagRemoveString("title", ""); -+ final String filteredTitle = item.tagRemoveString("filtered_title"); -+ -+ bookContent.setMap("title", makeFilteredText(title, filteredTitle, type)); -+ -+ item.migrateTagTo("author", bookContent, "author"); -+ item.migrateTagTo("resolved", bookContent, "resolved"); -+ item.migrateTagTo("generation", bookContent, "generation"); -+ } -+ -+ private static void convertMobBucket(final TransientItemStack item, final TypeUtil type) { -+ final MapType bucketEntityData = type.createEmptyMap(); -+ -+ for (final String oldKey : BUCKETED_MOB_TAGS) { -+ item.migrateTagTo(oldKey, bucketEntityData, oldKey); -+ } -+ -+ if (!bucketEntityData.isEmpty()) { -+ item.componentSetMap("minecraft:bucket_entity_data", bucketEntityData); -+ } -+ } -+ -+ private static void convertCompass(final TransientItemStack item, final TypeUtil type) { -+ final Object lodestonePos = item.tagRemoveGeneric("LodestonePos"); -+ final Object lodestoneDim = item.tagRemoveGeneric("LodestoneDimension"); -+ -+ if (lodestonePos == null && lodestoneDim == null) { -+ return; -+ } -+ -+ final MapType lodestoneTracker = type.createEmptyMap(); -+ item.componentSetMap("minecraft:lodestone_tracker", lodestoneTracker); -+ -+ if (lodestonePos != null && lodestoneDim != null) { -+ final MapType target = type.createEmptyMap(); -+ lodestoneTracker.setMap("target", target); -+ -+ target.setGeneric("pos", lodestonePos); -+ target.setGeneric("dimension", lodestoneDim); -+ } -+ -+ final boolean tracked = item.tagRemoveBoolean("LodestoneTracked", true); -+ if (!tracked) { -+ lodestoneTracker.setBoolean("tracked", false); -+ } -+ } -+ -+ private static void convertFireworkExplosion(final Object inputGeneric) { -+ if (!(inputGeneric instanceof MapType)) { -+ return; -+ } -+ -+ final MapType input = (MapType)inputGeneric; -+ -+ RenameHelper.renameSingle(input, "Colors", "colors"); -+ RenameHelper.renameSingle(input, "FadeColors", "fade_colors"); -+ RenameHelper.renameSingle(input, "Trail", "has_trail"); -+ RenameHelper.renameSingle(input, "Flicker", "has_twinkle"); -+ -+ final int type = input.getInt("Type", 0); -+ input.remove("Type"); -+ -+ final String newType; -+ switch (type) { -+ case 1: { -+ newType = "large_ball"; -+ break; -+ } -+ case 2: { -+ newType = "star"; -+ break; -+ } -+ case 3: { -+ newType = "creeper"; -+ break; -+ } -+ case 4: { -+ newType = "burst"; -+ break; -+ } -+ default: { -+ newType = "small_ball"; -+ break; -+ } -+ } -+ -+ input.setString("shape", newType); -+ } -+ -+ private static void convertFireworkRocket(final TransientItemStack item, final TypeUtil type) { -+ // adhere to fixSubTag(true) behavior -+ final Object fireworksGeneric = item.tag.getGeneric("Fireworks"); -+ if (fireworksGeneric == null) { -+ return; -+ } -+ -+ if (!(fireworksGeneric instanceof MapType)) { -+ final MapType newFireworks = type.createEmptyMap(); -+ item.componentSetMap("minecraft:fireworks", newFireworks); -+ -+ newFireworks.setList("explosions", type.createEmptyList()); -+ newFireworks.setByte("flight_duration", (byte)0); -+ -+ return; -+ } -+ -+ final MapType fireworks = (MapType)fireworksGeneric; -+ -+ final MapType newFireworks = type.createEmptyMap(); -+ item.componentSetMap("minecraft:fireworks", newFireworks); -+ -+ final int flight = fireworks.getInt("Flight", 0); -+ newFireworks.setByte("flight_duration", (byte)flight); -+ -+ final ListType explosions = fireworks.getListUnchecked("Explosions", type.createEmptyList()); -+ newFireworks.setList("explosions", explosions); -+ -+ for (int i = 0, len = explosions.size(); i < len; ++i) { -+ convertFireworkExplosion(explosions.getGeneric(i)); -+ } -+ -+ fireworks.remove("Explosions"); -+ fireworks.remove("Flight"); -+ if (fireworks.isEmpty()) { -+ item.tag.remove("Fireworks"); -+ } -+ } -+ -+ private static Object copyGeneric(final Object value, final TypeUtil type) { -+ if (value == null || value instanceof Number || value instanceof String) { -+ return value; -+ } -+ if (value instanceof MapType mapType) { -+ return mapType.copy(); -+ } -+ if (value instanceof ListType listType) { -+ return listType.copy(); -+ } -+ // rest of the cases can take the slow path -+ -+ final ListType dummy = type.createEmptyList(); -+ dummy.addGeneric(value); -+ -+ return dummy.copy().getGeneric(0); -+ } -+ -+ private static void convertFireworkStar(final TransientItemStack item, final TypeUtil type) { -+ // note: adhere to fixSubTag(true) behavior -+ final Object explosionGeneric = item.tag.getGeneric("Explosion"); -+ if (explosionGeneric == null) { -+ return; -+ } -+ -+ if (!(explosionGeneric instanceof MapType)) { -+ // important that we copy the generic value when not moving it -+ item.componentSetGeneric("minecraft:firework_explosion", copyGeneric(explosionGeneric, type)); -+ return; -+ } -+ -+ final MapType explosion = (MapType)explosionGeneric; -+ -+ final MapType explosionCopy = explosion.copy(); -+ item.componentSetGeneric("minecraft:firework_explosion", explosionCopy); -+ convertFireworkExplosion(explosionCopy); -+ -+ explosion.remove("Type"); -+ explosion.remove("Colors"); -+ explosion.remove("FadeColors"); -+ explosion.remove("Trail"); -+ explosion.remove("Flicker"); -+ -+ if (explosion.isEmpty()) { -+ item.tag.remove("Explosion"); -+ } -+ } -+ -+ private static boolean isValidPlayerName(final String name) { -+ if (name.length() > 16) { -+ return false; -+ } -+ -+ for (int i = 0, len = name.length(); i < len; ++i) { -+ final char character = name.charAt(i); -+ if (character <= 0x20 || character >= 0x7F) { // printable ascii -+ return false; -+ } -+ } -+ -+ return true; -+ } -+ -+ private static ListType convertProperties(final MapType properties, final TypeUtil type) { -+ final ListType ret = type.createEmptyList(); -+ -+ for (final String propertyKey : properties.keys()) { -+ final ListType propertyValues = properties.getListUnchecked(propertyKey); -+ -+ if (propertyValues == null) { -+ continue; -+ } -+ -+ for (int i = 0, len = propertyValues.size(); i < len; ++i) { -+ final MapType property = propertyValues.getGeneric(i) instanceof MapType casted ? (MapType)casted : null; -+ -+ final String value = property == null ? "" : property.getString("Value", ""); -+ final String signature = property == null ? null : property.getString("Signature"); -+ -+ final MapType newProperty = type.createEmptyMap(); -+ ret.addMap(newProperty); -+ -+ newProperty.setString("name", propertyKey); -+ newProperty.setString("value", value); -+ if (signature != null) { -+ newProperty.setString("signature", signature); -+ } -+ } -+ } -+ -+ return ret; -+ } -+ -+ public static MapType convertProfile(final Object inputGeneric, final TypeUtil type) { -+ final MapType ret = type.createEmptyMap(); -+ -+ if (inputGeneric instanceof String name) { -+ if (!isValidPlayerName(name)) { -+ return ret; -+ } -+ -+ ret.setString("name", name); -+ -+ return ret; -+ } -+ -+ final MapType input = inputGeneric instanceof MapType casted ? (MapType)casted : null; -+ -+ final String name = input == null ? "" : input.getString("Name", ""); -+ -+ if (isValidPlayerName(name)) { -+ ret.setString("name", name); -+ } -+ -+ final Object id = input == null ? null : input.getGeneric("Id"); -+ -+ if (id != null) { -+ ret.setGeneric("id", id); -+ } -+ -+ final MapType properties = input == null ? null : input.getMap("Properties"); -+ if (properties != null && !properties.isEmpty()) { -+ ret.setList("properties", convertProperties(properties, type)); -+ } -+ -+ return ret; -+ } -+ -+ private static void convertSukll(final TransientItemStack item, final TypeUtil type) { -+ final Object skullOwnerGeneric = item.tagRemoveGeneric("SkullOwner"); -+ if (skullOwnerGeneric == null) { -+ return; -+ } -+ -+ item.componentSetMap("minecraft:profile", convertProfile(skullOwnerGeneric, type)); -+ } -+ -+ // input is unmodified -+ public static MapType convertItem(final MapType input) { -+ if (!input.hasKey("id", ObjectType.STRING) || !input.hasKey("Count", ObjectType.NUMBER)) { -+ return input.copy(); -+ } -+ -+ final TypeUtil type = input.getTypeUtil(); -+ -+ final TransientItemStack item = new TransientItemStack(input); -+ -+ item.tagMigrateToComponent("Damage", "minecraft:damage", 0); -+ item.tagMigrateToComponent("RepairCost", "minecraft:repair_cost", 0); -+ item.tagMigrateToComponent("CustomModelData", "minecraft:custom_model_data"); -+ -+ final MapType blockStateProperties = item.tagRemoveMap("BlockStateTag"); -+ if (blockStateProperties != null) { -+ item.componentSetMap("minecraft:block_state", blockStateProperties); -+ convertBlockStateProperties(blockStateProperties); -+ } -+ -+ item.tagMigrateToComponent("EntityTag", "minecraft:entity_data"); -+ -+ final MapType tileEntityTag = item.tagRemoveMap("BlockEntityTag"); -+ if (tileEntityTag != null) { -+ convertTileEntity(tileEntityTag, item); -+ -+ if (tileEntityTag.size() > 1 || (tileEntityTag.size() == 1 && !tileEntityTag.hasKey("id"))) { -+ item.componentSetMap("minecraft:block_entity_data", tileEntityTag); -+ } -+ } -+ -+ final int flags = item.tagRemoveInt("HideFlags", 0); -+ -+ if (item.tagRemoveInt("Unbreakable", 0) != 0) { -+ final MapType unbreakable = type.createEmptyMap(); -+ item.componentSetMap("minecraft:unbreakable", unbreakable); -+ if ((flags & TOOLTIP_FLAG_HIDE_UNBREAKABLE) != 0) { -+ unbreakable.setBoolean("show_in_tooltip", false); -+ } -+ } -+ -+ convertEnchantments( -+ item, type, "Enchantments", "minecraft:enchantments", -+ (flags & TOOLTIP_FLAG_HIDE_ENCHANTMENTS) != 0 -+ ); -+ -+ convertDisplay(item, type, flags); -+ convertAdventureMode(item, type, flags); -+ convertAttributes(item, type, flags); -+ -+ final Object trim = item.tagRemoveGeneric("Trim"); -+ if (trim != null) { -+ // note: DFU set does nothing if not map -+ if ((flags & TOOLTIP_FLAG_HIDE_UPGRADES) != 0 && trim instanceof MapType) { -+ ((MapType)trim).setBoolean("show_in_tooltip", false); -+ } -+ -+ item.componentSetGeneric("minecraft:trim", trim); -+ } -+ -+ if ((flags & TOOLTIP_FLAG_HIDE_ADDITIONAL) != 0) { -+ item.componentSetMap("minecraft:hide_additional_tooltip", type.createEmptyMap()); -+ } -+ -+ switch (item.id) { -+ case "minecraft:enchanted_book": { -+ convertEnchantments( -+ item, type, "StoredEnchantments", "minecraft:stored_enchantments", -+ (flags & TOOLTIP_FLAG_HIDE_ADDITIONAL) != 0 -+ ); -+ break; -+ } -+ case "minecraft:crossbow": { -+ item.tagRemoveGeneric("Charged"); -+ item.tagMigrateNonEmptyListToComponent("ChargedProjectiles", "minecraft:charged_projectiles"); -+ break; -+ } -+ case "minecraft:bundle": { -+ item.tagMigrateNonEmptyListToComponent("Items", "minecraft:bundle_contents"); -+ break; -+ } -+ case "minecraft:filled_map": { -+ convertMap(item, type); -+ break; -+ } -+ case "minecraft:potion": -+ case "minecraft:splash_potion": -+ case "minecraft:lingering_potion": -+ case "minecraft:tipped_arrow": { -+ convertPotion(item, type); -+ break; -+ } -+ case "minecraft:writable_book": { -+ convertWritableBook(item, type); -+ break; -+ } -+ case "minecraft:written_book": { -+ convertWrittenBook(item, type); -+ break; -+ } -+ case "minecraft:suspicious_stew": { -+ item.tagMigrateToComponent("effects", "minecraft:suspicious_stew_effects"); -+ break; -+ } -+ case "minecraft:debug_stick": { -+ item.tagMigrateToComponent("DebugProperty", "minecraft:debug_stick_state"); -+ break; -+ } -+ case "minecraft:pufferfish_bucket": -+ case "minecraft:salmon_bucket": -+ case "minecraft:cod_bucket": -+ case "minecraft:tropical_fish_bucket": -+ case "minecraft:axolotl_bucket": -+ case "minecraft:tadpole_bucket": { -+ convertMobBucket(item, type); -+ break; -+ } -+ case "minecraft:goat_horn": { -+ item.tagMigrateToComponent("instrument", "minecraft:instrument"); -+ break; -+ } -+ case "minecraft:knowledge_book": { -+ item.tagMigrateToComponent("Recipes", "minecraft:recipes"); -+ break; -+ } -+ case "minecraft:compass": { -+ convertCompass(item, type); -+ break; -+ } -+ case "minecraft:firework_rocket": { -+ convertFireworkRocket(item, type); -+ break; -+ } -+ case "minecraft:firework_star": { -+ convertFireworkStar(item, type); -+ break; -+ } -+ case "minecraft:player_head": { -+ convertSukll(item, type); -+ break; -+ } -+ } -+ -+ return item.serialize(); -+ } -+ -+ private ConverterItemStackToDataComponents() {} -+ -+ private static final class TransientItemStack { -+ -+ private final String id; -+ private final int count; -+ -+ private final MapType components; -+ private final MapType tag; -+ private final MapType root; -+ -+ public TransientItemStack(final MapType root) { -+ this.id = root.getString("id"); -+ this.count = root.getInt("Count"); -+ -+ final TypeUtil type = root.getTypeUtil(); -+ -+ this.components = type.createEmptyMap(); -+ -+ final MapType rootCopy = root.copy(); -+ -+ final MapType tag = rootCopy.getMap("tag"); -+ -+ rootCopy.remove("id"); -+ rootCopy.remove("Count"); -+ rootCopy.remove("tag"); -+ -+ this.tag = tag == null ? type.createEmptyMap() : tag; -+ -+ this.root = rootCopy; -+ } -+ -+ public void migrateTagTo(final String tagKey, final MapType dst, final String dstKey) { -+ final Object value = this.tag.getGeneric(tagKey); -+ -+ if (value != null) { -+ this.tag.remove(tagKey); -+ -+ dst.setGeneric(dstKey, value); -+ } -+ } -+ -+ public String tagRemoveString(final String key) { -+ final String ret = this.tag.getString(key); -+ -+ this.tag.remove(key); -+ -+ return ret; -+ } -+ -+ public String tagRemoveString(final String key, final String dfl) { -+ final String ret = this.tag.getString(key, dfl); -+ -+ this.tag.remove(key); -+ -+ return ret; -+ } -+ -+ public ListType tagRemoveListUnchecked(final String key) { -+ final ListType ret = this.tag.getListUnchecked(key); -+ -+ this.tag.remove(key); -+ -+ return ret; -+ } -+ -+ public ListType tagRemoveList(final String key, final ObjectType listType) { -+ final ListType ret = this.tag.getList(key, listType); -+ -+ this.tag.remove(key); -+ -+ return ret; -+ } -+ -+ public MapType tagRemoveMap(final String key) { -+ final MapType ret = this.tag.getMap(key); -+ -+ this.tag.remove(key); -+ -+ return ret; -+ } -+ -+ public boolean tagRemoveBoolean(final String key, final boolean dfl) { -+ final boolean ret = this.tag.getBoolean(key, dfl); -+ -+ this.tag.remove(key); -+ -+ return ret; -+ } -+ -+ public int tagRemoveInt(final String key, final int dfl) { -+ final int ret = this.tag.getInt(key, dfl); -+ -+ this.tag.remove(key); -+ -+ return ret; -+ } -+ -+ public Object tagRemoveGeneric(final String key) { -+ final Object ret = this.tag.getGeneric(key); -+ -+ if (ret != null) { -+ this.tag.remove(key); -+ return ret; -+ } -+ -+ return ret; -+ } -+ -+ public void tagMigrateToComponent(final String tagKey, final String componentKey) { -+ final Object value = this.tag.getGeneric(tagKey); -+ if (value != null) { -+ this.tag.remove(tagKey); -+ -+ this.components.setGeneric(componentKey, value); -+ } -+ } -+ -+ public void tagMigrateNonEmptyListToComponent(final String tagKey, final String componentKey) { -+ final Object value = this.tag.getGeneric(tagKey); -+ if (value != null) { -+ this.tag.remove(tagKey); -+ -+ if (!(value instanceof ListType list) || list.size() > 0) { -+ this.components.setGeneric(componentKey, value); -+ } -+ } -+ } -+ -+ public void tagMigrateToComponent(final String tagKey, final String componentKey, final int defaultComponent) { -+ final int value = this.tag.getInt(tagKey, defaultComponent); -+ this.tag.remove(tagKey); -+ -+ if (value != defaultComponent) { -+ this.components.setGeneric(componentKey, value); -+ } -+ } -+ -+ public void componentSetBoolean(final String key, final boolean value) { -+ this.components.setBoolean(key, value); -+ } -+ -+ public void componentSetString(final String key, final String value) { -+ this.components.setString(key, value); -+ } -+ -+ public void componentSetList(final String key, final ListType value) { -+ this.components.setList(key, value); -+ } -+ -+ public void componentSetMap(final String key, final MapType value) { -+ this.components.setMap(key, value); -+ } -+ -+ public void componentSetGeneric(final String key, final Object value) { -+ this.components.setGeneric(key, value); -+ } -+ -+ public MapType serialize() { -+ final MapType ret = this.components.getTypeUtil().createEmptyMap(); -+ -+ ret.setString("id", this.id); -+ ret.setInt("count", this.count); -+ if (!this.tag.isEmpty()) { -+ this.components.setMap("minecraft:custom_data", this.tag); -+ } -+ -+ if (!this.components.isEmpty()) { -+ ret.setMap("components", this.components); -+ } -+ -+ // merge root to ret, with entries in ret taking priority -+ if (!this.root.isEmpty()) { -+ for (final String key : this.root.keys()) { -+ if (ret.hasKey(key)) { -+ continue; -+ } -+ -+ ret.setGeneric(key, this.root.getGeneric(key)); -+ } -+ } -+ -+ return ret; -+ } -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/leveldat/ConverterRemoveFeatureFlag.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/leveldat/ConverterRemoveFeatureFlag.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4c537b661b7a28193add3267ec2d639add49423b ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/leveldat/ConverterRemoveFeatureFlag.java -@@ -0,0 +1,46 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.leveldat; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import java.util.Set; -+ -+public final class ConverterRemoveFeatureFlag extends DataConverter, MapType> { -+ -+ private final Set flags; -+ -+ public ConverterRemoveFeatureFlag(final int toVersion, final Set flags) { -+ this(toVersion, 0, flags); -+ } -+ -+ public ConverterRemoveFeatureFlag(final int toVersion, final int versionStep, final Set flags) { -+ super(toVersion, versionStep); -+ this.flags = flags; -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final ListType enabledFeatures = data.getList("enabled_features", ObjectType.STRING); -+ if (enabledFeatures == null) { -+ return null; -+ } -+ -+ ListType removedFeatures = null; -+ -+ for (int i = 0; i < enabledFeatures.size(); ++i) { -+ final String flag = enabledFeatures.getString(i); -+ if (!this.flags.contains(flag)) { -+ continue; -+ } -+ enabledFeatures.remove(i--); -+ -+ if (removedFeatures == null) { -+ removedFeatures = data.getOrCreateList("removed_features", ObjectType.STRING); -+ } -+ removedFeatures.addString(flag); -+ } -+ -+ return null; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/options/ConverterAbstractOptionsRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/options/ConverterAbstractOptionsRename.java -new file mode 100644 -index 0000000000000000000000000000000000000000..769dd8447976b66dcfc36283ede4ae16f1e4206d ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/options/ConverterAbstractOptionsRename.java -@@ -0,0 +1,28 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.options; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import java.util.ArrayList; -+import java.util.function.Function; -+ -+public final class ConverterAbstractOptionsRename { -+ -+ private ConverterAbstractOptionsRename() {} -+ -+ public static void register(final int version, final Function renamer) { -+ register(version, 0, renamer); -+ } -+ -+ public static void register(final int version, final int subVersion, final Function renamer) { -+ MCTypeRegistry.OPTIONS.addStructureConverter(new DataConverter<>(version, subVersion) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ RenameHelper.renameKeys(data, renamer); -+ return null; -+ } -+ }); -+ } -+ -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/particle/ConverterParticleToNBT.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/particle/ConverterParticleToNBT.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0f961bca2ab2cd9e93cb7c23951ac6b867cdd04e ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/particle/ConverterParticleToNBT.java -@@ -0,0 +1,268 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.particle; -+ -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.TypeUtil; -+import ca.spottedleaf.dataconverter.types.nbt.NBTMapType; -+import ca.spottedleaf.dataconverter.util.NamespaceUtil; -+import com.mojang.brigadier.StringReader; -+import com.mojang.brigadier.exceptions.CommandSyntaxException; -+import com.mojang.logging.LogUtils; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.nbt.TagParser; -+import net.minecraft.util.Mth; -+import org.slf4j.Logger; -+ -+public final class ConverterParticleToNBT { -+ -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ -+ private static CompoundTag parseNBT(final String flat) { -+ try { -+ return TagParser.parseTag(flat); -+ } catch (final Exception ex) { -+ LOGGER.warn("Failed to parse nbt: " + flat, ex); -+ return null; -+ } -+ } -+ -+ private static void convertItem(final MapType nbt, final String data) { -+ final MapType itemNBT = nbt.getTypeUtil().createEmptyMap(); -+ nbt.setMap("item", itemNBT); -+ itemNBT.setInt("Count", 1); -+ -+ final int nbtStart = data.indexOf('{'); -+ if (nbtStart == -1) { -+ // assume data is item name -+ itemNBT.setString("id", NamespaceUtil.correctNamespace(data)); -+ return; -+ } -+ // itemname{tagNBT} -+ itemNBT.setString("id", NamespaceUtil.correctNamespace(data.substring(0, nbtStart))); -+ -+ final CompoundTag tag = parseNBT(data.substring(nbtStart)); -+ if (tag != null) { -+ // do we need to worry about type conversion? -+ itemNBT.setMap("tag", new NBTMapType(tag)); -+ } -+ } -+ -+ private static MapType parseProperties(final String input, final TypeUtil type) { -+ final MapType ret = type.createEmptyMap(); -+ try { -+ // format: [p1=v1, p2=v2, p3=v3, ...] -+ final StringReader reader = new StringReader(input); -+ -+ reader.expect('['); -+ reader.skipWhitespace(); -+ -+ while (reader.canRead() && reader.peek() != ']') { -+ reader.skipWhitespace(); -+ -+ final String property = reader.readString(); -+ -+ reader.skipWhitespace(); -+ reader.expect('='); -+ reader.skipWhitespace(); -+ -+ final String value = reader.readString(); -+ ret.setString(property, value); -+ -+ reader.skipWhitespace(); -+ if (reader.canRead()) { -+ if (reader.peek() != ',') { -+ // invalid character or ']' -+ break; -+ } -+ -+ // skip ',' and move onto next entry -+ reader.peek(); -+ } -+ } -+ -+ reader.expect(']'); -+ return ret; -+ } catch (final Exception ex) { -+ LOGGER.warn("Failed to parse block properties: " + input, ex); -+ return null; -+ } -+ } -+ -+ private static void convertBlock(final MapType nbt, final String data) { -+ final MapType blockNBT = nbt.getTypeUtil().createEmptyMap(); -+ nbt.setMap("block_state", blockNBT); -+ -+ final int propertiesStart = data.indexOf('['); -+ if (propertiesStart == -1) { -+ // assume data is id -+ blockNBT.setString("Name", NamespaceUtil.correctNamespace(data)); -+ return; -+ } -+ blockNBT.setString("Name", NamespaceUtil.correctNamespace(data.substring(0, propertiesStart))); -+ -+ // blockname{properties} -+ final MapType properties = parseProperties(data.substring(propertiesStart), nbt.getTypeUtil()); -+ if (properties != null && !properties.isEmpty()) { -+ blockNBT.setMap("Properties", properties); -+ } -+ } -+ -+ private static ListType parseFloatVector(final StringReader reader, final TypeUtil type) throws CommandSyntaxException { -+ final float x = reader.readFloat(); -+ -+ reader.expect(' '); -+ final float y = reader.readFloat(); -+ -+ reader.expect(' '); -+ final float z = reader.readFloat(); -+ -+ final ListType ret = type.createEmptyList(); -+ ret.addFloat(x); -+ ret.addFloat(y); -+ ret.addFloat(z); -+ -+ return ret; -+ } -+ -+ private static void convertDust(final MapType nbt, final String data) { -+ try { -+ final StringReader reader = new StringReader(data); -+ -+ final ListType color = parseFloatVector(reader, nbt.getTypeUtil()); -+ -+ reader.expect(' '); -+ final float scale = reader.readFloat(); -+ -+ nbt.setList("color", color); -+ nbt.setFloat("scale", scale); -+ } catch (final Exception ex) { -+ LOGGER.warn("Failed to parse dust particle: " + data, ex); -+ } -+ } -+ -+ private static void convertColorDust(final MapType nbt, final String data) { -+ try { -+ final StringReader reader = new StringReader(data); -+ -+ final ListType fromColor = parseFloatVector(reader, nbt.getTypeUtil()); -+ -+ reader.expect(' '); -+ final float scale = reader.readFloat(); -+ -+ reader.expect(' '); -+ final ListType toColor = parseFloatVector(reader, nbt.getTypeUtil()); -+ -+ nbt.setList("from_color", fromColor); -+ nbt.setFloat("scale", scale); -+ nbt.setList("to_color", toColor); -+ } catch (final Exception ex) { -+ LOGGER.warn("Failed to parse color transition dust particle: " + data, ex); -+ } -+ } -+ -+ private static void convertSculk(final MapType nbt, final String data) { -+ try { -+ final StringReader reader = new StringReader(data); -+ -+ final float roll = reader.readFloat(); -+ -+ nbt.setFloat("roll", roll); -+ } catch (final Exception ex) { -+ LOGGER.warn("Failed to parse sculk particle: " + data, ex); -+ } -+ } -+ -+ private static void convertVibration(final MapType nbt, final String data) { -+ try { -+ final StringReader reader = new StringReader(data); -+ -+ final double posX = reader.readDouble(); -+ -+ reader.expect(' '); -+ final double posY = reader.readDouble(); -+ -+ reader.expect(' '); -+ final double posZ = reader.readDouble(); -+ -+ reader.expect(' '); -+ final int arrival = reader.readInt(); -+ -+ nbt.setInt("arrival_in_ticks", arrival); -+ -+ final MapType destination = nbt.getTypeUtil().createEmptyMap(); -+ nbt.setMap("destination", destination); -+ -+ destination.setString("type", "minecraft:block"); -+ -+ final ListType pos = nbt.getTypeUtil().createEmptyList(); -+ destination.setList("pos", pos); -+ -+ pos.addInt(Mth.floor(posX)); -+ pos.addInt(Mth.floor(posY)); -+ pos.addInt(Mth.floor(posZ)); -+ } catch (final Exception ex) { -+ LOGGER.warn("Failed to parse vibration particle: " + data, ex); -+ } -+ } -+ -+ private static void convertShriek(final MapType nbt, final String data) { -+ try { -+ final StringReader reader = new StringReader(data); -+ -+ final int delay = reader.readInt(); -+ -+ nbt.setInt("delay", delay); -+ } catch (final Exception ex) { -+ LOGGER.warn("Failed to parse shriek particle: " + data, ex); -+ } -+ } -+ -+ public static MapType convert(final String flat, final TypeUtil type) { -+ final String[] split = flat.split(" ", 2); -+ final String name = NamespaceUtil.correctNamespace(split[0]); -+ -+ final MapType ret = type.createEmptyMap(); -+ ret.setString("type", name); -+ -+ if (split.length > 1) { -+ final String data = split[1]; -+ switch (name) { -+ case "minecraft:item": { -+ convertItem(ret, data); -+ break; -+ } -+ case "minecraft:block": -+ case "minecraft:block_marker": -+ case "minecraft:falling_dust": -+ case "minecraft:dust_pillar": { -+ convertBlock(ret, data); -+ break; -+ } -+ case "minecraft:dust": { -+ convertDust(ret, data); -+ break; -+ } -+ case "minecraft:dust_color_transition": { -+ convertColorDust(ret, data); -+ break; -+ } -+ case "minecraft:sculk_charge": { -+ convertSculk(ret, data); -+ break; -+ } -+ case "minecraft:vibration": { -+ convertVibration(ret, data); -+ break; -+ } -+ case "minecraft:shriek": { -+ convertShriek(ret, data); -+ break; -+ } -+ } -+ } -+ -+ return ret; -+ } -+ -+ private ConverterParticleToNBT() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterAbstractPOIRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterAbstractPOIRename.java -new file mode 100644 -index 0000000000000000000000000000000000000000..57e210bf2bb189b15a32899011c4800b19668a5e ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterAbstractPOIRename.java -@@ -0,0 +1,53 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.poi; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import java.util.function.Function; -+ -+public final class ConverterAbstractPOIRename { -+ -+ private ConverterAbstractPOIRename() {} -+ -+ public static void register(final int version, final Function renamer) { -+ register(version, 0, renamer); -+ } -+ -+ public static void register(final int version, final int subVersion, final Function renamer) { -+ MCTypeRegistry.POI_CHUNK.addStructureConverter(new DataConverter<>(version, subVersion) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType sections = data.getMap("Sections"); -+ if (sections == null) { -+ return null; -+ } -+ -+ for (final String key : sections.keys()) { -+ final MapType section = sections.getMap(key); -+ -+ final ListType records = section.getList("Records", ObjectType.MAP); -+ -+ if (records == null) { -+ continue; -+ } -+ -+ for (int i = 0, len = records.size(); i < len; ++i) { -+ final MapType record = records.getMap(i); -+ -+ final String type = record.getString("type"); -+ if (type != null) { -+ final String converted = renamer.apply(type); -+ if (converted != null) { -+ record.setString("type", converted); -+ } -+ } -+ } -+ } -+ -+ return null; -+ } -+ }); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterPoiDelete.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterPoiDelete.java -new file mode 100644 -index 0000000000000000000000000000000000000000..36aa9c3eedb3f2e2f577efed3622fed74268bce1 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/poi/ConverterPoiDelete.java -@@ -0,0 +1,53 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.poi; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import java.util.function.Predicate; -+ -+public final class ConverterPoiDelete extends DataConverter, MapType> { -+ -+ private final Predicate delete; -+ -+ public ConverterPoiDelete(final int toVersion, final Predicate delete) { -+ super(toVersion); -+ this.delete = delete; -+ } -+ -+ public ConverterPoiDelete(final int toVersion, final int versionStep, final Predicate delete) { -+ super(toVersion, versionStep); -+ this.delete = delete; -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType sections = data.getMap("Sections"); -+ if (sections == null) { -+ return null; -+ } -+ -+ for (final String key : sections.keys()) { -+ final MapType section = sections.getMap(key); -+ -+ final ListType records = section.getList("Records", ObjectType.MAP); -+ -+ if (records == null) { -+ continue; -+ } -+ -+ for (int i = 0; i < records.size();) { -+ final MapType record = records.getMap(i); -+ -+ final String type = record.getString("type"); -+ if (type != null && this.delete.test(type)) { -+ records.remove(i); -+ continue; -+ } -+ ++i; -+ } -+ } -+ -+ return null; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/recipe/ConverterAbstractRecipeRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/recipe/ConverterAbstractRecipeRename.java -new file mode 100644 -index 0000000000000000000000000000000000000000..8f35cbbd78a629712f9ae3cd5d180269f015a11d ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/recipe/ConverterAbstractRecipeRename.java -@@ -0,0 +1,18 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.recipe; -+ -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import java.util.function.Function; -+ -+public final class ConverterAbstractRecipeRename { -+ -+ private ConverterAbstractRecipeRename() {} -+ -+ public static void register(final int version, final Function renamer) { -+ register(version, 0, renamer); -+ } -+ -+ public static void register(final int version, final int subVersion, final Function renamer) { -+ ConverterAbstractStringValueTypeRename.register(version, subVersion, MCTypeRegistry.RECIPE, renamer); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterAbstractStatsRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterAbstractStatsRename.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a1985c85aa9193699d7d20e6f4f11b6e9744ee70 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterAbstractStatsRename.java -@@ -0,0 +1,66 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.stats; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import java.util.ArrayList; -+import java.util.function.Function; -+ -+public final class ConverterAbstractStatsRename { -+ -+ private ConverterAbstractStatsRename() {} -+ -+ public static void register(final int version, final Function renamer) { -+ register(version, 0, renamer); -+ } -+ -+ public static void register(final int version, final int subVersion, final Function renamer) { -+ MCTypeRegistry.OBJECTIVE.addStructureConverter(new DataConverter<>(version, subVersion) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType criteriaType = data.getMap("CriteriaType"); -+ if (criteriaType == null) { -+ return null; -+ } -+ -+ final String type = criteriaType.getString("type"); -+ if (!"minecraft:custom".equals(type)) { -+ return null; -+ } -+ -+ final String id = criteriaType.getString("id"); -+ if (id == null) { -+ return null; -+ } -+ -+ final String rename = renamer.apply(id); -+ if (rename != null) { -+ criteriaType.setString("id", rename); -+ } -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.STATS.addStructureConverter(new DataConverter<>(version, subVersion) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType stats = data.getMap("stats"); -+ -+ if (stats == null) { -+ return null; -+ } -+ -+ final MapType custom = stats.getMap("minecraft:custom"); -+ if (custom == null) { -+ return null; -+ } -+ -+ RenameHelper.renameKeys(custom, renamer); -+ -+ return null; -+ } -+ }); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterFlattenStats.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterFlattenStats.java -new file mode 100644 -index 0000000000000000000000000000000000000000..891be75bf5c4af56e839c88b26f0a828554ae5c4 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/stats/ConverterFlattenStats.java -@@ -0,0 +1,321 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.stats; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.HelperBlockFlatteningV1450; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemstack.ConverterFlattenItemStack; -+import ca.spottedleaf.dataconverter.minecraft.versions.V1451; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.Types; -+import com.google.common.collect.ImmutableMap; -+import com.google.common.collect.ImmutableSet; -+import org.apache.commons.lang3.StringUtils; -+import java.util.HashMap; -+import java.util.HashSet; -+import java.util.Map; -+import java.util.Set; -+ -+public final class ConverterFlattenStats { -+ -+ private static final int VERSION = MCVersions.V17W47A; -+ private static final int VERSION_STEP = 6; -+ -+ private static final Set SPECIAL_OBJECTIVE_CRITERIA = new HashSet<>( -+ Set.of( -+ "dummy", -+ "trigger", -+ "deathCount", -+ "playerKillCount", -+ "totalKillCount", -+ "health", -+ "food", -+ "air", -+ "armor", -+ "xp", -+ "level", -+ "killedByTeam.aqua", -+ "killedByTeam.black", -+ "killedByTeam.blue", -+ "killedByTeam.dark_aqua", -+ "killedByTeam.dark_blue", -+ "killedByTeam.dark_gray", -+ "killedByTeam.dark_green", -+ "killedByTeam.dark_purple", -+ "killedByTeam.dark_red", -+ "killedByTeam.gold", -+ "killedByTeam.gray", -+ "killedByTeam.green", -+ "killedByTeam.light_purple", -+ "killedByTeam.red", -+ "killedByTeam.white", -+ "killedByTeam.yellow", -+ "teamkill.aqua", -+ "teamkill.black", -+ "teamkill.blue", -+ "teamkill.dark_aqua", -+ "teamkill.dark_blue", -+ "teamkill.dark_gray", -+ "teamkill.dark_green", -+ "teamkill.dark_purple", -+ "teamkill.dark_red", -+ "teamkill.gold", -+ "teamkill.gray", -+ "teamkill.green", -+ "teamkill.light_purple", -+ "teamkill.red", -+ "teamkill.white", -+ "teamkill.yellow" -+ ) -+ ); -+ -+ private static final Set SKIP = new HashSet<>( -+ ImmutableSet.builder() -+ .add("stat.craftItem.minecraft.spawn_egg") -+ .add("stat.useItem.minecraft.spawn_egg") -+ .add("stat.breakItem.minecraft.spawn_egg") -+ .add("stat.pickup.minecraft.spawn_egg") -+ .add("stat.drop.minecraft.spawn_egg") -+ .build() -+ ); -+ -+ private static final Map CUSTOM_MAP = new HashMap<>( -+ ImmutableMap.builder() -+ .put("stat.leaveGame", "minecraft:leave_game") -+ .put("stat.playOneMinute", "minecraft:play_one_minute") -+ .put("stat.timeSinceDeath", "minecraft:time_since_death") -+ .put("stat.sneakTime", "minecraft:sneak_time") -+ .put("stat.walkOneCm", "minecraft:walk_one_cm") -+ .put("stat.crouchOneCm", "minecraft:crouch_one_cm") -+ .put("stat.sprintOneCm", "minecraft:sprint_one_cm") -+ .put("stat.swimOneCm", "minecraft:swim_one_cm") -+ .put("stat.fallOneCm", "minecraft:fall_one_cm") -+ .put("stat.climbOneCm", "minecraft:climb_one_cm") -+ .put("stat.flyOneCm", "minecraft:fly_one_cm") -+ .put("stat.diveOneCm", "minecraft:dive_one_cm") -+ .put("stat.minecartOneCm", "minecraft:minecart_one_cm") -+ .put("stat.boatOneCm", "minecraft:boat_one_cm") -+ .put("stat.pigOneCm", "minecraft:pig_one_cm") -+ .put("stat.horseOneCm", "minecraft:horse_one_cm") -+ .put("stat.aviateOneCm", "minecraft:aviate_one_cm") -+ .put("stat.jump", "minecraft:jump") -+ .put("stat.drop", "minecraft:drop") -+ .put("stat.damageDealt", "minecraft:damage_dealt") -+ .put("stat.damageTaken", "minecraft:damage_taken") -+ .put("stat.deaths", "minecraft:deaths") -+ .put("stat.mobKills", "minecraft:mob_kills") -+ .put("stat.animalsBred", "minecraft:animals_bred") -+ .put("stat.playerKills", "minecraft:player_kills") -+ .put("stat.fishCaught", "minecraft:fish_caught") -+ .put("stat.talkedToVillager", "minecraft:talked_to_villager") -+ .put("stat.tradedWithVillager", "minecraft:traded_with_villager") -+ .put("stat.cakeSlicesEaten", "minecraft:eat_cake_slice") -+ .put("stat.cauldronFilled", "minecraft:fill_cauldron") -+ .put("stat.cauldronUsed", "minecraft:use_cauldron") -+ .put("stat.armorCleaned", "minecraft:clean_armor") -+ .put("stat.bannerCleaned", "minecraft:clean_banner") -+ .put("stat.brewingstandInteraction", "minecraft:interact_with_brewingstand") -+ .put("stat.beaconInteraction", "minecraft:interact_with_beacon") -+ .put("stat.dropperInspected", "minecraft:inspect_dropper") -+ .put("stat.hopperInspected", "minecraft:inspect_hopper") -+ .put("stat.dispenserInspected", "minecraft:inspect_dispenser") -+ .put("stat.noteblockPlayed", "minecraft:play_noteblock") -+ .put("stat.noteblockTuned", "minecraft:tune_noteblock") -+ .put("stat.flowerPotted", "minecraft:pot_flower") -+ .put("stat.trappedChestTriggered", "minecraft:trigger_trapped_chest") -+ .put("stat.enderchestOpened", "minecraft:open_enderchest") -+ .put("stat.itemEnchanted", "minecraft:enchant_item") -+ .put("stat.recordPlayed", "minecraft:play_record") -+ .put("stat.furnaceInteraction", "minecraft:interact_with_furnace") -+ .put("stat.craftingTableInteraction", "minecraft:interact_with_crafting_table") -+ .put("stat.chestOpened", "minecraft:open_chest") -+ .put("stat.sleepInBed", "minecraft:sleep_in_bed") -+ .put("stat.shulkerBoxOpened", "minecraft:open_shulker_box") -+ .build() -+ ); -+ -+ private static final String BLOCK_KEY = "stat.mineBlock"; -+ private static final String NEW_BLOCK_KEY = "minecraft:mined"; -+ -+ private static final Map ITEM_KEYS = new HashMap<>( -+ ImmutableMap.builder() -+ .put("stat.craftItem", "minecraft:crafted") -+ .put("stat.useItem", "minecraft:used") -+ .put("stat.breakItem", "minecraft:broken") -+ .put("stat.pickup", "minecraft:picked_up") -+ .put("stat.drop", "minecraft:dropped") -+ .build() -+ ); -+ -+ private static final Map ENTITY_KEYS = new HashMap<>( -+ ImmutableMap.builder() -+ .put("stat.entityKilledBy", "minecraft:killed_by") -+ .put("stat.killEntity", "minecraft:killed") -+ .build() -+ ); -+ -+ private static final Map ENTITIES = new HashMap<>( -+ ImmutableMap.builder() -+ .put("Bat", "minecraft:bat") -+ .put("Blaze", "minecraft:blaze") -+ .put("CaveSpider", "minecraft:cave_spider") -+ .put("Chicken", "minecraft:chicken") -+ .put("Cow", "minecraft:cow") -+ .put("Creeper", "minecraft:creeper") -+ .put("Donkey", "minecraft:donkey") -+ .put("ElderGuardian", "minecraft:elder_guardian") -+ .put("Enderman", "minecraft:enderman") -+ .put("Endermite", "minecraft:endermite") -+ .put("EvocationIllager", "minecraft:evocation_illager") -+ .put("Ghast", "minecraft:ghast") -+ .put("Guardian", "minecraft:guardian") -+ .put("Horse", "minecraft:horse") -+ .put("Husk", "minecraft:husk") -+ .put("Llama", "minecraft:llama") -+ .put("LavaSlime", "minecraft:magma_cube") -+ .put("MushroomCow", "minecraft:mooshroom") -+ .put("Mule", "minecraft:mule") -+ .put("Ozelot", "minecraft:ocelot") -+ .put("Parrot", "minecraft:parrot") -+ .put("Pig", "minecraft:pig") -+ .put("PolarBear", "minecraft:polar_bear") -+ .put("Rabbit", "minecraft:rabbit") -+ .put("Sheep", "minecraft:sheep") -+ .put("Shulker", "minecraft:shulker") -+ .put("Silverfish", "minecraft:silverfish") -+ .put("SkeletonHorse", "minecraft:skeleton_horse") -+ .put("Skeleton", "minecraft:skeleton") -+ .put("Slime", "minecraft:slime") -+ .put("Spider", "minecraft:spider") -+ .put("Squid", "minecraft:squid") -+ .put("Stray", "minecraft:stray") -+ .put("Vex", "minecraft:vex") -+ .put("Villager", "minecraft:villager") -+ .put("VindicationIllager", "minecraft:vindication_illager") -+ .put("Witch", "minecraft:witch") -+ .put("WitherSkeleton", "minecraft:wither_skeleton") -+ .put("Wolf", "minecraft:wolf") -+ .put("ZombieHorse", "minecraft:zombie_horse") -+ .put("PigZombie", "minecraft:zombie_pigman") -+ .put("ZombieVillager", "minecraft:zombie_villager") -+ .put("Zombie", "minecraft:zombie") -+ .build() -+ ); -+ -+ private static final String NEW_CUSTOM_KEY = "minecraft:custom"; -+ -+ private ConverterFlattenStats() {} -+ -+ private static String upgradeItem(final String itemName) { -+ return ConverterFlattenItemStack.flattenItem(itemName, 0); -+ } -+ -+ private static String upgradeBlock(final String block) { -+ return HelperBlockFlatteningV1450.getNewBlockName(block); -+ } -+ -+ private static record StatType(String category, String key) {} -+ -+ private static StatType convertLegacyKey(final String key) { -+ if (SKIP.contains(key)) { -+ return null; -+ } -+ -+ final String custom = CUSTOM_MAP.get(key); -+ if (custom != null) { -+ return new StatType(NEW_CUSTOM_KEY, custom); -+ } -+ -+ final int i = StringUtils.ordinalIndexOf(key, ".", 2); -+ if (i < 0) { -+ return null; -+ } -+ -+ final String stat = key.substring(0, i); -+ -+ if (BLOCK_KEY.equals(stat)) { -+ return new StatType(NEW_BLOCK_KEY, upgradeBlock(key.substring(i + 1).replace('.', ':'))); -+ } -+ -+ final String itemStat = ITEM_KEYS.get(stat); -+ -+ if (itemStat != null) { -+ final String itemId = key.substring(i + 1).replace('.', ':'); -+ final String flattenedItem = upgradeItem(itemId); -+ -+ return new StatType(itemStat, flattenedItem == null ? itemId : flattenedItem); -+ } -+ -+ final String entityStat = ENTITY_KEYS.get(stat); -+ if (entityStat != null) { -+ final String entityId = key.substring(i + 1).replace('.', ':'); -+ -+ return new StatType(entityStat, ENTITIES.getOrDefault(entityId, entityId)); -+ } -+ -+ return null; -+ } -+ -+ public static DataConverter, MapType> makeStatsConverter() { -+ return new DataConverter<>(VERSION, VERSION_STEP) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType stats = Types.NBT.createEmptyMap(); -+ -+ for (final String statKey : data.keys()) { -+ final Number value = data.getNumber(statKey); -+ if (value == null) { -+ continue; -+ } -+ -+ final StatType converted = convertLegacyKey(statKey); -+ -+ if (converted == null) { -+ continue; -+ } -+ -+ stats.getOrCreateMap(converted.category()).setGeneric(converted.key(), value); -+ } -+ -+ data.clear(); -+ data.setMap("stats", stats); -+ -+ return null; -+ } -+ }; -+ } -+ -+ public static DataConverter, MapType> makeObjectiveConverter() { -+ return new DataConverter<>(VERSION, VERSION_STEP) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ convertCriteriaName(data, "CriteriaName"); -+ -+ // We also need to update CriteriaType that is created by the data hook in V1451, -+ // otherwise that data hook will overwrite our CriteriaName -+ final MapType criteriaType = data.getMap("CriteriaType"); -+ if (criteriaType != null) { -+ if ("_special".equals(criteriaType.getString("type"))) { -+ convertCriteriaName(criteriaType, "id"); -+ } -+ } -+ -+ return null; -+ } -+ -+ private void convertCriteriaName(final MapType data, final String key) { -+ final String criteriaName = data.getString(key); -+ -+ if (criteriaName == null) { -+ return; -+ } -+ -+ if (SPECIAL_OBJECTIVE_CRITERIA.contains(criteriaName)) { -+ return; -+ } -+ -+ final StatType converted = convertLegacyKey(criteriaName); -+ data.setString(key, converted == null ? "dummy" : V1451.packWithDot(converted.category()) + ":" + V1451.packWithDot(converted.key())); -+ } -+ }; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/tileentity/ConverterAbstractTileEntityRename.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/tileentity/ConverterAbstractTileEntityRename.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ab05dda0cc2083418443d0dee23ccc0a6f754ea0 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/converters/tileentity/ConverterAbstractTileEntityRename.java -@@ -0,0 +1,34 @@ -+package ca.spottedleaf.dataconverter.minecraft.converters.tileentity; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import java.util.function.Function; -+ -+public final class ConverterAbstractTileEntityRename { -+ -+ public static void register(final int version, final Function renamer) { -+ register(version, 0, renamer); -+ } -+ -+ public static void register(final int version, final int subVersion, final Function renamer) { -+ MCTypeRegistry.TILE_ENTITY.addStructureConverter(new DataConverter<>(version, subVersion) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final String id = data.getString("id"); -+ if (id == null) { -+ return null; -+ } -+ -+ final String converted = renamer.apply(id); -+ -+ if (converted != null) { -+ data.setString("id", converted); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/DynamicDataType.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/DynamicDataType.java -new file mode 100644 -index 0000000000000000000000000000000000000000..dfa750bdaef7d7b6dadbc5665c1461f7e6df08ca ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/DynamicDataType.java -@@ -0,0 +1,128 @@ -+package ca.spottedleaf.dataconverter.minecraft.datatypes; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.converters.datatypes.DataHook; -+import ca.spottedleaf.dataconverter.converters.datatypes.DataType; -+import ca.spottedleaf.dataconverter.converters.datatypes.DataWalker; -+import ca.spottedleaf.dataconverter.minecraft.MCVersionRegistry; -+import ca.spottedleaf.dataconverter.util.Long2ObjectArraySortedMap; -+import java.util.ArrayList; -+import java.util.List; -+ -+public class DynamicDataType extends DataType { -+ -+ public final String name; -+ -+ protected final ArrayList> structureConverters = new ArrayList<>(); -+ protected final Long2ObjectArraySortedMap>> structureWalkers = new Long2ObjectArraySortedMap<>(); -+ protected final Long2ObjectArraySortedMap>> structureHooks = new Long2ObjectArraySortedMap<>(); -+ -+ public DynamicDataType(final String name) { -+ this.name = name; -+ } -+ -+ public void addStructureConverter(final DataConverter converter) { -+ MCVersionRegistry.checkVersion(converter.getEncodedVersion()); -+ this.structureConverters.add(converter); -+ this.structureConverters.sort(DataConverter.LOWEST_VERSION_COMPARATOR); -+ } -+ -+ public void addStructureWalker(final int minVersion, final DataWalker walker) { -+ this.addStructureWalker(minVersion, 0, walker); -+ } -+ -+ public void addStructureWalker(final int minVersion, final int versionStep, final DataWalker walker) { -+ this.structureWalkers.computeIfAbsent(DataConverter.encodeVersions(minVersion, versionStep), (final long keyInMap) -> { -+ return new ArrayList<>(); -+ }).add(walker); -+ } -+ -+ public void addStructureHook(final int minVersion, final DataHook hook) { -+ this.addStructureHook(minVersion, 0, hook); -+ } -+ -+ public void addStructureHook(final int minVersion, final int versionStep, final DataHook hook) { -+ this.structureHooks.computeIfAbsent(DataConverter.encodeVersions(minVersion, versionStep), (final long keyInMap) -> { -+ return new ArrayList<>(); -+ }).add(hook); -+ } -+ -+ @Override -+ public Object convert(Object data, final long fromVersion, final long toVersion) { -+ Object ret = null; -+ -+ final List> converters = this.structureConverters; -+ for (int i = 0, len = converters.size(); i < len; ++i) { -+ final DataConverter converter = converters.get(i); -+ final long converterVersion = converter.getEncodedVersion(); -+ -+ if (converterVersion <= fromVersion) { -+ continue; -+ } -+ -+ if (converterVersion > toVersion) { -+ break; -+ } -+ -+ List> hooks = this.structureHooks.getFloor(converterVersion); -+ -+ if (hooks != null) { -+ for (int k = 0, klen = hooks.size(); k < klen; ++k) { -+ final Object replace = hooks.get(k).preHook(data, fromVersion, toVersion); -+ if (replace != null) { -+ ret = data = replace; -+ } -+ } -+ } -+ -+ final Object replace = converter.convert(data, fromVersion, toVersion); -+ if (replace != null) { -+ ret = data = replace; -+ } -+ -+ // possibly new data format, update hooks -+ hooks = this.structureHooks.getFloor(toVersion); -+ -+ if (hooks != null) { -+ for (int klen = hooks.size(), k = klen - 1; k >= 0; --k) { -+ final Object postReplace = hooks.get(k).postHook(data, fromVersion, toVersion); -+ if (postReplace != null) { -+ ret = data = postReplace; -+ } -+ } -+ } -+ } -+ -+ final List> hooks = this.structureHooks.getFloor(toVersion); -+ -+ if (hooks != null) { -+ for (int k = 0, klen = hooks.size(); k < klen; ++k) { -+ final Object replace = hooks.get(k).preHook(data, fromVersion, toVersion); -+ if (replace != null) { -+ ret = data = replace; -+ } -+ } -+ } -+ -+ final List> walkers = this.structureWalkers.getFloor(toVersion); -+ if (walkers != null) { -+ for (int i = 0, len = walkers.size(); i < len; ++i) { -+ final Object replace = walkers.get(i).walk(data, fromVersion, toVersion); -+ if (replace != null) { -+ ret = data = replace; -+ } -+ } -+ } -+ -+ if (hooks != null) { -+ for (int klen = hooks.size(), k = klen - 1; k >= 0; --k) { -+ final Object postReplace = hooks.get(k).postHook(data, fromVersion, toVersion); -+ if (postReplace != null) { -+ ret = data = postReplace; -+ } -+ } -+ } -+ -+ return ret; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/IDDataType.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/IDDataType.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b093a9eeeea3f7c1c220485b7144d22c6fd504a0 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/IDDataType.java -@@ -0,0 +1,166 @@ -+package ca.spottedleaf.dataconverter.minecraft.datatypes; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.converters.datatypes.DataHook; -+import ca.spottedleaf.dataconverter.converters.datatypes.DataWalker; -+import ca.spottedleaf.dataconverter.minecraft.MCVersionRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.util.Long2ObjectArraySortedMap; -+ -+import java.util.ArrayList; -+import java.util.HashMap; -+import java.util.List; -+import java.util.Map; -+ -+public class IDDataType extends MCDataType { -+ -+ protected final Map>>>> walkersById = new HashMap<>(); -+ -+ public IDDataType(final String name) { -+ super(name); -+ } -+ -+ public void addConverterForId(final String id, final DataConverter, MapType> converter) { -+ this.addStructureConverter(new DataConverter<>(converter.getToVersion(), converter.getVersionStep()) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (!id.equals(data.getString("id"))) { -+ return null; -+ } -+ return converter.convert(data, sourceVersion, toVersion); -+ } -+ }); -+ } -+ -+ public void addWalker(final int minVersion, final String id, final DataWalker> walker) { -+ this.addWalker(minVersion, 0, id, walker); -+ } -+ -+ public void addWalker(final int minVersion, final int versionStep, final String id, final DataWalker> walker) { -+ this.walkersById.computeIfAbsent(id, (final String keyInMap) -> { -+ return new Long2ObjectArraySortedMap<>(); -+ }).computeIfAbsent(DataConverter.encodeVersions(minVersion, versionStep), (final long keyInMap) -> { -+ return new ArrayList<>(); -+ }).add(walker); -+ } -+ -+ public void copyWalkers(final int minVersion, final String fromId, final String toId) { -+ this.copyWalkers(minVersion, 0, fromId, toId); -+ } -+ -+ public void copyWalkers(final int minVersion, final int versionStep, final String fromId, final String toId) { -+ final long version = DataConverter.encodeVersions(minVersion, versionStep); -+ final Long2ObjectArraySortedMap>>> walkersForId = this.walkersById.get(fromId); -+ if (walkersForId == null) { -+ return; -+ } -+ -+ final List>> nearest = walkersForId.getFloor(version); -+ -+ if (nearest == null) { -+ return; -+ } -+ -+ for (final DataWalker> walker : nearest) { -+ this.addWalker(minVersion, versionStep, toId, walker); -+ } -+ } -+ -+ @Override -+ public MapType convert(MapType data, final long fromVersion, final long toVersion) { -+ MapType ret = null; -+ -+ final List, MapType>> converters = this.structureConverters; -+ for (int i = 0, len = converters.size(); i < len; ++i) { -+ final DataConverter, MapType> converter = converters.get(i); -+ final long converterVersion = converter.getEncodedVersion(); -+ -+ if (converterVersion <= fromVersion) { -+ continue; -+ } -+ -+ if (converterVersion > toVersion) { -+ break; -+ } -+ -+ List, MapType>> hooks = this.structureHooks.getFloor(converterVersion); -+ -+ if (hooks != null) { -+ for (int k = 0, klen = hooks.size(); k < klen; ++k) { -+ final MapType replace = hooks.get(k).preHook(data, fromVersion, toVersion); -+ if (replace != null) { -+ ret = data = replace; -+ } -+ } -+ } -+ -+ final MapType replace = converter.convert(data, fromVersion, toVersion); -+ if (replace != null) { -+ ret = data = replace; -+ } -+ -+ // possibly new data format, update hooks -+ hooks = this.structureHooks.getFloor(toVersion); -+ -+ if (hooks != null) { -+ for (int klen = hooks.size(), k = klen - 1; k >= 0; --k) { -+ final MapType postReplace = hooks.get(k).postHook(data, fromVersion, toVersion); -+ if (postReplace != null) { -+ ret = data = postReplace; -+ } -+ } -+ } -+ } -+ -+ final List, MapType>> hooks = this.structureHooks.getFloor(toVersion); -+ -+ // run pre hooks -+ -+ if (hooks != null) { -+ for (int k = 0, klen = hooks.size(); k < klen; ++k) { -+ final MapType replace = hooks.get(k).preHook(data, fromVersion, toVersion); -+ if (replace != null) { -+ ret = data = replace; -+ } -+ } -+ } -+ -+ // run all walkers -+ -+ final List>> walkers = this.structureWalkers.getFloor(toVersion); -+ if (walkers != null) { -+ for (int i = 0, len = walkers.size(); i < len; ++i) { -+ final MapType replace = walkers.get(i).walk(data, fromVersion, toVersion); -+ if (replace != null) { -+ ret = data = replace; -+ } -+ } -+ } -+ -+ final Long2ObjectArraySortedMap>>> walkersByVersion = this.walkersById.get(data.getString("id")); -+ if (walkersByVersion != null) { -+ final List>> walkersForId = walkersByVersion.getFloor(toVersion); -+ if (walkersForId != null) { -+ for (int i = 0, len = walkersForId.size(); i < len; ++i) { -+ final MapType replace = walkersForId.get(i).walk(data, fromVersion, toVersion); -+ if (replace != null) { -+ ret = data = replace; -+ } -+ } -+ } -+ } -+ -+ // run post hooks -+ -+ if (hooks != null) { -+ for (int klen = hooks.size(), k = klen - 1; k >= 0; --k) { -+ final MapType postReplace = hooks.get(k).postHook(data, fromVersion, toVersion); -+ if (postReplace != null) { -+ ret = data = postReplace; -+ } -+ } -+ } -+ -+ return ret; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCDataType.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCDataType.java -new file mode 100644 -index 0000000000000000000000000000000000000000..075574f33476882ddc6787e3b8bac8643a414eb0 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCDataType.java -@@ -0,0 +1,129 @@ -+package ca.spottedleaf.dataconverter.minecraft.datatypes; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.converters.datatypes.DataHook; -+import ca.spottedleaf.dataconverter.converters.datatypes.DataType; -+import ca.spottedleaf.dataconverter.converters.datatypes.DataWalker; -+import ca.spottedleaf.dataconverter.minecraft.MCVersionRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.util.Long2ObjectArraySortedMap; -+import java.util.ArrayList; -+import java.util.List; -+ -+public class MCDataType extends DataType, MapType> { -+ -+ public final String name; -+ -+ protected final ArrayList, MapType>> structureConverters = new ArrayList<>(); -+ protected final Long2ObjectArraySortedMap>>> structureWalkers = new Long2ObjectArraySortedMap<>(); -+ protected final Long2ObjectArraySortedMap, MapType>>> structureHooks = new Long2ObjectArraySortedMap<>(); -+ -+ public MCDataType(final String name) { -+ this.name = name; -+ } -+ -+ public void addStructureConverter(final DataConverter, MapType> converter) { -+ MCVersionRegistry.checkVersion(converter.getEncodedVersion()); -+ this.structureConverters.add(converter); -+ this.structureConverters.sort(DataConverter.LOWEST_VERSION_COMPARATOR); -+ } -+ -+ public void addStructureWalker(final int minVersion, final DataWalker> walker) { -+ this.addStructureWalker(minVersion, 0, walker); -+ } -+ -+ public void addStructureWalker(final int minVersion, final int versionStep, final DataWalker> walker) { -+ this.structureWalkers.computeIfAbsent(DataConverter.encodeVersions(minVersion, versionStep), (final long keyInMap) -> { -+ return new ArrayList<>(); -+ }).add(walker); -+ } -+ -+ public void addStructureHook(final int minVersion, final DataHook, MapType> hook) { -+ this.addStructureHook(minVersion, 0, hook); -+ } -+ -+ public void addStructureHook(final int minVersion, final int versionStep, final DataHook, MapType> hook) { -+ this.structureHooks.computeIfAbsent(DataConverter.encodeVersions(minVersion, versionStep), (final long keyInMap) -> { -+ return new ArrayList<>(); -+ }).add(hook); -+ } -+ -+ @Override -+ public MapType convert(MapType data, final long fromVersion, final long toVersion) { -+ MapType ret = null; -+ -+ final List, MapType>> converters = this.structureConverters; -+ for (int i = 0, len = converters.size(); i < len; ++i) { -+ final DataConverter, MapType> converter = converters.get(i); -+ final long converterVersion = converter.getEncodedVersion(); -+ -+ if (converterVersion <= fromVersion) { -+ continue; -+ } -+ -+ if (converterVersion > toVersion) { -+ break; -+ } -+ -+ List, MapType>> hooks = this.structureHooks.getFloor(converterVersion); -+ -+ if (hooks != null) { -+ for (int k = 0, klen = hooks.size(); k < klen; ++k) { -+ final MapType replace = hooks.get(k).preHook(data, fromVersion, toVersion); -+ if (replace != null) { -+ ret = data = replace; -+ } -+ } -+ } -+ -+ final MapType replace = converter.convert(data, fromVersion, toVersion); -+ if (replace != null) { -+ ret = data = replace; -+ } -+ -+ // possibly new data format, update hooks -+ hooks = this.structureHooks.getFloor(toVersion); -+ -+ if (hooks != null) { -+ for (int klen = hooks.size(), k = klen - 1; k >= 0; --k) { -+ final MapType postReplace = hooks.get(k).postHook(data, fromVersion, toVersion); -+ if (postReplace != null) { -+ ret = data = postReplace; -+ } -+ } -+ } -+ } -+ -+ final List, MapType>> hooks = this.structureHooks.getFloor(toVersion); -+ -+ if (hooks != null) { -+ for (int k = 0, klen = hooks.size(); k < klen; ++k) { -+ final MapType replace = hooks.get(k).preHook(data, fromVersion, toVersion); -+ if (replace != null) { -+ ret = data = replace; -+ } -+ } -+ } -+ -+ final List>> walkers = this.structureWalkers.getFloor(toVersion); -+ if (walkers != null) { -+ for (int i = 0, len = walkers.size(); i < len; ++i) { -+ final MapType replace = walkers.get(i).walk(data, fromVersion, toVersion); -+ if (replace != null) { -+ ret = data = replace; -+ } -+ } -+ } -+ -+ if (hooks != null) { -+ for (int klen = hooks.size(), k = klen - 1; k >= 0; --k) { -+ final MapType postReplace = hooks.get(k).postHook(data, fromVersion, toVersion); -+ if (postReplace != null) { -+ ret = data = postReplace; -+ } -+ } -+ } -+ -+ return ret; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCTypeRegistry.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCTypeRegistry.java -new file mode 100644 -index 0000000000000000000000000000000000000000..37519f2aab94c299f68819d1b2df2caffb75c9cd ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCTypeRegistry.java -@@ -0,0 +1,298 @@ -+package ca.spottedleaf.dataconverter.minecraft.datatypes; -+ -+import ca.spottedleaf.dataconverter.minecraft.versions.*; -+import com.mojang.logging.LogUtils; -+import org.slf4j.Logger; -+ -+public final class MCTypeRegistry { -+ -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ -+ public static final MCDataType LEVEL = new MCDataType("Level"); -+ public static final MCDataType PLAYER = new MCDataType("Player"); -+ public static final MCDataType CHUNK = new MCDataType("Chunk"); -+ public static final MCDataType HOTBAR = new MCDataType("CreativeHotbar"); -+ public static final MCDataType OPTIONS = new MCDataType("Options"); -+ public static final MCDataType STRUCTURE = new MCDataType("Structure"); -+ public static final MCDataType STATS = new MCDataType("Stats"); -+ public static final MCDataType ADVANCEMENTS = new MCDataType("Advancements"); -+ public static final MCDataType POI_CHUNK = new MCDataType("PoiChunk"); -+ public static final MCDataType ENTITY_CHUNK = new MCDataType("EntityChunk"); -+ public static final IDDataType TILE_ENTITY = new IDDataType("TileEntity"); -+ public static final IDDataType ITEM_STACK = new IDDataType("ItemStack"); -+ public static final MCDataType BLOCK_STATE = new MCDataType("BlockState"); -+ public static final MCValueType FLAT_BLOCK_STATE = new MCValueType("FlatBlockState"); -+ public static final MCDataType DATA_COMPONENTS = new MCDataType("DataComponents"); -+ public static final MCDataType VILLAGER_TRADE = new MCDataType("VillagerTrade"); -+ public static final DynamicDataType PARTICLE = new DynamicDataType("Particle"); -+ public static final MCValueType ENTITY_NAME = new MCValueType("EntityName"); -+ public static final IDDataType ENTITY = new IDDataType("Entity"); -+ public static final MCValueType BLOCK_NAME = new MCValueType("BlockName"); -+ public static final MCValueType ITEM_NAME = new MCValueType("ItemName"); -+ public static final MCDataType UNTAGGED_SPAWNER = new MCDataType("Spawner"); -+ public static final MCDataType STRUCTURE_FEATURE = new MCDataType("StructureFeature"); -+ public static final MCDataType OBJECTIVE = new MCDataType("Objective"); -+ public static final MCDataType TEAM = new MCDataType("Team"); -+ public static final MCValueType RECIPE = new MCValueType("RecipeName"); -+ public static final MCValueType BIOME = new MCValueType("Biome"); -+ public static final MCDataType WORLD_GEN_SETTINGS = new MCDataType("WorldGenSettings"); -+ public static final MCValueType GAME_EVENT_NAME = new MCValueType("GameEventName"); -+ -+ public static final MCValueType MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST = new MCValueType("MultiNoiseBiomeSourceParameterList"); -+ -+ public static final MCDataType SAVED_DATA_RANDOM_SEQUENCES = new MCDataType("SavedData/RandomSequences"); -+ public static final MCDataType SAVED_DATA_SCOREBOARD = new MCDataType("SavedData/Scoreboard"); -+ public static final MCDataType SAVED_DATA_STRUCTURE_FEATURE_INDICES = new MCDataType("SavedData/StructureFeatureIndices"); -+ public static final MCDataType SAVED_DATA_MAP_DATA = new MCDataType("SavedData/MapData"); -+ public static final MCDataType SAVED_DATA_RAIDS = new MCDataType("SavedData/Raids"); -+ public static final MCDataType SAVED_DATA_COMMAND_STORAGE = new MCDataType("SavedData/CommandStorage"); -+ public static final MCDataType SAVED_DATA_FORCED_CHUNKS = new MCDataType("SavedData/Chunks"); -+ public static final MCDataType SAVED_DATA_MAP_INDEX = new MCDataType("SavedData/IdCounts"); -+ -+ public static final MCValueType DATACONVERTER_CUSTOM_TYPE_COMMAND = new MCValueType("DC_Custom/Command"); -+ -+ static { -+ try { -+ registerAll(); -+ } catch (final Throwable thr) { -+ LOGGER.error(LogUtils.FATAL_MARKER, "Failed to register data converters", thr); -+ throw new RuntimeException(thr); -+ } -+ } -+ -+ private static void registerAll() { -+ // General notes: -+ // - Structure converters run before everything. -+ // - ID specific converters run after structure converters. -+ // - Structure walkers run after id specific converters. -+ // - ID specific walkers run after structure walkers. -+ -+ V99.register(); // all legacy data before converters existed -+ V100.register(); // first version with version id -+ V101.register(); -+ V102.register(); -+ V105.register(); -+ V106.register(); -+ V107.register(); -+ V108.register(); -+ V109.register(); -+ V110.register(); -+ V111.register(); -+ V113.register(); -+ V135.register(); -+ V143.register(); -+ V147.register(); -+ V165.register(); -+ V501.register(); -+ V502.register(); -+ V505.register(); -+ V700.register(); -+ V701.register(); -+ V702.register(); -+ V703.register(); -+ V704.register(); -+ V705.register(); -+ V804.register(); -+ V806.register(); -+ V808.register(); -+ V813.register(); -+ V816.register(); -+ V820.register(); -+ V1022.register(); -+ V1125.register(); -+ // END OF LEGACY DATA CONVERTERS -+ -+ // V1.13 -+ V1344.register(); -+ V1446.register(); -+ // START THE FLATTENING -+ V1450.register(); -+ V1451.register(); -+ // END THE FLATTENING -+ -+ V1456.register(); -+ V1458.register(); -+ V1460.register(); -+ V1466.register(); -+ V1470.register(); -+ V1474.register(); -+ V1475.register(); -+ V1480.register(); -+ // V1481 is adding simple block entity -+ V1483.register(); -+ V1484.register(); -+ V1486.register(); -+ V1487.register(); -+ V1488.register(); -+ V1490.register(); -+ V1492.register(); -+ V1494.register(); -+ V1496.register(); -+ V1500.register(); -+ V1501.register(); -+ V1502.register(); -+ V1506.register(); -+ V1510.register(); -+ V1514.register(); -+ V1515.register(); -+ V1624.register(); -+ // V1.14 -+ V1800.register(); -+ V1801.register(); -+ V1802.register(); -+ V1803.register(); -+ V1904.register(); -+ V1905.register(); -+ V1906.register(); -+ V1909.register(); -+ V1911.register(); -+ V1914.register(); -+ V1917.register(); -+ V1918.register(); -+ V1920.register(); -+ V1925.register(); -+ V1928.register(); -+ V1929.register(); -+ V1931.register(); -+ V1936.register(); -+ V1946.register(); -+ V1948.register(); -+ V1953.register(); -+ V1955.register(); -+ V1961.register(); -+ V1963.register(); -+ // V1.15 -+ V2100.register(); -+ V2202.register(); -+ V2209.register(); -+ V2211.register(); -+ V2218.register(); -+ // V1.16 -+ V2501.register(); -+ V2502.register(); -+ V2503.register(); -+ V2505.register(); -+ V2508.register(); -+ V2509.register(); -+ V2511.register(); -+ V2514.register(); -+ V2516.register(); -+ V2518.register(); -+ V2519.register(); -+ V2522.register(); -+ V2523.register(); -+ V2527.register(); -+ V2528.register(); -+ V2529.register(); -+ V2531.register(); -+ V2533.register(); -+ V2535.register(); -+ V2538.register(); -+ V2550.register(); -+ V2551.register(); -+ V2552.register(); -+ V2553.register(); -+ V2558.register(); -+ V2568.register(); -+ // V1.17 -+ // WARN: Mojang registers V2671 under 2571, but that version predates 1.16.5? So it looks like a typo... -+ // I changed it to 2671, just so that it's after 1.16.5, but even then this looks misplaced... Thankfully this is -+ // the first datafixer, and all it does is add a walker, so I think even if the version here is just wrong it will -+ // work. -+ V2671.register(); -+ V2679.register(); -+ V2680.register(); -+ V2684.register(); -+ V2686.register(); -+ V2688.register(); -+ V2690.register(); -+ V2691.register(); -+ V2693.register(); -+ V2696.register(); -+ V2700.register(); -+ V2701.register(); -+ V2702.register(); -+ // In reference to V2671, why the fuck is goat being registered again? For this obvious reason, V2704 is absent. -+ V2707.register(); -+ V2710.register(); -+ V2717.register(); -+ // V1.18 -+ V2825.register(); -+ V2831.register(); -+ V2832.register(); -+ V2833.register(); -+ V2838.register(); -+ V2841.register(); -+ V2842.register(); -+ V2843.register(); -+ V2846.register(); -+ V2852.register(); -+ V2967.register(); -+ V2970.register(); -+ // V1.19 -+ // V3076 is registering a simple tile entity (sculk_catalyst) -+ V3077.register(); -+ V3078.register(); -+ V3081.register(); -+ V3082.register(); -+ V3083.register(); -+ V3084.register(); -+ V3086.register(); -+ V3087.register(); -+ V3088.register(); -+ V3090.register(); -+ V3093.register(); -+ V3094.register(); -+ V3097.register(); -+ V3108.register(); -+ V3201.register(); -+ // V3202 registers a simple tile entity -+ V3203.register(); -+ V3204.register(); -+ V3209.register(); -+ V3214.register(); -+ V3319.register(); -+ V3322.register(); -+ V3325.register(); -+ V3326.register(); -+ V3327.register(); -+ V3328.register(); -+ // V1.20 -+ V3438.register(); -+ V3439.register(); -+ V3440.register(); -+ V3441.register(); -+ V3447.register(); -+ V3448.register(); -+ V3450.register(); -+ V3451.register(); -+ V3459.register(); -+ V3564.register(); -+ V3565.register(); -+ V3566.register(); -+ V3568.register(); -+ V3682.register(); -+ V3683.register(); -+ V3685.register(); -+ V3689.register(); -+ V3692.register(); -+ // V1.20.5 -+ V3799.register(); -+ V3800.register(); -+ V3803.register(); -+ V3807.register(); -+ V3808.register(); -+ V3809.register(); -+ V3812.register(); -+ V3813.register(); -+ V3814.register(); -+ V3816.register(); -+ V3818.register(); -+ V3820.register(); -+ V3825.register(); -+ V3828.register(); -+ V3833.register(); -+ } -+ -+ private MCTypeRegistry() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCValueType.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCValueType.java -new file mode 100644 -index 0000000000000000000000000000000000000000..13c1381261909ef672fbeb665907f01f2d5c1ced ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/datatypes/MCValueType.java -@@ -0,0 +1,86 @@ -+package ca.spottedleaf.dataconverter.minecraft.datatypes; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.converters.datatypes.DataHook; -+import ca.spottedleaf.dataconverter.converters.datatypes.DataType; -+import ca.spottedleaf.dataconverter.minecraft.MCVersionRegistry; -+import ca.spottedleaf.dataconverter.util.Long2ObjectArraySortedMap; -+import java.util.ArrayList; -+import java.util.List; -+ -+public class MCValueType extends DataType { -+ -+ public final String name; -+ -+ protected final ArrayList> converters = new ArrayList<>(); -+ protected final Long2ObjectArraySortedMap>> structureHooks = new Long2ObjectArraySortedMap<>(); -+ -+ public MCValueType(final String name) { -+ this.name = name; -+ } -+ -+ public void addStructureHook(final int minVersion, final DataHook hook) { -+ this.addStructureHook(minVersion, 0, hook); -+ } -+ -+ public void addStructureHook(final int minVersion, final int versionStep, final DataHook hook) { -+ this.structureHooks.computeIfAbsent(DataConverter.encodeVersions(minVersion, versionStep), (final long keyInMap) -> { -+ return new ArrayList<>(); -+ }).add(hook); -+ } -+ -+ public void addConverter(final DataConverter converter) { -+ MCVersionRegistry.checkVersion(converter.getEncodedVersion()); -+ this.converters.add(converter); -+ this.converters.sort(DataConverter.LOWEST_VERSION_COMPARATOR); -+ } -+ -+ @Override -+ public Object convert(final Object data, final long fromVersion, final long toVersion) { -+ Object ret = null; -+ final List> converters = this.converters; -+ -+ for (int i = 0, len = converters.size(); i < len; ++i) { -+ final DataConverter converter = converters.get(i); -+ final long converterVersion = converter.getEncodedVersion(); -+ -+ if (converterVersion <= fromVersion) { -+ continue; -+ } -+ -+ if (converterVersion > toVersion) { -+ break; -+ } -+ -+ List> hooks = this.structureHooks.getFloor(converterVersion); -+ -+ if (hooks != null) { -+ for (int k = 0, klen = hooks.size(); k < klen; ++k) { -+ final Object replace = hooks.get(k).preHook(ret == null ? data : ret, fromVersion, toVersion); -+ if (replace != null) { -+ ret = replace; -+ } -+ } -+ } -+ -+ final Object converted = converter.convert(ret == null ? data : ret, fromVersion, toVersion); -+ if (converted != null) { -+ ret = converted; -+ } -+ -+ // possibly new data format, update hooks -+ hooks = this.structureHooks.getFloor(toVersion); -+ -+ if (hooks != null) { -+ for (int k = 0, klen = hooks.size(); k < klen; ++k) { -+ final Object replace = hooks.get(k).postHook(ret == null ? data : ret, fromVersion, toVersion); -+ if (replace != null) { -+ ret = replace; -+ } -+ } -+ } -+ } -+ -+ return ret; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookEnforceNamespacedID.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookEnforceNamespacedID.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f7dced8a47ebdd262ae815ff9bc453312343ce49 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookEnforceNamespacedID.java -@@ -0,0 +1,29 @@ -+package ca.spottedleaf.dataconverter.minecraft.hooks; -+ -+import ca.spottedleaf.dataconverter.converters.datatypes.DataHook; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.util.NamespaceUtil; -+ -+public class DataHookEnforceNamespacedID implements DataHook, MapType> { -+ -+ private final String path; -+ -+ public DataHookEnforceNamespacedID() { -+ this("id"); -+ } -+ -+ public DataHookEnforceNamespacedID(final String path) { -+ this.path = path; -+ } -+ -+ @Override -+ public MapType preHook(final MapType data, final long fromVersion, final long toVersion) { -+ NamespaceUtil.enforceForPath(data, this.path); -+ return null; -+ } -+ -+ @Override -+ public MapType postHook(final MapType data, final long fromVersion, final long toVersion) { -+ return null; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookValueTypeEnforceNamespaced.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookValueTypeEnforceNamespaced.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7f88487e7db589070512fafef1eb243ae29a379a ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/hooks/DataHookValueTypeEnforceNamespaced.java -@@ -0,0 +1,20 @@ -+package ca.spottedleaf.dataconverter.minecraft.hooks; -+ -+import ca.spottedleaf.dataconverter.converters.datatypes.DataHook; -+import ca.spottedleaf.dataconverter.util.NamespaceUtil; -+ -+public class DataHookValueTypeEnforceNamespaced implements DataHook { -+ -+ @Override -+ public Object preHook(final Object data, final long fromVersion, final long toVersion) { -+ if (data instanceof String) { -+ return NamespaceUtil.correctNamespaceOrNull((String)data); -+ } -+ return null; -+ } -+ -+ @Override -+ public Object postHook(final Object data, final long fromVersion, final long toVersion) { -+ return null; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/util/ComponentUtils.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/util/ComponentUtils.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7f8f9d002b286f166d6e59bbf77aff74af10731c ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/util/ComponentUtils.java -@@ -0,0 +1,78 @@ -+package ca.spottedleaf.dataconverter.minecraft.util; -+ -+import com.google.gson.JsonElement; -+import com.google.gson.JsonObject; -+import com.google.gson.JsonParseException; -+import com.google.gson.JsonParser; -+import com.google.gson.JsonPrimitive; -+import net.minecraft.util.GsonHelper; -+ -+public final class ComponentUtils { -+ -+ public static final String EMPTY = createPlainTextComponent(""); -+ -+ public static String createPlainTextComponent(final String text) { -+ final JsonObject ret = new JsonObject(); -+ -+ ret.addProperty("text", text); -+ -+ return GsonHelper.toStableString(ret); -+ } -+ -+ public static String createTranslatableComponent(final String key) { -+ final JsonObject ret = new JsonObject(); -+ -+ ret.addProperty("translate", key); -+ -+ return GsonHelper.toStableString(ret); -+ } -+ -+ public static String retrieveTranslationString(final String possibleJson) { -+ try { -+ final JsonElement element = JsonParser.parseString(possibleJson); -+ -+ if (element instanceof JsonObject object) { -+ final JsonElement translation = object.get("translate"); -+ if (translation instanceof JsonPrimitive primitive) { -+ return primitive.getAsString(); -+ } -+ } -+ -+ return null; -+ } catch (final Exception ex) { -+ return null; -+ } -+ } -+ -+ public static String convertFromLenient(final String input) { -+ if (input == null) { -+ return input; -+ } -+ -+ if (input.isEmpty() || input.equals("null")) { -+ return EMPTY; -+ } -+ -+ final char firstCharacter = input.charAt(0); -+ final char lastCharacter = input.charAt(input.length() - 1); -+ if ((firstCharacter == '"' && lastCharacter == '"') -+ || (firstCharacter == '{' && lastCharacter == '}') -+ || (firstCharacter == '[' && lastCharacter == ']')) { -+ try { -+ final JsonElement json = JsonParser.parseString(input); -+ -+ if (json.isJsonPrimitive()) { -+ return createPlainTextComponent(json.getAsString()); -+ } -+ -+ return GsonHelper.toStableString(json); -+ } catch (final JsonParseException ignored) { -+ // fall through to plain text -+ } -+ } -+ -+ return createPlainTextComponent(input); -+ } -+ -+ private ComponentUtils() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V100.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V100.java -new file mode 100644 -index 0000000000000000000000000000000000000000..91b1d0be9d697a4fa8bc5b448b329df1f5deabc4 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V100.java -@@ -0,0 +1,161 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.block_name.DataWalkerBlockNames; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.Types; -+ -+public final class V100 { -+ -+ private static final int VERSION = MCVersions.V15W32A; -+ -+ static void registerEquipment(final int version, final String id) { -+ registerEquipment(version, 0, id); -+ } -+ -+ private static final DataWalkerItemLists EQUIPMENT_ITEM_LISTS = new DataWalkerItemLists("ArmorItems", "HandItems"); -+ private static final DataWalkerItems EQUIPMENT_ITEMS = new DataWalkerItems("body_armor_item"); -+ -+ static void registerEquipment(final int version, final int versionStep, final String id) { -+ MCTypeRegistry.ENTITY.addWalker(version, versionStep, id, EQUIPMENT_ITEM_LISTS); -+ MCTypeRegistry.ENTITY.addWalker(version, versionStep, id, EQUIPMENT_ITEMS); -+ } -+ -+ private static void registerMob(final String id) { -+ registerEquipment(VERSION, 0, id); -+ } -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final ListType equipment = data.getList("Equipment", ObjectType.MAP); -+ data.remove("Equipment"); -+ -+ if (equipment != null) { -+ if (equipment.size() > 0 && data.getListUnchecked("HandItems") == null) { -+ final ListType handItems = Types.NBT.createEmptyList(); -+ data.setList("HandItems", handItems); -+ handItems.addMap(equipment.getMap(0)); -+ handItems.addMap(Types.NBT.createEmptyMap()); -+ } -+ -+ if (equipment.size() > 1 && data.getListUnchecked("ArmorItems") == null) { -+ final ListType armorItems = Types.NBT.createEmptyList(); -+ data.setList("ArmorItems", armorItems); -+ for (int i = 1; i < Math.min(equipment.size(), 5); ++i) { -+ armorItems.addMap(equipment.getMap(i)); -+ } -+ } -+ } -+ -+ final ListType dropChances = data.getList("DropChances", ObjectType.FLOAT); -+ data.remove("DropChances"); -+ -+ if (dropChances != null) { -+ if (data.getListUnchecked("HandDropChances") == null) { -+ final ListType handDropChances = Types.NBT.createEmptyList(); -+ data.setList("HandDropChances", handDropChances); -+ if (0 < dropChances.size()) { -+ handDropChances.addFloat(dropChances.getFloat(0)); -+ } else { -+ handDropChances.addFloat(0.0F); -+ } -+ handDropChances.addFloat(0.0F); -+ } -+ -+ if (data.getListUnchecked("ArmorDropChances") == null) { -+ final ListType armorDropChances = Types.NBT.createEmptyList(); -+ data.setList("ArmorDropChances", armorDropChances); -+ for (int i = 1; i < 5; ++i) { -+ if (i < dropChances.size()) { -+ armorDropChances.addFloat(dropChances.getFloat(i)); -+ } else { -+ armorDropChances.addFloat(0.0F); -+ } -+ } -+ } -+ } -+ -+ return null; -+ } -+ }); -+ -+ registerMob("ArmorStand"); -+ registerMob("Creeper"); -+ registerMob("Skeleton"); -+ registerMob("Spider"); -+ registerMob("Giant"); -+ registerMob("Zombie"); -+ registerMob("Slime"); -+ registerMob("Ghast"); -+ registerMob("PigZombie"); -+ registerMob("Enderman"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "Enderman", new DataWalkerBlockNames("carried")); -+ registerMob("CaveSpider"); -+ registerMob("Silverfish"); -+ registerMob("Blaze"); -+ registerMob("LavaSlime"); -+ registerMob("EnderDragon"); -+ registerMob("WitherBoss"); -+ registerMob("Bat"); -+ registerMob("Witch"); -+ registerMob("Endermite"); -+ registerMob("Guardian"); -+ registerMob("Pig"); -+ registerMob("Sheep"); -+ registerMob("Cow"); -+ registerMob("Chicken"); -+ registerMob("Squid"); -+ registerMob("Wolf"); -+ registerMob("MushroomCow"); -+ registerMob("SnowMan"); -+ registerMob("Ozelot"); -+ registerMob("VillagerGolem"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "EntityHorse", new DataWalkerItemLists("Items", "ArmorItems", "HandItems")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "EntityHorse", new DataWalkerItems("ArmorItem", "SaddleItem")); -+ registerMob("Rabbit"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "Villager", (final MapType data, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, data, "Inventory", fromVersion, toVersion); -+ -+ WalkerUtils.convertList(MCTypeRegistry.VILLAGER_TRADE, data.getMap("Offers"), "Recipes", fromVersion, toVersion); -+ -+ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, data, "ArmorItems", fromVersion, toVersion); -+ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, data, "HandItems", fromVersion, toVersion); -+ -+ return null; -+ }); -+ registerMob("Shulker"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "AreaEffectCloud", new DataWalkerTypePaths<>(MCTypeRegistry.PARTICLE, "Particle")); -+ -+ MCTypeRegistry.STRUCTURE.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ final ListType entities = data.getList("entities", ObjectType.MAP); -+ if (entities != null) { -+ for (int i = 0, len = entities.size(); i < len; ++i) { -+ WalkerUtils.convert(MCTypeRegistry.ENTITY, entities.getMap(i), "nbt", fromVersion, toVersion); -+ } -+ } -+ -+ final ListType blocks = data.getList("blocks", ObjectType.MAP); -+ if (blocks != null) { -+ for (int i = 0, len = blocks.size(); i < len; ++i) { -+ WalkerUtils.convert(MCTypeRegistry.TILE_ENTITY, blocks.getMap(i), "nbt", fromVersion, toVersion); -+ } -+ } -+ -+ WalkerUtils.convertList(MCTypeRegistry.BLOCK_STATE, data, "palette", fromVersion, toVersion); -+ -+ return null; -+ }); -+ } -+ -+ private V100() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V101.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V101.java -new file mode 100644 -index 0000000000000000000000000000000000000000..96ea4669d6ad893ba52af045082f9ccb90fe90bf ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V101.java -@@ -0,0 +1,38 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.util.ComponentUtils; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V101 { -+ -+ private static final int VERSION = MCVersions.V15W32A + 1; -+ -+ private static void updateLine(final MapType data, final String path) { -+ final String textString = data.getString(path); -+ -+ if (textString == null) { -+ return; -+ } -+ -+ data.setString(path, ComponentUtils.convertFromLenient(textString)); -+ } -+ -+ public static void register() { -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("Sign", new DataConverter<>(VERSION) { -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ updateLine(data, "Text1"); -+ updateLine(data, "Text2"); -+ updateLine(data, "Text3"); -+ updateLine(data, "Text4"); -+ return null; -+ } -+ }); -+ } -+ -+ private V101() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V102.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V102.java -new file mode 100644 -index 0000000000000000000000000000000000000000..00bb3cff8f3d220d65a18f9b82b4b5361588b109 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V102.java -@@ -0,0 +1,86 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.HelperItemNameV102; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.Types; -+import com.mojang.logging.LogUtils; -+import org.slf4j.Logger; -+ -+public final class V102 { -+ -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ -+ private static final int VERSION = MCVersions.V15W32A + 2; -+ -+ public static void register() { -+ // V102 -> V15W32A + 2 -+ // V102 schema only modifies ITEM_STACK to have only a string ID, but our ITEM_NAME is generic (int or String) so we don't -+ // actually need to update the walker -+ -+ MCTypeRegistry.ITEM_NAME.addConverter(new DataConverter<>(VERSION) { -+ @Override -+ public Object convert(final Object data, final long sourceVersion, final long toVersion) { -+ if (!(data instanceof Number)) { -+ return null; -+ } -+ final int id = ((Number)data).intValue(); -+ final String remap = HelperItemNameV102.getNameFromId(id); -+ if (remap == null) { -+ LOGGER.warn("Unknown legacy integer id (V102) " + id); -+ } -+ return remap == null ? HelperItemNameV102.getNameFromId(0) : remap; -+ } -+ }); -+ -+ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (!data.hasKey("id", ObjectType.NUMBER)) { -+ return null; -+ } -+ -+ final int id = data.getInt("id"); -+ -+ String remap = HelperItemNameV102.getNameFromId(id); -+ if (remap == null) { -+ LOGGER.warn("Unknown legacy integer id (V102) " + id); -+ remap = HelperItemNameV102.getNameFromId(0); -+ } -+ -+ data.setString("id", remap); -+ -+ return null; -+ } -+ }); -+ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:potion", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final short damage = data.getShort("Damage"); -+ if (damage != 0) { -+ data.setShort("Damage", (short)0); -+ } -+ MapType tag = data.getMap("tag"); -+ if (tag == null) { -+ tag = Types.NBT.createEmptyMap(); -+ data.setMap("tag", tag); -+ } -+ -+ if (!tag.hasKey("Potion", ObjectType.STRING)) { -+ final String converted = HelperItemNameV102.getPotionNameFromId(damage); -+ tag.setString("Potion", converted == null ? "minecraft:water" : converted); -+ if ((damage & 16384) == 16384) { -+ data.setString("id", "minecraft:splash_potion"); -+ } -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V102() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1022.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1022.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c1510d9167f466d3b7e3756353224f12f3876442 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1022.java -@@ -0,0 +1,41 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V1022 { -+ -+ private static final int VERSION = MCVersions.V17W06A; -+ -+ public static void register() { -+ MCTypeRegistry.PLAYER.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convert(MCTypeRegistry.ENTITY, data.getMap("RootVehicle"), "Entity", fromVersion, toVersion); -+ -+ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, data, "Inventory", fromVersion, toVersion); -+ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, data, "EnderItems", fromVersion, toVersion); -+ -+ WalkerUtils.convert(MCTypeRegistry.ENTITY, data, "ShoulderEntityLeft", fromVersion, toVersion); -+ WalkerUtils.convert(MCTypeRegistry.ENTITY, data, "ShoulderEntityRight", fromVersion, toVersion); -+ -+ final MapType recipeBook = data.getMap("recipeBook"); -+ if (recipeBook != null) { -+ WalkerUtils.convertList(MCTypeRegistry.RECIPE, recipeBook, "recipes", fromVersion, toVersion); -+ WalkerUtils.convertList(MCTypeRegistry.RECIPE, recipeBook, "toBeDisplayed", fromVersion, toVersion); -+ } -+ -+ return null; -+ }); -+ -+ MCTypeRegistry.HOTBAR.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ for (final String key : data.keys()) { -+ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, data, key, fromVersion, toVersion); -+ } -+ -+ return null; -+ }); -+ } -+ -+ private V1022() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V105.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V105.java -new file mode 100644 -index 0000000000000000000000000000000000000000..189b682da7eacea118610e466e8648675fccf776 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V105.java -@@ -0,0 +1,49 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.HelperSpawnEggNameV105; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.Types; -+ -+public final class V105 { -+ -+ private static final int VERSION = MCVersions.V15W32C + 1; -+ -+ public static void register() { -+ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:spawn_egg", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ MapType tag = data.getMap("tag"); -+ if (tag == null) { -+ tag = Types.NBT.createEmptyMap(); -+ } -+ -+ final short damage = data.getShort("Damage"); -+ if (damage != 0) { -+ data.setShort("Damage", (short)0); -+ } -+ -+ MapType entityTag = tag.getMap("EntityTag"); -+ if (entityTag == null) { -+ entityTag = Types.NBT.createEmptyMap(); -+ } -+ -+ if (!entityTag.hasKey("id", ObjectType.STRING)) { -+ final String converted = HelperSpawnEggNameV105.getSpawnNameFromId(damage); -+ if (converted != null) { -+ entityTag.setString("id", converted); -+ tag.setMap("EntityTag", entityTag); -+ data.setMap("tag", tag); -+ } -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V105() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V106.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V106.java -new file mode 100644 -index 0000000000000000000000000000000000000000..fa9b11b46f0fbcaabcaed02a7fc3f5af3337ec27 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V106.java -@@ -0,0 +1,83 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.Types; -+ -+public final class V106 { -+ -+ private static final int VERSION = MCVersions.V15W32C + 2; -+ -+ public static void register() { -+ // V106 -> V15W32C + 2 -+ -+ MCTypeRegistry.UNTAGGED_SPAWNER.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ // While all converters for spawners check the id for this version, we don't because spawners exist in minecarts. ooops! Loading a chunk -+ // with a minecart spawner from 1.7.10 in 1.16.5 vanilla will fail to convert! Clearly there was a mistake in how they -+ // used and applied spawner converters. In anycase, do not check the id - we are not guaranteed to be a tile -+ // entity. We can be a regular old minecart spawner. And we know we are a spawner because this is only called from data walkers. -+ -+ final String entityId = data.getString("EntityId"); -+ if (entityId != null) { -+ data.remove("EntityId"); -+ MapType spawnData = data.getMap("SpawnData"); -+ if (spawnData == null) { -+ spawnData = Types.NBT.createEmptyMap(); -+ data.setMap("SpawnData", spawnData); -+ } -+ spawnData.setString("id", entityId.isEmpty() ? "Pig" : entityId); -+ } -+ -+ final ListType spawnPotentials = data.getList("SpawnPotentials", ObjectType.MAP); -+ if (spawnPotentials != null) { -+ for (int i = 0, len = spawnPotentials.size(); i < len; ++i) { -+ // convert to standard entity format (it's not a coincidence a walker for spawners is only added -+ // in this version) -+ final MapType spawn = spawnPotentials.getMap(i); -+ final String spawnType = spawn.getString("Type"); -+ if (spawnType == null) { -+ continue; -+ } -+ spawn.remove("Type"); -+ -+ MapType properties = spawn.getMap("Properties"); -+ if (properties == null) { -+ properties = Types.NBT.createEmptyMap(); -+ } else { -+ spawn.remove("Properties"); -+ } -+ -+ properties.setString("id", spawnType); -+ -+ spawn.setMap("Entity", properties); -+ } -+ } -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.UNTAGGED_SPAWNER.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ final ListType spawnPotentials = data.getList("SpawnPotentials", ObjectType.MAP); -+ if (spawnPotentials != null) { -+ for (int i = 0, len = spawnPotentials.size(); i < len; ++i) { -+ final MapType spawnPotential = spawnPotentials.getMap(i); -+ WalkerUtils.convert(MCTypeRegistry.ENTITY, spawnPotential, "Entity", fromVersion, toVersion); -+ } -+ } -+ -+ WalkerUtils.convert(MCTypeRegistry.ENTITY, data, "SpawnData", fromVersion, toVersion); -+ -+ return null; -+ }); -+ } -+ -+ private V106() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V107.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V107.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e9d288c41c40d96ac7c6b605babc436d6a5796f3 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V107.java -@@ -0,0 +1,43 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V107 { -+ -+ private static final int VERSION = MCVersions.V15W32C + 3; -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("Minecart", new DataConverter<>(VERSION) { -+ protected final String[] MINECART_IDS = new String[] { -+ "MinecartRideable", // 0 -+ "MinecartChest", // 1 -+ "MinecartFurnace", // 2 -+ "MinecartTNT", // 3 -+ "MinecartSpawner", // 4 -+ "MinecartHopper", // 5 -+ "MinecartCommandBlock" // 6 -+ }; -+ // Vanilla does not use all of the IDs here. The legacy (pre DFU) code does, so I'm going to use them. -+ // No harm in catching more cases here. -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ String newId = "MinecartRideable"; // dfl -+ final int type = data.getInt("Type"); -+ data.remove("Type"); -+ -+ if (type >= 0 && type < MINECART_IDS.length) { -+ newId = MINECART_IDS[type]; -+ } -+ data.setString("id", newId); -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V107() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V108.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V108.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ba9487bc35bedfd7261d4a4fd9476de070f65f33 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V108.java -@@ -0,0 +1,46 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import com.mojang.logging.LogUtils; -+import org.slf4j.Logger; -+import java.util.UUID; -+ -+public final class V108 { -+ -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ -+ private static final int VERSION = MCVersions.V15W32C + 4; -+ -+ public static void register() { -+ // Convert String UUID into UUIDMost and UUIDLeast -+ MCTypeRegistry.ENTITY.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final String uuidString = data.getString("UUID"); -+ -+ if (uuidString == null) { -+ return null; -+ } -+ data.remove("UUID"); -+ -+ final UUID uuid; -+ try { -+ uuid = UUID.fromString(uuidString); -+ } catch (final Exception ex) { -+ LOGGER.warn("Failed to parse UUID for legacy entity (V108): " + uuidString, ex); -+ return null; -+ } -+ -+ data.setLong("UUIDMost", uuid.getMostSignificantBits()); -+ data.setLong("UUIDLeast", uuid.getLeastSignificantBits()); -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V108() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V109.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V109.java -new file mode 100644 -index 0000000000000000000000000000000000000000..5df0c8da6415a4651e5678a170bc8ff32dd66337 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V109.java -@@ -0,0 +1,41 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V109 { -+ -+ private static final int VERSION = MCVersions.V15W32C + 5; -+ -+ public static void register() { -+ // Converts health to be in float, and cleans up whatever the hell was going on with HealF and Health... -+ MCTypeRegistry.ENTITY.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final Number healF = data.getNumber("HealF"); -+ final Number heal = data.getNumber("Health"); -+ -+ final float newHealth; -+ -+ if (healF != null) { -+ data.remove("HealF"); -+ newHealth = healF.floatValue(); -+ } else { -+ if (heal == null) { -+ return null; -+ } -+ -+ newHealth = heal.floatValue(); -+ } -+ -+ data.setFloat("Health", newHealth); -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V109() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V110.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V110.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b089fc93b88c5a7b4bb1eb0e105120b5393de1b1 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V110.java -@@ -0,0 +1,38 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.Types; -+ -+public final class V110 { -+ -+ private static final int VERSION = MCVersions.V15W32C + 6; -+ -+ public static void register() { -+ // Moves the Saddle boolean to be an actual saddle item. Note: The data walker for the SaddleItem exists -+ // in V99, it doesn't need to be added here. -+ MCTypeRegistry.ENTITY.addConverterForId("EntityHorse", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (!data.getBoolean("Saddle") || data.hasKey("SaddleItem", ObjectType.MAP)) { -+ return null; -+ } -+ -+ final MapType saddleItem = Types.NBT.createEmptyMap(); -+ data.remove("Saddle"); -+ data.setMap("SaddleItem", saddleItem); -+ -+ saddleItem.setString("id", "minecraft:saddle"); -+ saddleItem.setByte("Count", (byte)1); -+ saddleItem.setShort("Damage", (short)0); -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V110() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V111.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V111.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0c69cf9b419049dc5338abb408fa3f0390e4e353 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V111.java -@@ -0,0 +1,64 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V111 { -+ -+ private static final int VERSION = MCVersions.V15W33B; -+ -+ public static void register() { -+ final EntityRotationFix rotationFix = new EntityRotationFix(VERSION); -+ MCTypeRegistry.ENTITY.addConverterForId("Painting", rotationFix); -+ MCTypeRegistry.ENTITY.addConverterForId("ItemFrame", rotationFix); -+ } -+ -+ private V111() {} -+ -+ protected static final class EntityRotationFix extends DataConverter, MapType> { -+ -+ private static final int[][] DIRECTIONS = new int[][] { -+ {0, 0, 1}, -+ {-1, 0, 0}, -+ {0, 0, -1}, -+ {1, 0, 0} -+ }; -+ -+ public EntityRotationFix(final int version) { -+ super(version); -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (data.getNumber("Facing") != null) { -+ return null; -+ } -+ -+ final Number direction = data.getNumber("Direction"); -+ final int facing; -+ if (direction != null) { -+ data.remove("Direction"); -+ facing = direction.intValue() % DIRECTIONS.length; -+ final int[] offsets = DIRECTIONS[facing]; -+ data.setInt("TileX", data.getInt("TileX") + offsets[0]); -+ data.setInt("TileY", data.getInt("TileY") + offsets[1]); -+ data.setInt("TileZ", data.getInt("TileZ") + offsets[2]); -+ if ("ItemFrame".equals(data.getString("id"))) { -+ final Number rotation = data.getNumber("ItemRotation"); -+ if (rotation != null) { -+ data.setByte("ItemRotation", (byte)(rotation.byteValue() * 2)); -+ } -+ } -+ } else { -+ facing = data.getByte("Dir") % DIRECTIONS.length; -+ data.remove("Dir"); -+ } -+ -+ data.setByte("Facing", (byte)facing); -+ -+ return null; -+ } -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1125.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1125.java -new file mode 100644 -index 0000000000000000000000000000000000000000..41ceef54e202420616ad57e9f9c200457c7d2848 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1125.java -@@ -0,0 +1,101 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.hooks.DataHookValueTypeEnforceNamespaced; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.Types; -+ -+public final class V1125 { -+ -+ private static final int VERSION = MCVersions.V17W15A; -+ private static final int BED_BLOCK_ID = 416; -+ -+ public static void register() { -+ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType level = data.getMap("Level"); -+ if (level == null) { -+ return null; -+ } -+ -+ final int chunkX = level.getInt("xPos"); -+ final int chunkZ = level.getInt("zPos"); -+ -+ final ListType sections = level.getList("Sections", ObjectType.MAP); -+ if (sections == null) { -+ return null; -+ } -+ -+ ListType tileEntities = level.getList("TileEntities", ObjectType.MAP); -+ if (tileEntities == null) { -+ tileEntities = Types.NBT.createEmptyList(); -+ level.setList("TileEntities", tileEntities); -+ } -+ -+ for (int i = 0, len = sections.size(); i < len; ++i) { -+ final MapType section = sections.getMap(i); -+ -+ final byte sectionY = section.getByte("Y"); -+ final byte[] blocks = section.getBytes("Blocks"); -+ -+ if (blocks == null) { -+ continue; -+ } -+ -+ for (int blockIndex = 0; blockIndex < blocks.length; ++blockIndex) { -+ if (BED_BLOCK_ID != ((blocks[blockIndex] & 255) << 4)) { -+ continue; -+ } -+ -+ final int localX = blockIndex & 15; -+ final int localZ = (blockIndex >> 4) & 15; -+ final int localY = (blockIndex >> 8) & 15; -+ -+ final MapType newTile = Types.NBT.createEmptyMap(); -+ newTile.setString("id", "minecraft:bed"); -+ newTile.setInt("x", localX + (chunkX << 4)); -+ newTile.setInt("y", localY + (sectionY << 4)); -+ newTile.setInt("z", localZ + (chunkZ << 4)); -+ newTile.setShort("color", (short)14); // Red -+ -+ tileEntities.addMap(newTile); -+ } -+ } -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:bed", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (data.getShort("Damage") == 0) { -+ data.setShort("Damage", (short)14); // Red -+ } -+ -+ return null; -+ } -+ }); -+ -+ -+ MCTypeRegistry.ADVANCEMENTS.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convertKeys(MCTypeRegistry.BIOME, data.getMap("minecraft:adventure/adventuring_time"), "criteria", fromVersion, toVersion); -+ WalkerUtils.convertKeys(MCTypeRegistry.ENTITY_NAME, data.getMap("minecraft:adventure/kill_a_mob"), "criteria", fromVersion, toVersion); -+ WalkerUtils.convertKeys(MCTypeRegistry.ENTITY_NAME, data.getMap("minecraft:adventure/kill_all_mobs"), "criteria", fromVersion, toVersion); -+ WalkerUtils.convertKeys(MCTypeRegistry.ENTITY_NAME, data.getMap("minecraft:adventure/bred_all_animals"), "criteria", fromVersion, toVersion); -+ -+ return null; -+ }); -+ -+ // Enforce namespacing for ids -+ MCTypeRegistry.BIOME.addStructureHook(VERSION, new DataHookValueTypeEnforceNamespaced()); -+ } -+ -+ private V1125() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V113.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V113.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7b7d02eac9e121c45b557b664e156327d182c015 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V113.java -@@ -0,0 +1,40 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V113 { -+ -+ private static final int VERSION = MCVersions.V15W33C + 1; -+ -+ private static void checkList(final MapType data, final String id, final int requiredLength) { -+ final ListType list = data.getList(id, ObjectType.FLOAT); -+ if (list != null && list.size() == requiredLength) { -+ for (int i = 0; i < requiredLength; ++i) { -+ if (list.getFloat(i) != 0.0F) { -+ return; -+ } -+ } -+ } -+ -+ data.remove(id); -+ } -+ -+ public static void register() { -+ // Removes "HandDropChances" and "ArmorDropChances" if they're empty. -+ MCTypeRegistry.ENTITY.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ checkList(data, "HandDropChances", 2); -+ checkList(data, "ArmorDropChances", 4); -+ return null; -+ } -+ }); -+ } -+ -+ private V113() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1344.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1344.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b735165f9b296730b77339875255aa982e18a40a ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1344.java -@@ -0,0 +1,176 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -+ -+public final class V1344 { -+ -+ private static final int VERSION = MCVersions.V1_12_2 + 1; -+ -+ private static final Int2ObjectOpenHashMap BUTTON_ID_TO_NAME = new Int2ObjectOpenHashMap<>(); -+ static { -+ BUTTON_ID_TO_NAME.put(0, "key.unknown"); -+ BUTTON_ID_TO_NAME.put(11, "key.0"); -+ BUTTON_ID_TO_NAME.put(2, "key.1"); -+ BUTTON_ID_TO_NAME.put(3, "key.2"); -+ BUTTON_ID_TO_NAME.put(4, "key.3"); -+ BUTTON_ID_TO_NAME.put(5, "key.4"); -+ BUTTON_ID_TO_NAME.put(6, "key.5"); -+ BUTTON_ID_TO_NAME.put(7, "key.6"); -+ BUTTON_ID_TO_NAME.put(8, "key.7"); -+ BUTTON_ID_TO_NAME.put(9, "key.8"); -+ BUTTON_ID_TO_NAME.put(10, "key.9"); -+ BUTTON_ID_TO_NAME.put(30, "key.a"); -+ BUTTON_ID_TO_NAME.put(40, "key.apostrophe"); -+ BUTTON_ID_TO_NAME.put(48, "key.b"); -+ BUTTON_ID_TO_NAME.put(43, "key.backslash"); -+ BUTTON_ID_TO_NAME.put(14, "key.backspace"); -+ BUTTON_ID_TO_NAME.put(46, "key.c"); -+ BUTTON_ID_TO_NAME.put(58, "key.caps.lock"); -+ BUTTON_ID_TO_NAME.put(51, "key.comma"); -+ BUTTON_ID_TO_NAME.put(32, "key.d"); -+ BUTTON_ID_TO_NAME.put(211, "key.delete"); -+ BUTTON_ID_TO_NAME.put(208, "key.down"); -+ BUTTON_ID_TO_NAME.put(18, "key.e"); -+ BUTTON_ID_TO_NAME.put(207, "key.end"); -+ BUTTON_ID_TO_NAME.put(28, "key.enter"); -+ BUTTON_ID_TO_NAME.put(13, "key.equal"); -+ BUTTON_ID_TO_NAME.put(1, "key.escape"); -+ BUTTON_ID_TO_NAME.put(33, "key.f"); -+ BUTTON_ID_TO_NAME.put(59, "key.f1"); -+ BUTTON_ID_TO_NAME.put(68, "key.f10"); -+ BUTTON_ID_TO_NAME.put(87, "key.f11"); -+ BUTTON_ID_TO_NAME.put(88, "key.f12"); -+ BUTTON_ID_TO_NAME.put(100, "key.f13"); -+ BUTTON_ID_TO_NAME.put(101, "key.f14"); -+ BUTTON_ID_TO_NAME.put(102, "key.f15"); -+ BUTTON_ID_TO_NAME.put(103, "key.f16"); -+ BUTTON_ID_TO_NAME.put(104, "key.f17"); -+ BUTTON_ID_TO_NAME.put(105, "key.f18"); -+ BUTTON_ID_TO_NAME.put(113, "key.f19"); -+ BUTTON_ID_TO_NAME.put(60, "key.f2"); -+ BUTTON_ID_TO_NAME.put(61, "key.f3"); -+ BUTTON_ID_TO_NAME.put(62, "key.f4"); -+ BUTTON_ID_TO_NAME.put(63, "key.f5"); -+ BUTTON_ID_TO_NAME.put(64, "key.f6"); -+ BUTTON_ID_TO_NAME.put(65, "key.f7"); -+ BUTTON_ID_TO_NAME.put(66, "key.f8"); -+ BUTTON_ID_TO_NAME.put(67, "key.f9"); -+ BUTTON_ID_TO_NAME.put(34, "key.g"); -+ BUTTON_ID_TO_NAME.put(41, "key.grave.accent"); -+ BUTTON_ID_TO_NAME.put(35, "key.h"); -+ BUTTON_ID_TO_NAME.put(199, "key.home"); -+ BUTTON_ID_TO_NAME.put(23, "key.i"); -+ BUTTON_ID_TO_NAME.put(210, "key.insert"); -+ BUTTON_ID_TO_NAME.put(36, "key.j"); -+ BUTTON_ID_TO_NAME.put(37, "key.k"); -+ BUTTON_ID_TO_NAME.put(82, "key.keypad.0"); -+ BUTTON_ID_TO_NAME.put(79, "key.keypad.1"); -+ BUTTON_ID_TO_NAME.put(80, "key.keypad.2"); -+ BUTTON_ID_TO_NAME.put(81, "key.keypad.3"); -+ BUTTON_ID_TO_NAME.put(75, "key.keypad.4"); -+ BUTTON_ID_TO_NAME.put(76, "key.keypad.5"); -+ BUTTON_ID_TO_NAME.put(77, "key.keypad.6"); -+ BUTTON_ID_TO_NAME.put(71, "key.keypad.7"); -+ BUTTON_ID_TO_NAME.put(72, "key.keypad.8"); -+ BUTTON_ID_TO_NAME.put(73, "key.keypad.9"); -+ BUTTON_ID_TO_NAME.put(78, "key.keypad.add"); -+ BUTTON_ID_TO_NAME.put(83, "key.keypad.decimal"); -+ BUTTON_ID_TO_NAME.put(181, "key.keypad.divide"); -+ BUTTON_ID_TO_NAME.put(156, "key.keypad.enter"); -+ BUTTON_ID_TO_NAME.put(141, "key.keypad.equal"); -+ BUTTON_ID_TO_NAME.put(55, "key.keypad.multiply"); -+ BUTTON_ID_TO_NAME.put(74, "key.keypad.subtract"); -+ BUTTON_ID_TO_NAME.put(38, "key.l"); -+ BUTTON_ID_TO_NAME.put(203, "key.left"); -+ BUTTON_ID_TO_NAME.put(56, "key.left.alt"); -+ BUTTON_ID_TO_NAME.put(26, "key.left.bracket"); -+ BUTTON_ID_TO_NAME.put(29, "key.left.control"); -+ BUTTON_ID_TO_NAME.put(42, "key.left.shift"); -+ BUTTON_ID_TO_NAME.put(219, "key.left.win"); -+ BUTTON_ID_TO_NAME.put(50, "key.m"); -+ BUTTON_ID_TO_NAME.put(12, "key.minus"); -+ BUTTON_ID_TO_NAME.put(49, "key.n"); -+ BUTTON_ID_TO_NAME.put(69, "key.num.lock"); -+ BUTTON_ID_TO_NAME.put(24, "key.o"); -+ BUTTON_ID_TO_NAME.put(25, "key.p"); -+ BUTTON_ID_TO_NAME.put(209, "key.page.down"); -+ BUTTON_ID_TO_NAME.put(201, "key.page.up"); -+ BUTTON_ID_TO_NAME.put(197, "key.pause"); -+ BUTTON_ID_TO_NAME.put(52, "key.period"); -+ BUTTON_ID_TO_NAME.put(183, "key.print.screen"); -+ BUTTON_ID_TO_NAME.put(16, "key.q"); -+ BUTTON_ID_TO_NAME.put(19, "key.r"); -+ BUTTON_ID_TO_NAME.put(205, "key.right"); -+ BUTTON_ID_TO_NAME.put(184, "key.right.alt"); -+ BUTTON_ID_TO_NAME.put(27, "key.right.bracket"); -+ BUTTON_ID_TO_NAME.put(157, "key.right.control"); -+ BUTTON_ID_TO_NAME.put(54, "key.right.shift"); -+ BUTTON_ID_TO_NAME.put(220, "key.right.win"); -+ BUTTON_ID_TO_NAME.put(31, "key.s"); -+ BUTTON_ID_TO_NAME.put(70, "key.scroll.lock"); -+ BUTTON_ID_TO_NAME.put(39, "key.semicolon"); -+ BUTTON_ID_TO_NAME.put(53, "key.slash"); -+ BUTTON_ID_TO_NAME.put(57, "key.space"); -+ BUTTON_ID_TO_NAME.put(20, "key.t"); -+ BUTTON_ID_TO_NAME.put(15, "key.tab"); -+ BUTTON_ID_TO_NAME.put(22, "key.u"); -+ BUTTON_ID_TO_NAME.put(200, "key.up"); -+ BUTTON_ID_TO_NAME.put(47, "key.v"); -+ BUTTON_ID_TO_NAME.put(17, "key.w"); -+ BUTTON_ID_TO_NAME.put(45, "key.x"); -+ BUTTON_ID_TO_NAME.put(21, "key.y"); -+ BUTTON_ID_TO_NAME.put(44, "key.z"); -+ } -+ -+ public static void register() { -+ MCTypeRegistry.OPTIONS.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ for (final String key : data.keys()) { -+ if (!key.startsWith("key_")) { -+ continue; -+ } -+ final String value = data.getString(key); -+ final int code; -+ try { -+ code = Integer.parseInt(value); -+ } catch (final NumberFormatException ex) { -+ continue; -+ } -+ -+ final String newEntry; -+ -+ if (code < 0) { -+ final int mouseCode = code + 100; -+ switch (mouseCode) { -+ case 0: -+ newEntry = "key.mouse.left"; -+ break; -+ case 1: -+ newEntry = "key.mouse.right"; -+ break; -+ case 2: -+ newEntry = "key.mouse.middle"; -+ break; -+ default: -+ newEntry = "key.mouse." + (mouseCode + 1); -+ break; -+ } -+ } else { -+ newEntry = BUTTON_ID_TO_NAME.getOrDefault(code, "key.unknown"); -+ } -+ -+ // No CMEs occur for existing entries in maps. -+ data.setString(key, newEntry); -+ } -+ return null; -+ } -+ }); -+ } -+ -+ private V1344() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V135.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V135.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1c9f754332d8a2299b7e2e5565882da52948d3a9 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V135.java -@@ -0,0 +1,60 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.Types; -+ -+public final class V135 { -+ -+ private static final int VERSION = MCVersions.V15W40B + 1; -+ -+ public static void register() { -+ // In this update they changed the "Riding" value to be "Passengers", which is now a list. So it added -+ // support for multiple entities riding. Of course, Riding and Passenger are opposites - so it also will -+ // switch the data layout to be from highest rider to lowest rider, in terms of depth. -+ MCTypeRegistry.ENTITY.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(MapType data, final long sourceVersion, final long toVersion) { -+ MapType ret = null; -+ while (data.hasKey("Riding", ObjectType.MAP)) { -+ final MapType riding = data.getMap("Riding"); -+ data.remove("Riding"); -+ -+ final ListType passengers = Types.NBT.createEmptyList(); -+ riding.setList("Passengers", passengers); -+ passengers.addMap(data); -+ -+ ret = data = riding; -+ } -+ -+ return ret; -+ } -+ }); -+ -+ -+ MCTypeRegistry.PLAYER.addStructureWalker(VERSION, new DataWalkerItemLists("Inventory", "EnderItems")); -+ MCTypeRegistry.PLAYER.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ final MapType rootVehicle = data.getMap("RootVehicle"); -+ if (rootVehicle != null) { -+ WalkerUtils.convert(MCTypeRegistry.ENTITY, rootVehicle, "Entity", fromVersion, toVersion); -+ } -+ -+ return null; -+ }); -+ -+ MCTypeRegistry.ENTITY.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convertList(MCTypeRegistry.ENTITY, data, "Passengers", fromVersion, toVersion); -+ -+ return null; -+ }); -+ -+ } -+ -+ private V135() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V143.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V143.java -new file mode 100644 -index 0000000000000000000000000000000000000000..90889dddd8a510fe69c47413f5fe3ed4a756fedb ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V143.java -@@ -0,0 +1,17 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterAbstractEntityRename; -+ -+public final class V143 { -+ -+ private static final int VERSION = MCVersions.V15W44B; -+ -+ public static void register() { -+ ConverterAbstractEntityRename.register(VERSION, (final String input) -> { -+ return "TippedArrow".equals(input) ? "Arrow" : null; -+ }); -+ } -+ -+ private V143() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1446.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1446.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0e198bef171c92d53725d338bb793b1e269f2997 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1446.java -@@ -0,0 +1,35 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V1446 { -+ -+ private static final int VERSION = MCVersions.V17W43B + 1; -+ -+ public static void register() { -+ MCTypeRegistry.OPTIONS.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ for (final String key : data.keys()) { -+ if (!key.startsWith("key_")) { -+ continue; -+ } -+ -+ final String value = data.getString(key); -+ -+ if (value.startsWith("key.mouse") || value.startsWith("scancode.")) { -+ continue; -+ } -+ -+ data.setString(key, "key.keyboard." + value.substring("key.".length())); -+ } -+ return null; -+ } -+ }); -+ } -+ -+ private V1446() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1450.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1450.java -new file mode 100644 -index 0000000000000000000000000000000000000000..bf6f57bc84785622aea35dc70872db6d4d9516a1 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1450.java -@@ -0,0 +1,24 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.HelperBlockFlatteningV1450; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V1450 { -+ -+ private static final int VERSION = MCVersions.V17W46A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.BLOCK_STATE.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType ret = HelperBlockFlatteningV1450.flattenNBT(data); -+ return ret == data ? null : ret.copy(); // copy to avoid problems with later state datafixers -+ } -+ }); -+ } -+ -+ private V1450() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1451.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1451.java -new file mode 100644 -index 0000000000000000000000000000000000000000..fd46b075f3c18c52e31aeee4cb4638d5d7b673c3 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1451.java -@@ -0,0 +1,513 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.converters.datatypes.DataHook; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.chunk.ConverterFlattenChunk; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.HelperBlockFlatteningV1450; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.HelperItemNameV102; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemstack.ConverterFlattenItemStack; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemstack.ConverterFlattenSpawnEgg; -+import ca.spottedleaf.dataconverter.minecraft.converters.stats.ConverterFlattenStats; -+import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterFlattenEntity; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; -+import ca.spottedleaf.dataconverter.minecraft.walkers.tile_entity.DataWalkerTileEntities; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.Types; -+import com.google.common.base.Splitter; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.util.datafix.fixes.BlockStateData; -+import net.minecraft.util.datafix.fixes.EntityBlockStateFix; -+import org.apache.commons.lang3.math.NumberUtils; -+import java.util.Iterator; -+import java.util.List; -+import java.util.stream.Collectors; -+import java.util.stream.StreamSupport; -+ -+public final class V1451 { -+ -+ private static final int VERSION = MCVersions.V17W47A; -+ -+ public static String packWithDot(final String string) { -+ final ResourceLocation resourceLocation = ResourceLocation.tryParse(string); -+ return resourceLocation != null ? resourceLocation.getNamespace() + "." + resourceLocation.getPath() : string; -+ } -+ -+ public static void register() { -+ // V0 -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, 0, "minecraft:trapped_chest", new DataWalkerItemLists("Items")); -+ -+ // V1 -+ MCTypeRegistry.CHUNK.addStructureConverter(new ConverterFlattenChunk()); -+ -+ MCTypeRegistry.CHUNK.addStructureWalker(VERSION, 1, (final MapType data, final long fromVersion, final long toVersion) -> { -+ final MapType level = data.getMap("Level"); -+ if (level == null) { -+ return null; -+ } -+ -+ WalkerUtils.convertList(MCTypeRegistry.ENTITY, level, "Entities", fromVersion, toVersion); -+ WalkerUtils.convertList(MCTypeRegistry.TILE_ENTITY, level, "TileEntities", fromVersion, toVersion); -+ -+ final ListType tileTicks = level.getList("TileTicks", ObjectType.MAP); -+ if (tileTicks != null) { -+ for (int i = 0, len = tileTicks.size(); i < len; ++i) { -+ final MapType tileTick = tileTicks.getMap(i); -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, tileTick, "i", fromVersion, toVersion); -+ } -+ } -+ -+ final ListType sections = level.getList("Sections", ObjectType.MAP); -+ if (sections != null) { -+ for (int i = 0, len = sections.size(); i < len; ++i) { -+ final MapType section = sections.getMap(i); -+ WalkerUtils.convertList(MCTypeRegistry.BLOCK_STATE, section, "Palette", fromVersion, toVersion); -+ } -+ } -+ -+ return null; -+ }); -+ -+ // V2 -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:piston", new DataConverter<>(VERSION, 2) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final int blockId = data.getInt("blockId"); -+ final int blockData = data.getInt("blockData") & 15; -+ -+ data.remove("blockId"); -+ data.remove("blockData"); -+ -+ data.setMap("blockState", HelperBlockFlatteningV1450.getNBTForId((blockId << 4) | blockData).copy()); // copy to avoid problems with later state datafixers -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, 2, "minecraft:piston", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "blockState")); -+ -+ // V3 -+ ConverterFlattenEntity.register(); -+ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:filled_map", new DataConverter<>(VERSION, 3) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ MapType tag = data.getMap("tag"); -+ if (tag == null) { -+ tag = Types.NBT.createEmptyMap(); -+ data.setMap("tag", tag); -+ } -+ -+ if (!tag.hasKey("map", ObjectType.NUMBER)) { // This if is from CB. as usual, no documentation from CB. I'm guessing it just wants to avoid possibly overwriting it. seems fine. -+ tag.setInt("map", data.getInt("Damage")); -+ } -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:potion", new DataWalkerItems("Potion")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:arrow", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "inBlockState")); -+ V100.registerEquipment(VERSION, 3, "minecraft:enderman"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:enderman", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "carriedBlockState")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:falling_block", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "BlockState")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:falling_block", new DataWalkerTileEntities("TileEntityData")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:spectral_arrow", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "inBlockState")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:chest_minecart", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "DisplayState")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:chest_minecart", new DataWalkerItemLists("Items")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:commandblock_minecart", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "DisplayState")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:furnace_minecart", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "DisplayState")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:hopper_minecart", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "DisplayState")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:hopper_minecart", new DataWalkerItemLists("Items")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:minecart", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "DisplayState")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:spawner_minecart", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "DisplayState")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:spawner_minecart", (final MapType data, final long fromVersion, final long toVersion) -> { -+ MCTypeRegistry.UNTAGGED_SPAWNER.convert(data, fromVersion, toVersion); -+ return null; -+ }); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, 3, "minecraft:tnt_minecart", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "DisplayState")); -+ -+ // V4 -+ MCTypeRegistry.BLOCK_NAME.addConverter(new DataConverter<>(VERSION, 4) { -+ @Override -+ public Object convert(final Object data, final long sourceVersion, final long toVersion) { -+ if (data instanceof Number) { -+ return HelperBlockFlatteningV1450.getNameForId(((Number)data).intValue()); -+ } else if (data instanceof String) { -+ return HelperBlockFlatteningV1450.getNewBlockName((String)data); // structure hook ensured data is namespaced -+ } -+ return null; -+ } -+ }); -+ MCTypeRegistry.ITEM_STACK.addStructureConverter(new ConverterFlattenItemStack()); -+ -+ // V5 -+ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:spawn_egg", new ConverterFlattenSpawnEgg(VERSION, 5)); -+ /* This datafixer has been disabled because the collar colour handler did not change from 1.12 -> 1.13 at all. -+ // So clearly somebody fucked up. This fixes wolf colours incorrectly converting between versions -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:wolf", new DataConverter<>(VERSION, 5) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final Number colour = data.getNumber("CollarColor"); -+ -+ if (colour != null) { -+ data.setByte("CollarColor", (byte)(15 - colour.intValue())); -+ } -+ -+ return null; -+ } -+ }); -+ */ -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:banner", new DataConverter<>(VERSION, 5) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final Number base = data.getNumber("Base"); -+ if (base != null) { -+ data.setInt("Base", 15 - base.intValue()); -+ } -+ -+ final ListType patterns = data.getList("Patterns", ObjectType.MAP); -+ if (patterns != null) { -+ for (int i = 0, len = patterns.size(); i < len; ++i) { -+ final MapType pattern = patterns.getMap(i); -+ final Number colour = pattern.getNumber("Color"); -+ if (colour != null) { -+ pattern.setInt("Color", 15 - colour.intValue()); -+ } -+ } -+ } -+ -+ return null; -+ } -+ }); -+ MCTypeRegistry.LEVEL.addStructureConverter(new DataConverter<>(VERSION, 5) { -+ private final Splitter SPLITTER = Splitter.on(';').limit(5); -+ private final Splitter LAYER_SPLITTER = Splitter.on(','); -+ private final Splitter OLD_AMOUNT_SPLITTER = Splitter.on('x').limit(2); -+ private final Splitter AMOUNT_SPLITTER = Splitter.on('*').limit(2); -+ private final Splitter BLOCK_SPLITTER = Splitter.on(':').limit(3); -+ -+ // idk man i just copy and pasted this one -+ private String fixGeneratorSettings(final String generatorSettings) { -+ if (generatorSettings.isEmpty()) { -+ return "minecraft:bedrock,2*minecraft:dirt,minecraft:grass_block;1;village"; -+ } else { -+ Iterator iterator = SPLITTER.split(generatorSettings).iterator(); -+ String string2 = (String)iterator.next(); -+ int j; -+ String string4; -+ if (iterator.hasNext()) { -+ j = NumberUtils.toInt(string2, 0); -+ string4 = (String)iterator.next(); -+ } else { -+ j = 0; -+ string4 = string2; -+ } -+ -+ if (j >= 0 && j <= 3) { -+ StringBuilder stringBuilder = new StringBuilder(); -+ Splitter splitter = j < 3 ? OLD_AMOUNT_SPLITTER : AMOUNT_SPLITTER; -+ stringBuilder.append((String) StreamSupport.stream(LAYER_SPLITTER.split(string4).spliterator(), false).map((stringx) -> { -+ List list = splitter.splitToList(stringx); -+ int k; -+ String string3; -+ if (list.size() == 2) { -+ k = NumberUtils.toInt((String)list.get(0)); -+ string3 = (String)list.get(1); -+ } else { -+ k = 1; -+ string3 = (String)list.get(0); -+ } -+ -+ List list2 = BLOCK_SPLITTER.splitToList(string3); -+ int l = ((String)list2.get(0)).equals("minecraft") ? 1 : 0; -+ String string5 = (String)list2.get(l); -+ int m = j == 3 ? EntityBlockStateFix.getBlockId("minecraft:" + string5) : NumberUtils.toInt(string5, 0); -+ int n = l + 1; -+ int o = list2.size() > n ? NumberUtils.toInt((String)list2.get(n), 0) : 0; -+ return (k == 1 ? "" : k + "*") + BlockStateData.getTag(m << 4 | o).get("Name").asString(""); -+ }).collect(Collectors.joining(","))); -+ -+ while(iterator.hasNext()) { -+ stringBuilder.append(';').append((String)iterator.next()); -+ } -+ -+ return stringBuilder.toString(); -+ } else { -+ return "minecraft:bedrock,2*minecraft:dirt,minecraft:grass_block;1;village"; -+ } -+ } -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (!"flat".equalsIgnoreCase(data.getString("generatorName"))) { -+ return null; -+ } -+ -+ final String generatorOptions = data.getString("generatorOptions"); -+ if (generatorOptions == null) { -+ return null; -+ } -+ -+ data.setString("generatorOptions", this.fixGeneratorSettings(generatorOptions)); -+ -+ return null; -+ } -+ }); -+ -+ // V6 -+ MCTypeRegistry.STATS.addStructureConverter(ConverterFlattenStats.makeStatsConverter()); -+ MCTypeRegistry.OBJECTIVE.addStructureConverter(ConverterFlattenStats.makeObjectiveConverter()); -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:jukebox", new DataConverter<>(VERSION, 6) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final int record = data.getInt("Record"); -+ if (record <= 0) { -+ return null; -+ } -+ -+ data.remove("Record"); -+ -+ final String newItemId = ConverterFlattenItemStack.flattenItem(HelperItemNameV102.getNameFromId(record), 0); -+ if (newItemId == null) { -+ return null; -+ } -+ -+ final MapType recordItem = Types.NBT.createEmptyMap(); -+ data.setMap("RecordItem", recordItem); -+ -+ recordItem.setString("id", newItemId); -+ recordItem.setByte("Count", (byte)1); -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.STATS.addStructureWalker(VERSION, 6, (final MapType data, final long fromVersion, final long toVersion) -> { -+ final MapType stats = data.getMap("stats"); -+ if (stats == null) { -+ return null; -+ } -+ -+ WalkerUtils.convertKeys(MCTypeRegistry.BLOCK_NAME, stats, "minecraft:mined", fromVersion, toVersion); -+ -+ WalkerUtils.convertKeys(MCTypeRegistry.ITEM_NAME, stats, "minecraft:crafted", fromVersion, toVersion); -+ WalkerUtils.convertKeys(MCTypeRegistry.ITEM_NAME, stats, "minecraft:used", fromVersion, toVersion); -+ WalkerUtils.convertKeys(MCTypeRegistry.ITEM_NAME, stats, "minecraft:broken", fromVersion, toVersion); -+ WalkerUtils.convertKeys(MCTypeRegistry.ITEM_NAME, stats, "minecraft:picked_up", fromVersion, toVersion); -+ WalkerUtils.convertKeys(MCTypeRegistry.ITEM_NAME, stats, "minecraft:dropped", fromVersion, toVersion); -+ -+ WalkerUtils.convertKeys(MCTypeRegistry.ENTITY_NAME, stats, "minecraft:killed", fromVersion, toVersion); -+ WalkerUtils.convertKeys(MCTypeRegistry.ENTITY_NAME, stats, "minecraft:killed_by", fromVersion, toVersion); -+ -+ return null; -+ }); -+ -+ MCTypeRegistry.OBJECTIVE.addStructureHook(VERSION, 6, new DataHook<>() { -+ @Override -+ public MapType preHook(final MapType data, final long fromVersion, final long toVersion) { -+ // unpack -+ final String criteriaName = data.getString("CriteriaName"); -+ String type; -+ String id; -+ -+ if (criteriaName != null) { -+ final int index = criteriaName.indexOf(':'); -+ if (index < 0) { -+ type = "_special"; -+ id = criteriaName; -+ } else { -+ try { -+ type = ResourceLocation.of(criteriaName.substring(0, index), '.').toString(); -+ id = ResourceLocation.of(criteriaName.substring(index + 1), '.').toString(); -+ } catch (final Exception ex) { -+ type = "_special"; -+ id = criteriaName; -+ } -+ } -+ } else { -+ type = null; -+ id = null; -+ } -+ -+ if (type != null && id != null) { -+ final MapType criteriaType = Types.NBT.createEmptyMap(); -+ data.setMap("CriteriaType", criteriaType); -+ -+ criteriaType.setString("type", type); -+ criteriaType.setString("id", id); -+ } -+ -+ return null; -+ } -+ -+ @Override -+ public MapType postHook(final MapType data, final long fromVersion, final long toVersion) { -+ // repack -+ final MapType criteriaType = data.getMap("CriteriaType"); -+ -+ final String newName; -+ if (criteriaType == null) { -+ newName = null; -+ } else { -+ final String type = criteriaType.getString("type"); -+ final String id = criteriaType.getString("id"); -+ if (type != null && id != null) { -+ if ("_special".equals(type)) { -+ newName = id; -+ } else { -+ newName = packWithDot(type) + ":" + packWithDot(id); -+ } -+ } else { -+ newName = null; -+ } -+ } -+ -+ if (newName != null) { -+ data.remove("CriteriaType"); -+ data.setString("CriteriaName", newName); -+ } -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.OBJECTIVE.addStructureWalker(VERSION, 6, (final MapType data, final long fromVersion, final long toVersion) -> { -+ final MapType criteriaType = data.getMap("CriteriaType"); -+ if (criteriaType == null) { -+ return null; -+ } -+ -+ final String type = criteriaType.getString("type"); -+ -+ if (type == null) { -+ return null; -+ } -+ -+ switch (type) { -+ case "minecraft:mined": { -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, criteriaType, "id", fromVersion, toVersion); -+ break; -+ } -+ -+ case "minecraft:crafted": -+ case "minecraft:used": -+ case "minecraft:broken": -+ case "minecraft:picked_up": -+ case "minecraft:dropped": { -+ WalkerUtils.convert(MCTypeRegistry.ITEM_NAME, criteriaType, "id", fromVersion, toVersion); -+ break; -+ } -+ -+ case "minecraft:killed": -+ case "minecraft:killed_by": { -+ WalkerUtils.convert(MCTypeRegistry.ENTITY_NAME, criteriaType, "id", fromVersion, toVersion); -+ break; -+ } -+ } -+ -+ return null; -+ }); -+ -+ -+ // V7 -+ MCTypeRegistry.STRUCTURE_FEATURE.addStructureConverter(new DataConverter<>(VERSION, 7) { -+ private static void convertToBlockState(final MapType data, final String path) { -+ final Number number = data.getNumber(path); -+ if (number == null) { -+ return; -+ } -+ -+ data.setMap(path, HelperBlockFlatteningV1450.getNBTForId(number.intValue() << 4).copy()); // copy to avoid problems with later state datafixers -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final ListType children = data.getList("Children", ObjectType.MAP); -+ if (children == null) { -+ return null; -+ } -+ -+ for (int i = 0, len = children.size(); i < len; ++i) { -+ final MapType child = children.getMap(i); -+ -+ final String id = child.getString("id"); -+ -+ switch (id) { -+ case "ViF": -+ convertToBlockState(child, "CA"); -+ convertToBlockState(child, "CB"); -+ break; -+ case "ViDF": -+ convertToBlockState(child, "CA"); -+ convertToBlockState(child, "CB"); -+ convertToBlockState(child, "CC"); -+ convertToBlockState(child, "CD"); -+ break; -+ } -+ } -+ -+ return null; -+ } -+ }); -+ -+ // convert villagers to trade with pumpkins and not the carved pumpkin -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:villager", new DataConverter<>(VERSION, 7) { -+ private static void convertPumpkin(final MapType data, final String path) { -+ final MapType item = data.getMap(path); -+ if (item == null) { -+ return; -+ } -+ -+ final String id = item.getString("id"); -+ -+ if (id.equals("minecraft:carved_pumpkin")) { -+ item.setString("id", "minecraft:pumpkin"); -+ } -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType offers = data.getMap("Offers"); -+ if (offers != null) { -+ final ListType recipes = offers.getList("Recipes", ObjectType.MAP); -+ if (recipes != null) { -+ for (int i = 0, len = recipes.size(); i < len; ++i) { -+ final MapType recipe = recipes.getMap(i); -+ -+ convertPumpkin(recipe, "buy"); -+ convertPumpkin(recipe, "buyB"); -+ convertPumpkin(recipe, "sell"); -+ } -+ } -+ } -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.STRUCTURE_FEATURE.addStructureWalker(VERSION, 7, (final MapType data, final long fromVersion, final long toVersion) -> { -+ final ListType list = data.getList("Children", ObjectType.MAP); -+ if (list == null) { -+ return null; -+ } -+ -+ for (int i = 0, len = list.size(); i < len; ++i) { -+ final MapType child = list.getMap(i); -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_STATE, child, "CA", fromVersion, toVersion); -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_STATE, child, "CB", fromVersion, toVersion); -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_STATE, child, "CC", fromVersion, toVersion); -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_STATE, child, "CD", fromVersion, toVersion); -+ } -+ -+ return null; -+ }); -+ } -+ -+ private V1451() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1456.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1456.java -new file mode 100644 -index 0000000000000000000000000000000000000000..47682ffbc10805a4cba73dca43198e52c0ce63df ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1456.java -@@ -0,0 +1,37 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V1456 { -+ -+ private static final int VERSION = MCVersions.V17W49B + 1; -+ -+ private static byte direction2dTo3d(final byte old) { -+ switch (old) { -+ case 0: -+ return 3; -+ case 1: -+ return 4; -+ case 2: -+ default: -+ return 2; -+ case 3: -+ return 5; -+ } -+ } -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:item_frame", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ data.setByte("Facing", direction2dTo3d(data.getByte("Facing"))); -+ return null; -+ } -+ }); -+ } -+ -+ private V1456() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1458.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1458.java -new file mode 100644 -index 0000000000000000000000000000000000000000..95822caa64d6c8a780bb120bedd2728355d26b84 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1458.java -@@ -0,0 +1,87 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.util.ComponentUtils; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V1458 { -+ -+ private static final int VERSION = MCVersions.V17W50A + 1; -+ -+ public static MapType updateCustomName(final MapType data) { -+ final String customName = data.getString("CustomName", ""); -+ -+ if (customName.isEmpty()) { -+ data.remove("CustomName"); -+ } else { -+ data.setString("CustomName", ComponentUtils.createPlainTextComponent(customName)); -+ } -+ -+ return null; -+ } -+ -+ public static void register() { -+ // From CB -+ MCTypeRegistry.PLAYER.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ return updateCustomName(data); -+ } -+ }); -+ -+ MCTypeRegistry.ENTITY.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if ("minecraft:commandblock_minecart".equals(data.getString("id"))) { -+ return null; -+ } -+ -+ return updateCustomName(data); -+ } -+ }); -+ -+ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType tag = data.getMap("tag"); -+ if (tag == null) { -+ return null; -+ } -+ -+ final MapType display = tag.getMap("display"); -+ if (display == null) { -+ return null; -+ } -+ -+ final String name = display.getString("Name"); -+ if (name != null) { -+ display.setString("Name", ComponentUtils.createPlainTextComponent(name)); -+ } /* In 1.20.5, Mojang removed this branch (ItemCustomNameToComponentFix) */ /*else { -+ final String localisedName = display.getString("LocName"); -+ if (localisedName != null) { -+ display.setString("Name", ComponentUtils.createTranslatableComponent(localisedName)); -+ display.remove("LocName"); -+ } -+ }*/ -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.TILE_ENTITY.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if ("minecraft:command_block".equals(data.getString("id"))) { -+ return null; -+ } -+ -+ return updateCustomName(data); -+ } -+ }); -+ -+ } -+ -+ private V1458() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1460.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1460.java -new file mode 100644 -index 0000000000000000000000000000000000000000..9933c54eba686ba851e0d2112c7a22a09544c2c2 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1460.java -@@ -0,0 +1,44 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.block_name.DataWalkerBlockNames; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+import ca.spottedleaf.dataconverter.types.MapType; -+import net.minecraft.resources.ResourceLocation; -+import java.util.HashMap; -+import java.util.Locale; -+import java.util.Map; -+ -+public final class V1460 { -+ -+ private static final Map MOTIVE_REMAP = new HashMap<>(); -+ -+ static { -+ MOTIVE_REMAP.put("donkeykong", "donkey_kong"); -+ MOTIVE_REMAP.put("burningskull", "burning_skull"); -+ MOTIVE_REMAP.put("skullandroses", "skull_and_roses"); -+ } -+ -+ private static final int VERSION = MCVersions.V18W01A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:painting", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ String motive = data.getString("Motive"); -+ if (motive != null) { -+ motive = motive.toLowerCase(Locale.ROOT); -+ data.setString("Motive", new ResourceLocation(MOTIVE_REMAP.getOrDefault(motive, motive)).toString()); -+ } -+ return null; -+ } -+ }); -+ -+ // No idea why so many type redefines exist here in Vanilla. nothing about the data structure changed, it's literally a copy of -+ // the existing types. -+ } -+ -+ private V1460() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1466.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1466.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d870aaca4ff623c71604f889c2e667bfe50fe696 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1466.java -@@ -0,0 +1,142 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.Types; -+ -+public final class V1466 { -+ -+ private static final int VERSION = MCVersions.V18W06A; -+ -+ private static short packOffsetCoordinates(final int x, final int y, final int z) { -+ return (short)((x & 15) | ((y & 15) << 4) | ((z & 15) << 8)); -+ } -+ -+ public static void register() { -+ // There is a rather critical change I've made to this converter: changing the chunk status determination. -+ // In Vanilla, this is determined by whether the terrain has been populated and whether the chunk is lit. -+ // For reference, here is the full status progression (at the time of 18w06a): -+ // empty -> base -> carved -> decorated -> lighted -> mobs_spawned -> finalized -> fullchunk -> postprocessed -+ // So one of those must be picked. -+ // If the chunk is lit and terrain is populated, the Vanilla converter will set the status to "mobs_spawned." -+ // If it is anything else, it will be "empty" -+ // I've changed it to the following: if terrain is populated, it is set to at least decorated. If it is populated -+ // and lit, it is set to "mobs_spawned" -+ // But what if it is not populated? If it is not populated, ignore the lit field - obviously that's just broken. -+ // It can't be lit and not populated. -+ // Let's take a look at chunk generation logic for a chunk that is not populated, or even near a populated chunk. -+ // It actually will generate a chunk up to the "carved" stage. It generates the base terrain, (i.e using noise -+ // to figure out where stone is, dirt, grass) and it will generate caves. Nothing else though. No populators. -+ // So "carved" is the correct stage to use, not empty. Setting it to empty would clobber chunk data, when we don't -+ // need to. If it is populated, at least set it to decorated. If it is lit and populated, set it to mobs_spawned. Else, -+ // it is carved. -+ // This change also fixes the random light check "bug" (really this is Mojang's fault for fucking up the status conversion here) -+ // caused by spigot, which would not set the lit value for some chunks. Now those chunks will not be regenerated. -+ -+ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType level = data.getMap("Level"); -+ if (level == null) { -+ return null; -+ } -+ -+ final boolean terrainPopulated = level.getBoolean("TerrainPopulated"); -+ final boolean lightPopulated = level.getBoolean("LightPopulated") || level.getNumber("LightPopulated") == null; -+ final String newStatus = !terrainPopulated ? "carved" : (lightPopulated ? "mobs_spawned" : "decorated"); -+ -+ level.setString("Status", newStatus); -+ level.setBoolean("hasLegacyStructureData", true); -+ -+ // convert biome byte[] into int[] -+ final byte[] biomes = level.getBytes("Biomes"); -+ if (biomes != null) { -+ final int[] newBiomes = new int[256]; -+ for (int i = 0, len = Math.min(newBiomes.length, biomes.length); i < len; ++i) { -+ newBiomes[i] = biomes[i] & 255; -+ } -+ level.setInts("Biomes", newBiomes); -+ } -+ -+ // ProtoChunks have their own dedicated tick list, so we must convert the TileTicks to that. -+ final ListType ticks = level.getList("TileTicks", ObjectType.MAP); -+ if (ticks != null) { -+ final ListType sections = Types.NBT.createEmptyList(); -+ final ListType[] sectionAccess = new ListType[16]; -+ for (int i = 0; i < sectionAccess.length; ++i) { -+ sections.addList(sectionAccess[i] = Types.NBT.createEmptyList()); -+ } -+ level.setList("ToBeTicked", sections); -+ -+ for (int i = 0, len = ticks.size(); i < len; ++i) { -+ final MapType tick = ticks.getMap(i); -+ -+ final int x = tick.getInt("x"); -+ final int y = tick.getInt("y"); -+ final int z = tick.getInt("z"); -+ final short coordinate = packOffsetCoordinates(x, y, z); -+ -+ sectionAccess[y >> 4].addShort(coordinate); -+ } -+ } -+ -+ return null; -+ } -+ }); -+ -+ -+ MCTypeRegistry.CHUNK.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ final MapType level = data.getMap("Level"); -+ if (level == null) { -+ return null; -+ } -+ -+ WalkerUtils.convertList(MCTypeRegistry.ENTITY, level, "Entities", fromVersion, toVersion); -+ WalkerUtils.convertList(MCTypeRegistry.TILE_ENTITY, level, "TileEntities", fromVersion, toVersion); -+ -+ final ListType tileTicks = level.getList("TileTicks", ObjectType.MAP); -+ if (tileTicks != null) { -+ for (int i = 0, len = tileTicks.size(); i < len; ++i) { -+ final MapType tileTick = tileTicks.getMap(i); -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, tileTick, "i", fromVersion, toVersion); -+ } -+ } -+ -+ final ListType sections = level.getList("Sections", ObjectType.MAP); -+ if (sections != null) { -+ for (int i = 0, len = sections.size(); i < len; ++i) { -+ final MapType section = sections.getMap(i); -+ WalkerUtils.convertList(MCTypeRegistry.BLOCK_STATE, section, "Palette", fromVersion, toVersion); -+ } -+ } -+ -+ WalkerUtils.convertValues(MCTypeRegistry.STRUCTURE_FEATURE, level.getMap("Structures"), "Starts", fromVersion, toVersion); -+ -+ return null; -+ }); -+ MCTypeRegistry.STRUCTURE_FEATURE.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ final ListType list = data.getList("Children", ObjectType.MAP); -+ if (list != null) { -+ for (int i = 0, len = list.size(); i < len; ++i) { -+ final MapType child = list.getMap(i); -+ -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_STATE, child, "CA", fromVersion, toVersion); -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_STATE, child, "CB", fromVersion, toVersion); -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_STATE, child, "CC", fromVersion, toVersion); -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_STATE, child, "CD", fromVersion, toVersion); -+ } -+ } -+ -+ WalkerUtils.convert(MCTypeRegistry.BIOME, data, "biome", fromVersion, toVersion); -+ -+ return null; -+ }); -+ } -+ -+ private V1466() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V147.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V147.java -new file mode 100644 -index 0000000000000000000000000000000000000000..af9c6ee26580eb10bf8426f5b61c26df63a910a6 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V147.java -@@ -0,0 +1,26 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V147 { -+ -+ private static final int VERSION = MCVersions.V15W46A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("ArmorStand", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (data.getBoolean("Silent") && !data.getBoolean("Marker")) { -+ data.remove("Silent"); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V147() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1470.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1470.java -new file mode 100644 -index 0000000000000000000000000000000000000000..2bf1baee2321b3cb584ab6355f43263d6c8ec0be ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1470.java -@@ -0,0 +1,31 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; -+ -+public final class V1470 { -+ -+ private static final int VERSION = MCVersions.V18W08A; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ registerMob("minecraft:turtle"); -+ registerMob("minecraft:cod_mob"); -+ registerMob("minecraft:tropical_fish"); -+ registerMob("minecraft:salmon_mob"); -+ registerMob("minecraft:puffer_fish"); -+ registerMob("minecraft:phantom"); -+ registerMob("minecraft:dolphin"); -+ registerMob("minecraft:drowned"); -+ -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:trident", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "inBlockState")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:trident", new DataWalkerItems("Trident")); -+ } -+ -+ private V1470() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1474.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1474.java -new file mode 100644 -index 0000000000000000000000000000000000000000..99f0f34cc14639ed8ed73b847f74cdc607607af8 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1474.java -@@ -0,0 +1,34 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V1474 { -+ -+ private static final int VERSION = MCVersions.V18W10B; -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:shulker", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (data.getInt("Color") == 10) { -+ data.setByte("Color", (byte)16); -+ } -+ return null; -+ } -+ }); -+ // data hooks ensure the inputs are namespaced -+ ConverterAbstractBlockRename.register(VERSION, (final String old) -> { -+ return "minecraft:purple_shulker_box".equals(old) ? "minecraft:shulker_box" : null; -+ }); -+ ConverterAbstractItemRename.register(VERSION, (final String old) -> { -+ return "minecraft:purple_shulker_box".equals(old) ? "minecraft:shulker_box" : null; -+ }); -+ } -+ -+ private V1474() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1475.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1475.java -new file mode 100644 -index 0000000000000000000000000000000000000000..2ae50eea847671f3995688901c79caf520440d7a ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1475.java -@@ -0,0 +1,22 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+ -+public final class V1475 { -+ -+ private static final int VERSION = MCVersions.V18W10B + 1; -+ -+ public static void register() { -+ ConverterAbstractBlockRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:flowing_water", "minecraft:water", -+ "minecraft:flowing_lava", "minecraft:lava" -+ ) -+ )::get); -+ } -+ -+ private V1475() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1480.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1480.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7180c1168bffb9fe70d18fe7414a5372518413a8 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1480.java -@@ -0,0 +1,45 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V1480 { -+ -+ private static final int VERSION = MCVersions.V18W14A + 1; -+ -+ public static final Map RENAMED_IDS = new HashMap<>( -+ ImmutableMap.builder() -+ .put("minecraft:blue_coral", "minecraft:tube_coral_block") -+ .put("minecraft:pink_coral", "minecraft:brain_coral_block") -+ .put("minecraft:purple_coral", "minecraft:bubble_coral_block") -+ .put("minecraft:red_coral", "minecraft:fire_coral_block") -+ .put("minecraft:yellow_coral", "minecraft:horn_coral_block") -+ .put("minecraft:blue_coral_plant", "minecraft:tube_coral") -+ .put("minecraft:pink_coral_plant", "minecraft:brain_coral") -+ .put("minecraft:purple_coral_plant", "minecraft:bubble_coral") -+ .put("minecraft:red_coral_plant", "minecraft:fire_coral") -+ .put("minecraft:yellow_coral_plant", "minecraft:horn_coral") -+ .put("minecraft:blue_coral_fan", "minecraft:tube_coral_fan") -+ .put("minecraft:pink_coral_fan", "minecraft:brain_coral_fan") -+ .put("minecraft:purple_coral_fan", "minecraft:bubble_coral_fan") -+ .put("minecraft:red_coral_fan", "minecraft:fire_coral_fan") -+ .put("minecraft:yellow_coral_fan", "minecraft:horn_coral_fan") -+ .put("minecraft:blue_dead_coral", "minecraft:dead_tube_coral") -+ .put("minecraft:pink_dead_coral", "minecraft:dead_brain_coral") -+ .put("minecraft:purple_dead_coral", "minecraft:dead_bubble_coral") -+ .put("minecraft:red_dead_coral", "minecraft:dead_fire_coral") -+ .put("minecraft:yellow_dead_coral", "minecraft:dead_horn_coral") -+ .build() -+ ); -+ -+ public static void register() { -+ ConverterAbstractBlockRename.register(VERSION, RENAMED_IDS::get); -+ ConverterAbstractItemRename.register(VERSION, RENAMED_IDS::get); -+ } -+ -+ private V1480() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1483.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1483.java -new file mode 100644 -index 0000000000000000000000000000000000000000..56d9babebba8b8ba6be07ea413e9c04ffea84023 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1483.java -@@ -0,0 +1,30 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterAbstractEntityRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+ -+public final class V1483 { -+ -+ private static final int VERSION = MCVersions.V18W16A; -+ -+ public static void register() { -+ ConverterAbstractEntityRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:puffer_fish", "minecraft:pufferfish" -+ ) -+ )::get); -+ ConverterAbstractItemRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:puffer_fish_spawn_egg", "minecraft:pufferfish_spawn_egg" -+ ) -+ )::get); -+ -+ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:puffer_fish", "minecraft:pufferfish"); -+ } -+ -+ private V1483() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1484.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1484.java -new file mode 100644 -index 0000000000000000000000000000000000000000..cdbb9379f66aa6edc05c5e6cb2bdeae97f1ea38b ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1484.java -@@ -0,0 +1,75 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V1484 { -+ -+ private static final int VERSION = MCVersions.V18W19A; -+ -+ public static void register() { -+ final Map renamed = new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:sea_grass", "minecraft:seagrass", -+ "minecraft:tall_sea_grass", "minecraft:tall_seagrass" -+ ) -+ ); -+ -+ ConverterAbstractItemRename.register(VERSION, renamed::get); -+ ConverterAbstractBlockRename.register(VERSION, renamed::get); -+ -+ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType level = data.getMap("Level"); -+ -+ if (level == null) { -+ return null; -+ } -+ -+ final MapType heightmaps = level.getMap("Heightmaps"); -+ -+ if (heightmaps == null) { -+ return null; -+ } -+ -+ final Object liquid = heightmaps.getGeneric("LIQUID"); -+ if (liquid != null) { -+ heightmaps.remove("LIQUID"); -+ heightmaps.setGeneric("WORLD_SURFACE_WG", liquid); -+ } -+ -+ final Object solid = heightmaps.getGeneric("SOLID"); -+ if (solid != null) { -+ heightmaps.remove("SOLID"); -+ heightmaps.setGeneric("OCEAN_FLOOR_WG", solid); -+ heightmaps.setGeneric("OCEAN_FLOOR", solid); -+ } -+ -+ final Object light = heightmaps.getGeneric("LIGHT"); -+ if (light != null) { -+ heightmaps.remove("LIGHT"); -+ heightmaps.setGeneric("LIGHT_BLOCKING", light); -+ } -+ -+ final Object rain = heightmaps.getGeneric("RAIN"); -+ if (rain != null) { -+ heightmaps.remove("RAIN"); -+ heightmaps.setGeneric("MOTION_BLOCKING", rain); -+ heightmaps.setGeneric("MOTION_BLOCKING_NO_LEAVES", rain); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V1484() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1486.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1486.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a9e42da41064ea293a71dbf2d681a857b2e1812e ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1486.java -@@ -0,0 +1,39 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterAbstractEntityRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V1486 { -+ -+ private static final int VERSION = MCVersions.V18W19B + 1; -+ -+ public static final Map RENAMED_ENTITY_IDS = new HashMap<>( -+ ImmutableMap.builder() -+ .put("minecraft:salmon_mob", "minecraft:salmon") -+ .put("minecraft:cod_mob", "minecraft:cod") -+ .build() -+ ); -+ public static final Map RENAMED_ITEM_IDS = new HashMap<>( -+ ImmutableMap.builder() -+ .put("minecraft:salmon_mob_spawn_egg", "minecraft:salmon_spawn_egg") -+ .put("minecraft:cod_mob_spawn_egg", "minecraft:cod_spawn_egg") -+ .build() -+ ); -+ -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:cod_mob", "minecraft:cod"); -+ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:salmon_mob", "minecraft:salmon"); -+ -+ ConverterAbstractEntityRename.register(VERSION, RENAMED_ENTITY_IDS::get); -+ ConverterAbstractItemRename.register(VERSION, RENAMED_ITEM_IDS::get); -+ } -+ -+ private V1486() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1487.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1487.java -new file mode 100644 -index 0000000000000000000000000000000000000000..884049818efdf273443fb3d1c2d7250564fbdbf7 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1487.java -@@ -0,0 +1,27 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V1487 { -+ -+ private static final int VERSION = MCVersions.V18W19B + 2; -+ -+ public static void register() { -+ final Map remap = new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:prismarine_bricks_slab", "minecraft:prismarine_brick_slab", -+ "minecraft:prismarine_bricks_stairs", "minecraft:prismarine_brick_stairs" -+ ) -+ ); -+ -+ ConverterAbstractItemRename.register(VERSION, remap::get); -+ ConverterAbstractBlockRename.register(VERSION, remap::get); -+ } -+ -+ private V1487() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1488.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1488.java -new file mode 100644 -index 0000000000000000000000000000000000000000..907a5e2a26ee046e292508e1f06d5f26d10af8c1 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1488.java -@@ -0,0 +1,93 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import com.google.common.collect.ImmutableMap; -+ -+import java.util.HashMap; -+ -+public final class V1488 { -+ -+ private static final int VERSION = MCVersions.V18W19B + 3; -+ -+ private static boolean isIglooPiece(final MapType piece) { -+ return "Iglu".equals(piece.getString("id")); -+ } -+ -+ public static void register() { -+ ConverterAbstractBlockRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:kelp_top", "minecraft:kelp", -+ "minecraft:kelp", "minecraft:kelp_plant" -+ ) -+ )::get); -+ ConverterAbstractItemRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:kelp_top", "minecraft:kelp" -+ ) -+ )::get); -+ -+ // Don't ask me why in V1458 they wrote the converter to NOT do command blocks and THEN in THIS version -+ // to ONLY do command blocks. I don't know. -+ -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:command_block", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ return V1458.updateCustomName(data); -+ } -+ }); -+ -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:commandblock_minecart", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ return V1458.updateCustomName(data); -+ } -+ }); -+ -+ MCTypeRegistry.STRUCTURE_FEATURE.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final ListType children = data.getList("Children", ObjectType.MAP); -+ boolean isIgloo; -+ if (children != null) { -+ isIgloo = true; -+ for (int i = 0, len = children.size(); i < len; ++i) { -+ if (!isIglooPiece(children.getMap(i))) { -+ isIgloo = false; -+ break; -+ } -+ } -+ } else { -+ isIgloo = false; -+ } -+ -+ if (isIgloo) { -+ data.remove("Children"); -+ data.setString("id", "Igloo"); -+ return null; -+ } -+ -+ if (children != null) { -+ for (int i = 0; i < children.size();) { -+ final MapType child = children.getMap(i); -+ if (isIglooPiece(child)) { -+ children.remove(i); -+ continue; -+ } -+ ++i; -+ } -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V1488() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1490.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1490.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1e99de15732bdd283835a9531f76e29ddab91f46 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1490.java -@@ -0,0 +1,30 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import com.google.common.collect.ImmutableMap; -+ -+import java.util.HashMap; -+ -+public final class V1490 { -+ -+ private static final int VERSION = MCVersions.V18W20A + 1; -+ -+ public static void register() { -+ ConverterAbstractBlockRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:melon_block", "minecraft:melon" -+ ) -+ )::get); -+ ConverterAbstractItemRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:melon_block", "minecraft:melon", -+ "minecraft:melon", "minecraft:melon_slice", -+ "minecraft:speckled_melon", "minecraft:glistering_melon_slice" -+ ) -+ )::get); -+ } -+ -+ private V1490() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1492.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1492.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1259216b43434d0f7c7be10a081fd05057c253cf ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1492.java -@@ -0,0 +1,151 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import com.google.common.collect.ImmutableMap; -+import com.mojang.datafixers.util.Pair; -+ -+public final class V1492 { -+ -+ private static final ImmutableMap>> RENAMES = ImmutableMap.>>builder() -+ .put("EndCity", Pair.of( -+ "ECP", -+ ImmutableMap.builder() -+ .put("second_floor", "second_floor_1") -+ .put("third_floor", "third_floor_1") -+ .put("third_floor_c", "third_floor_2") -+ .build() -+ ) -+ ) -+ -+ .put("Mansion", Pair.of( -+ "WMP", -+ ImmutableMap.builder() -+ .put("carpet_south", "carpet_south_1") -+ .put("carpet_west", "carpet_west_1") -+ .put("indoors_door", "indoors_door_1") -+ .put("indoors_wall", "indoors_wall_1") -+ .build() -+ ) -+ ) -+ -+ .put("Igloo", Pair.of( -+ "Iglu", -+ ImmutableMap.builder() -+ .put("minecraft:igloo/igloo_bottom", "minecraft:igloo/bottom") -+ .put("minecraft:igloo/igloo_middle", "minecraft:igloo/middle") -+ .put("minecraft:igloo/igloo_top", "minecraft:igloo/top") -+ .build() -+ ) -+ ) -+ .put("Ocean_Ruin", Pair.of( -+ "ORP", -+ ImmutableMap.builder() -+ .put("minecraft:ruin/big_ruin1_brick", "minecraft:underwater_ruin/big_brick_1") -+ .put("minecraft:ruin/big_ruin2_brick", "minecraft:underwater_ruin/big_brick_2") -+ .put("minecraft:ruin/big_ruin3_brick", "minecraft:underwater_ruin/big_brick_3") -+ .put("minecraft:ruin/big_ruin8_brick", "minecraft:underwater_ruin/big_brick_8") -+ .put("minecraft:ruin/big_ruin1_cracked", "minecraft:underwater_ruin/big_cracked_1") -+ .put("minecraft:ruin/big_ruin2_cracked", "minecraft:underwater_ruin/big_cracked_2") -+ .put("minecraft:ruin/big_ruin3_cracked", "minecraft:underwater_ruin/big_cracked_3") -+ .put("minecraft:ruin/big_ruin8_cracked", "minecraft:underwater_ruin/big_cracked_8") -+ .put("minecraft:ruin/big_ruin1_mossy", "minecraft:underwater_ruin/big_mossy_1") -+ .put("minecraft:ruin/big_ruin2_mossy", "minecraft:underwater_ruin/big_mossy_2") -+ .put("minecraft:ruin/big_ruin3_mossy", "minecraft:underwater_ruin/big_mossy_3") -+ .put("minecraft:ruin/big_ruin8_mossy", "minecraft:underwater_ruin/big_mossy_8") -+ .put("minecraft:ruin/big_ruin_warm4", "minecraft:underwater_ruin/big_warm_4") -+ .put("minecraft:ruin/big_ruin_warm5", "minecraft:underwater_ruin/big_warm_5") -+ .put("minecraft:ruin/big_ruin_warm6", "minecraft:underwater_ruin/big_warm_6") -+ .put("minecraft:ruin/big_ruin_warm7", "minecraft:underwater_ruin/big_warm_7") -+ .put("minecraft:ruin/ruin1_brick", "minecraft:underwater_ruin/brick_1") -+ .put("minecraft:ruin/ruin2_brick", "minecraft:underwater_ruin/brick_2") -+ .put("minecraft:ruin/ruin3_brick", "minecraft:underwater_ruin/brick_3") -+ .put("minecraft:ruin/ruin4_brick", "minecraft:underwater_ruin/brick_4") -+ .put("minecraft:ruin/ruin5_brick", "minecraft:underwater_ruin/brick_5") -+ .put("minecraft:ruin/ruin6_brick", "minecraft:underwater_ruin/brick_6") -+ .put("minecraft:ruin/ruin7_brick", "minecraft:underwater_ruin/brick_7") -+ .put("minecraft:ruin/ruin8_brick", "minecraft:underwater_ruin/brick_8") -+ .put("minecraft:ruin/ruin1_cracked", "minecraft:underwater_ruin/cracked_1") -+ .put("minecraft:ruin/ruin2_cracked", "minecraft:underwater_ruin/cracked_2") -+ .put("minecraft:ruin/ruin3_cracked", "minecraft:underwater_ruin/cracked_3") -+ .put("minecraft:ruin/ruin4_cracked", "minecraft:underwater_ruin/cracked_4") -+ .put("minecraft:ruin/ruin5_cracked", "minecraft:underwater_ruin/cracked_5") -+ .put("minecraft:ruin/ruin6_cracked", "minecraft:underwater_ruin/cracked_6") -+ .put("minecraft:ruin/ruin7_cracked", "minecraft:underwater_ruin/cracked_7") -+ .put("minecraft:ruin/ruin8_cracked", "minecraft:underwater_ruin/cracked_8") -+ .put("minecraft:ruin/ruin1_mossy", "minecraft:underwater_ruin/mossy_1") -+ .put("minecraft:ruin/ruin2_mossy", "minecraft:underwater_ruin/mossy_2") -+ .put("minecraft:ruin/ruin3_mossy", "minecraft:underwater_ruin/mossy_3") -+ .put("minecraft:ruin/ruin4_mossy", "minecraft:underwater_ruin/mossy_4") -+ .put("minecraft:ruin/ruin5_mossy", "minecraft:underwater_ruin/mossy_5") -+ .put("minecraft:ruin/ruin6_mossy", "minecraft:underwater_ruin/mossy_6") -+ .put("minecraft:ruin/ruin7_mossy", "minecraft:underwater_ruin/mossy_7") -+ .put("minecraft:ruin/ruin8_mossy", "minecraft:underwater_ruin/mossy_8") -+ .put("minecraft:ruin/ruin_warm1", "minecraft:underwater_ruin/warm_1") -+ .put("minecraft:ruin/ruin_warm2", "minecraft:underwater_ruin/warm_2") -+ .put("minecraft:ruin/ruin_warm3", "minecraft:underwater_ruin/warm_3") -+ .put("minecraft:ruin/ruin_warm4", "minecraft:underwater_ruin/warm_4") -+ .put("minecraft:ruin/ruin_warm5", "minecraft:underwater_ruin/warm_5") -+ .put("minecraft:ruin/ruin_warm6", "minecraft:underwater_ruin/warm_6") -+ .put("minecraft:ruin/ruin_warm7", "minecraft:underwater_ruin/warm_7") -+ .put("minecraft:ruin/ruin_warm8", "minecraft:underwater_ruin/warm_8") -+ .put("minecraft:ruin/big_brick_1", "minecraft:underwater_ruin/big_brick_1") -+ .put("minecraft:ruin/big_brick_2", "minecraft:underwater_ruin/big_brick_2") -+ .put("minecraft:ruin/big_brick_3", "minecraft:underwater_ruin/big_brick_3") -+ .put("minecraft:ruin/big_brick_8", "minecraft:underwater_ruin/big_brick_8") -+ .put("minecraft:ruin/big_mossy_1", "minecraft:underwater_ruin/big_mossy_1") -+ .put("minecraft:ruin/big_mossy_2", "minecraft:underwater_ruin/big_mossy_2") -+ .put("minecraft:ruin/big_mossy_3", "minecraft:underwater_ruin/big_mossy_3") -+ .put("minecraft:ruin/big_mossy_8", "minecraft:underwater_ruin/big_mossy_8") -+ .put("minecraft:ruin/big_cracked_1", "minecraft:underwater_ruin/big_cracked_1") -+ .put("minecraft:ruin/big_cracked_2", "minecraft:underwater_ruin/big_cracked_2") -+ .put("minecraft:ruin/big_cracked_3", "minecraft:underwater_ruin/big_cracked_3") -+ .put("minecraft:ruin/big_cracked_8", "minecraft:underwater_ruin/big_cracked_8") -+ .put("minecraft:ruin/big_warm_4", "minecraft:underwater_ruin/big_warm_4") -+ .put("minecraft:ruin/big_warm_5", "minecraft:underwater_ruin/big_warm_5") -+ .put("minecraft:ruin/big_warm_6", "minecraft:underwater_ruin/big_warm_6") -+ .put("minecraft:ruin/big_warm_7", "minecraft:underwater_ruin/big_warm_7") -+ .build() -+ ) -+ ) -+ -+ .build(); -+ -+ private static final int VERSION = MCVersions.V18W20B + 1; -+ -+ public static void register() { -+ MCTypeRegistry.STRUCTURE_FEATURE.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final ListType children = data.getList("Children", ObjectType.MAP); -+ if (children == null) { -+ return null; -+ } -+ -+ final String id = data.getString("id"); -+ -+ final Pair> renames = RENAMES.get(id); -+ if (renames == null) { -+ return null; -+ } -+ -+ for (int i = 0, len = children.size(); i < len; ++i) { -+ final MapType child = children.getMap(i); -+ -+ if (renames.getFirst().equals(child.getString("id"))) { -+ final String template = child.getString("Template", ""); -+ child.setString("Template", renames.getSecond().getOrDefault(template, template)); -+ } -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V1492() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1494.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1494.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b72fe109aa8c60425c00aad234d60a1c70dda60b ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1494.java -@@ -0,0 +1,88 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -+ -+public final class V1494 { -+ -+ private static final int VERSION = MCVersions.V18W20C + 1; -+ -+ private static final Int2ObjectOpenHashMap ENCH_ID_TO_NAME = new Int2ObjectOpenHashMap<>(); -+ static { -+ ENCH_ID_TO_NAME.put(0, "minecraft:protection"); -+ ENCH_ID_TO_NAME.put(1, "minecraft:fire_protection"); -+ ENCH_ID_TO_NAME.put(2, "minecraft:feather_falling"); -+ ENCH_ID_TO_NAME.put(3, "minecraft:blast_protection"); -+ ENCH_ID_TO_NAME.put(4, "minecraft:projectile_protection"); -+ ENCH_ID_TO_NAME.put(5, "minecraft:respiration"); -+ ENCH_ID_TO_NAME.put(6, "minecraft:aqua_affinity"); -+ ENCH_ID_TO_NAME.put(7, "minecraft:thorns"); -+ ENCH_ID_TO_NAME.put(8, "minecraft:depth_strider"); -+ ENCH_ID_TO_NAME.put(9, "minecraft:frost_walker"); -+ ENCH_ID_TO_NAME.put(10, "minecraft:binding_curse"); -+ ENCH_ID_TO_NAME.put(16, "minecraft:sharpness"); -+ ENCH_ID_TO_NAME.put(17, "minecraft:smite"); -+ ENCH_ID_TO_NAME.put(18, "minecraft:bane_of_arthropods"); -+ ENCH_ID_TO_NAME.put(19, "minecraft:knockback"); -+ ENCH_ID_TO_NAME.put(20, "minecraft:fire_aspect"); -+ ENCH_ID_TO_NAME.put(21, "minecraft:looting"); -+ ENCH_ID_TO_NAME.put(22, "minecraft:sweeping"); -+ ENCH_ID_TO_NAME.put(32, "minecraft:efficiency"); -+ ENCH_ID_TO_NAME.put(33, "minecraft:silk_touch"); -+ ENCH_ID_TO_NAME.put(34, "minecraft:unbreaking"); -+ ENCH_ID_TO_NAME.put(35, "minecraft:fortune"); -+ ENCH_ID_TO_NAME.put(48, "minecraft:power"); -+ ENCH_ID_TO_NAME.put(49, "minecraft:punch"); -+ ENCH_ID_TO_NAME.put(50, "minecraft:flame"); -+ ENCH_ID_TO_NAME.put(51, "minecraft:infinity"); -+ ENCH_ID_TO_NAME.put(61, "minecraft:luck_of_the_sea"); -+ ENCH_ID_TO_NAME.put(62, "minecraft:lure"); -+ ENCH_ID_TO_NAME.put(65, "minecraft:loyalty"); -+ ENCH_ID_TO_NAME.put(66, "minecraft:impaling"); -+ ENCH_ID_TO_NAME.put(67, "minecraft:riptide"); -+ ENCH_ID_TO_NAME.put(68, "minecraft:channeling"); -+ ENCH_ID_TO_NAME.put(70, "minecraft:mending"); -+ ENCH_ID_TO_NAME.put(71, "minecraft:vanishing_curse"); -+ } -+ -+ public static void register() { -+ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType tag = data.getMap("tag"); -+ if (tag == null) { -+ return null; -+ } -+ -+ final ListType enchants = tag.getList("ench", ObjectType.MAP); -+ if (enchants != null) { -+ tag.remove("ench"); -+ tag.setList("Enchantments", enchants); -+ -+ for (int i = 0, len = enchants.size(); i < len; ++i) { -+ final MapType enchant = enchants.getMap(i); -+ enchant.setString("id", ENCH_ID_TO_NAME.getOrDefault(enchant.getInt("id"), "null")); -+ } -+ } -+ -+ final ListType storedEnchants = tag.getList("StoredEnchantments", ObjectType.MAP); -+ if (storedEnchants != null) { -+ for (int i = 0, len = storedEnchants.size(); i < len; ++i) { -+ final MapType enchant = storedEnchants.getMap(i); -+ enchant.setString("id", ENCH_ID_TO_NAME.getOrDefault(enchant.getInt("id"), "null")); -+ } -+ } -+ -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V1494() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1496.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1496.java -new file mode 100644 -index 0000000000000000000000000000000000000000..10349a70b865b19cca471a16548fd49910a2b0e7 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1496.java -@@ -0,0 +1,370 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.Types; -+import com.mojang.datafixers.DataFixUtils; -+import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; -+import it.unimi.dsi.fastutil.ints.IntIterator; -+import it.unimi.dsi.fastutil.ints.IntOpenHashSet; -+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; -+import net.minecraft.util.datafix.PackedBitStorage; -+import java.util.Arrays; -+import java.util.HashSet; -+import java.util.Set; -+ -+public final class V1496 { -+ -+ private static final int VERSION = MCVersions.V18W21B; -+ -+ private static final int[][] DIRECTIONS = new int[][] { -+ new int[] {-1, 0, 0}, -+ new int[] {1, 0, 0}, -+ new int[] {0, -1, 0}, -+ new int[] {0, 1, 0}, -+ new int[] {0, 0, -1}, -+ new int[] {0, 0, 1} -+ }; -+ -+ private static final Object2IntOpenHashMap LEAVES_TO_ID = new Object2IntOpenHashMap<>(); -+ static { -+ LEAVES_TO_ID.put("minecraft:acacia_leaves", 0); -+ LEAVES_TO_ID.put("minecraft:birch_leaves", 1); -+ LEAVES_TO_ID.put("minecraft:dark_oak_leaves", 2); -+ LEAVES_TO_ID.put("minecraft:jungle_leaves", 3); -+ LEAVES_TO_ID.put("minecraft:oak_leaves", 4); -+ LEAVES_TO_ID.put("minecraft:spruce_leaves", 5); -+ } -+ -+ private static final Set LOGS = new HashSet<>( -+ Arrays.asList( -+ "minecraft:acacia_bark", -+ "minecraft:birch_bark", -+ "minecraft:dark_oak_bark", -+ "minecraft:jungle_bark", -+ "minecraft:oak_bark", -+ "minecraft:spruce_bark", -+ "minecraft:acacia_log", -+ "minecraft:birch_log", -+ "minecraft:dark_oak_log", -+ "minecraft:jungle_log", -+ "minecraft:oak_log", -+ "minecraft:spruce_log", -+ "minecraft:stripped_acacia_log", -+ "minecraft:stripped_birch_log", -+ "minecraft:stripped_dark_oak_log", -+ "minecraft:stripped_jungle_log", -+ "minecraft:stripped_oak_log", -+ "minecraft:stripped_spruce_log" -+ ) -+ ); -+ -+ public static void register() { -+ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType level = data.getMap("Level"); -+ if (level == null) { -+ return null; -+ } -+ -+ final ListType sectionsNBT = level.getList("Sections", ObjectType.MAP); -+ if (sectionsNBT == null) { -+ return null; -+ } -+ -+ int newSides = 0; -+ -+ final LeavesSection[] sections = new LeavesSection[16]; -+ boolean skippable = true; -+ for (int i = 0, len = sectionsNBT.size(); i < len; ++i) { -+ final LeavesSection section = new LeavesSection(sectionsNBT.getMap(i)); -+ sections[section.sectionY] = section; -+ -+ skippable &= section.isSkippable(); -+ } -+ -+ if (skippable) { -+ return null; -+ } -+ -+ final IntOpenHashSet[] positionsByDistance = new IntOpenHashSet[7]; -+ for (int i = 0; i < positionsByDistance.length; ++i) { -+ positionsByDistance[i] = new IntOpenHashSet(); -+ } -+ -+ for (final LeavesSection section : sections) { -+ if (section == null || section.isSkippable()) { -+ continue; -+ } -+ -+ for (int index = 0; index < 4096; ++index) { -+ final int block = section.getBlock(index); -+ if (section.isLog(block)) { -+ positionsByDistance[0].add(section.getSectionY() << 12 | index); -+ } else if (section.isLeaf(block)) { -+ int x = getX(index); -+ int z = getZ(index); -+ newSides |= getSideMask(x == 0, x == 15, z == 0, z == 15); -+ } -+ } -+ } -+ -+ // this is basically supposed to recalculate the distances, because a higher cap was added -+ for (int distance = 1; distance < 7; ++distance) { -+ final IntOpenHashSet positionsLess = positionsByDistance[distance - 1]; -+ final IntOpenHashSet positionsEqual = positionsByDistance[distance]; -+ -+ for (final IntIterator iterator = positionsLess.iterator(); iterator.hasNext();) { -+ final int position = iterator.nextInt(); -+ final int fromX = getX(position); -+ final int fromY = getY(position); -+ final int fromZ = getZ(position); -+ -+ for (final int[] direction : DIRECTIONS) { -+ final int toX = fromX + direction[0]; -+ final int toY = fromY + direction[1]; -+ final int toZ = fromZ + direction[2]; -+ -+ if (!(toX >= 0 && toX <= 15 && toZ >= 0 && toZ <= 15 && toY >= 0 && toY <= 255)) { -+ continue; -+ } -+ -+ final LeavesSection toSection = sections[toY >> 4]; -+ if (toSection == null || toSection.isSkippable()) { -+ continue; -+ } -+ -+ final int sectionLocalIndex = getIndex(toX, toY & 15, toZ); -+ final int toBlock = toSection.getBlock(sectionLocalIndex); -+ -+ if (toSection.isLeaf(toBlock)) { -+ final int newDistance = toSection.getDistance(toBlock); -+ if (newDistance > distance) { -+ toSection.setDistance(sectionLocalIndex, toBlock, distance); -+ positionsEqual.add(getIndex(toX, toY, toZ)); -+ } -+ } -+ } -+ } -+ } -+ -+ // done updating blocks, now just update the blockstates and palette -+ for (int i = 0, len = sectionsNBT.size(); i < len; ++i) { -+ final MapType sectionNBT = sectionsNBT.getMap(i); -+ final int y = sectionNBT.getInt("Y"); -+ final LeavesSection section = sections[y]; -+ -+ section.writeInto(sectionNBT); -+ } -+ -+ // if sides changed during process, update it now -+ if (newSides != 0) { -+ MapType upgradeData = level.getMap("UpgradeData"); -+ if (upgradeData == null) { -+ level.setMap("UpgradeData", upgradeData = Types.NBT.createEmptyMap()); -+ } -+ -+ upgradeData.setByte("Sides", (byte)(upgradeData.getByte("Sides") | newSides)); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ public static int getIndex(final int x, final int y, final int z) { -+ return y << 8 | z << 4 | x; -+ } -+ -+ public static int getX(final int index) { -+ return index & 15; -+ } -+ -+ public static int getY(final int index) { -+ return index >> 8 & 255; -+ } -+ -+ public static int getZ(final int index) { -+ return index >> 4 & 15; -+ } -+ -+ public static int getSideMask(final boolean noLeft, final boolean noRight, final boolean noBack, final boolean noForward) { -+ final int ret; -+ -+ if (noBack) { -+ if (noRight) { -+ ret = 2; -+ } else if (noLeft) { -+ ret = 128; -+ } else { -+ ret = 1; -+ } -+ } else if (noForward) { -+ if (noLeft) { -+ ret = 32; -+ } else if (noRight) { -+ ret = 8; -+ } else { -+ ret = 16; -+ } -+ } else if (noRight) { -+ ret = 4; -+ } else if (noLeft) { -+ ret = 64; -+ } else { -+ ret = 0; -+ } -+ -+ return ret; -+ } -+ -+ private V1496() {} -+ -+ public abstract static class Section { -+ protected final ListType palette; -+ protected final int sectionY; -+ protected PackedBitStorage storage; -+ -+ public Section(final MapType section) { -+ this.palette = section.getList("Palette", ObjectType.MAP); -+ this.sectionY = section.getInt("Y"); -+ this.readStorage(section); -+ } -+ -+ protected void readStorage(final MapType section) { -+ if (this.initSkippable()) { -+ this.storage = null; -+ } else { -+ final long[] states = section.getLongs("BlockStates"); -+ final int bits = Math.max(4, DataFixUtils.ceillog2(this.palette.size())); -+ this.storage = new PackedBitStorage(bits, 4096, states); -+ } -+ } -+ -+ public void writeInto(final MapType section) { -+ if (this.isSkippable()) { -+ return; -+ } -+ -+ section.setList("Palette", this.palette); -+ section.setLongs("BlockStates", this.storage.getRaw()); -+ } -+ -+ public boolean isSkippable() { -+ return this.storage == null; -+ } -+ -+ public int getBlock(final int index) { -+ return this.storage.get(index); -+ } -+ -+ protected int getStateId(final String name, final boolean persistent, final int distance) { -+ return LEAVES_TO_ID.getInt(name) << 5 | (persistent ? 16 : 0) | distance; -+ } -+ -+ protected int getSectionY() { -+ return this.sectionY; -+ } -+ -+ protected abstract boolean initSkippable(); -+ } -+ -+ public static final class LeavesSection extends Section { -+ private IntOpenHashSet leaveIds; -+ private IntOpenHashSet logIds; -+ private Int2IntOpenHashMap stateToIdMap; -+ -+ public LeavesSection(final MapType section) { -+ super(section); -+ } -+ -+ @Override -+ protected boolean initSkippable() { -+ this.leaveIds = new IntOpenHashSet(); -+ this.logIds = new IntOpenHashSet(); -+ this.stateToIdMap = new Int2IntOpenHashMap(); -+ this.stateToIdMap.defaultReturnValue(-1); -+ -+ for(int i = 0; i < this.palette.size(); ++i) { -+ final MapType blockState = this.palette.getMap(i); -+ final String name = blockState.getString("Name", ""); -+ if (LEAVES_TO_ID.containsKey(name)) { -+ final MapType properties = blockState.getMap("Properties"); -+ final boolean notDecayable = properties != null && "false".equals(properties.getString("decayable")); -+ -+ this.leaveIds.add(i); -+ this.stateToIdMap.put(this.getStateId(name, notDecayable, 7), i); -+ this.palette.setMap(i, this.makeNewLeafTag(name, notDecayable, 7)); -+ } -+ -+ if (LOGS.contains(name)) { -+ this.logIds.add(i); -+ } -+ } -+ -+ return this.leaveIds.isEmpty() && this.logIds.isEmpty(); -+ } -+ -+ private MapType makeNewLeafTag(final String name, final boolean notDecayable, final int distance) { -+ final MapType properties = Types.NBT.createEmptyMap(); -+ final MapType ret = Types.NBT.createEmptyMap(); -+ -+ ret.setString("Name", name); -+ ret.setMap("Properties", properties); -+ -+ properties.setString("persistent", Boolean.toString(notDecayable)); -+ properties.setString("distance", Integer.toString(distance)); -+ -+ return ret; -+ } -+ -+ public boolean isLog(final int id) { -+ return this.logIds.contains(id); -+ } -+ -+ public boolean isLeaf(final int id) { -+ return this.leaveIds.contains(id); -+ } -+ -+ // only call for logs or leaves, will throw otherwise! -+ private int getDistance(final int id) { -+ if (this.isLog(id)) { -+ return 0; -+ } -+ -+ return Integer.parseInt(this.palette.getMap(id).getMap("Properties").getString("distance")); -+ } -+ -+ private void setDistance(final int index, final int id, final int distance) { -+ final MapType state = this.palette.getMap(id); -+ final String name = state.getString("Name"); -+ final boolean persistent = "true".equals(state.getMap("Properties").getString("persistent")); -+ final int newState = this.getStateId(name, persistent, distance); -+ int newStateId; -+ if ((newStateId = this.stateToIdMap.get(newState)) == -1) { -+ newStateId = this.palette.size(); -+ this.leaveIds.add(newStateId); -+ this.stateToIdMap.put(newState, newStateId); -+ this.palette.addMap(this.makeNewLeafTag(name, persistent, distance)); -+ } -+ -+ if (1 << this.storage.getBits() <= newStateId) { -+ // need to widen storage -+ final PackedBitStorage newStorage = new PackedBitStorage(this.storage.getBits() + 1, 4096); -+ -+ for(int i = 0; i < 4096; ++i) { -+ newStorage.set(i, this.storage.get(i)); -+ } -+ -+ this.storage = newStorage; -+ } -+ -+ this.storage.set(index, newStateId); -+ } -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1500.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1500.java -new file mode 100644 -index 0000000000000000000000000000000000000000..fae8cf61c9900544cdecd223f72e1311c8a1cfb1 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1500.java -@@ -0,0 +1,23 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V1500 { -+ -+ private static final int VERSION = MCVersions.V18W22C + 1; -+ -+ public static void register() { -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("DUMMY", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ data.setBoolean("keepPacked", true); -+ return null; -+ } -+ }); -+ } -+ -+ private V1500() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1501.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1501.java -new file mode 100644 -index 0000000000000000000000000000000000000000..dbfb51b74c54a9a479de49ecb295854fc69aef64 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1501.java -@@ -0,0 +1,78 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.advancements.ConverterAbstractAdvancementsRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V1501 { -+ -+ private static final int VERSION = MCVersions.V1_13_PRE1; -+ -+ private static final Map RENAMES = new HashMap<>( -+ ImmutableMap.builder() -+ .put("minecraft:recipes/brewing/speckled_melon", "minecraft:recipes/brewing/glistering_melon_slice") -+ .put("minecraft:recipes/building_blocks/black_stained_hardened_clay", "minecraft:recipes/building_blocks/black_terracotta") -+ .put("minecraft:recipes/building_blocks/blue_stained_hardened_clay", "minecraft:recipes/building_blocks/blue_terracotta") -+ .put("minecraft:recipes/building_blocks/brown_stained_hardened_clay", "minecraft:recipes/building_blocks/brown_terracotta") -+ .put("minecraft:recipes/building_blocks/cyan_stained_hardened_clay", "minecraft:recipes/building_blocks/cyan_terracotta") -+ .put("minecraft:recipes/building_blocks/gray_stained_hardened_clay", "minecraft:recipes/building_blocks/gray_terracotta") -+ .put("minecraft:recipes/building_blocks/green_stained_hardened_clay", "minecraft:recipes/building_blocks/green_terracotta") -+ .put("minecraft:recipes/building_blocks/light_blue_stained_hardened_clay", "minecraft:recipes/building_blocks/light_blue_terracotta") -+ .put("minecraft:recipes/building_blocks/light_gray_stained_hardened_clay", "minecraft:recipes/building_blocks/light_gray_terracotta") -+ .put("minecraft:recipes/building_blocks/lime_stained_hardened_clay", "minecraft:recipes/building_blocks/lime_terracotta") -+ .put("minecraft:recipes/building_blocks/magenta_stained_hardened_clay", "minecraft:recipes/building_blocks/magenta_terracotta") -+ .put("minecraft:recipes/building_blocks/orange_stained_hardened_clay", "minecraft:recipes/building_blocks/orange_terracotta") -+ .put("minecraft:recipes/building_blocks/pink_stained_hardened_clay", "minecraft:recipes/building_blocks/pink_terracotta") -+ .put("minecraft:recipes/building_blocks/purple_stained_hardened_clay", "minecraft:recipes/building_blocks/purple_terracotta") -+ .put("minecraft:recipes/building_blocks/red_stained_hardened_clay", "minecraft:recipes/building_blocks/red_terracotta") -+ .put("minecraft:recipes/building_blocks/white_stained_hardened_clay", "minecraft:recipes/building_blocks/white_terracotta") -+ .put("minecraft:recipes/building_blocks/yellow_stained_hardened_clay", "minecraft:recipes/building_blocks/yellow_terracotta") -+ .put("minecraft:recipes/building_blocks/acacia_wooden_slab", "minecraft:recipes/building_blocks/acacia_slab") -+ .put("minecraft:recipes/building_blocks/birch_wooden_slab", "minecraft:recipes/building_blocks/birch_slab") -+ .put("minecraft:recipes/building_blocks/dark_oak_wooden_slab", "minecraft:recipes/building_blocks/dark_oak_slab") -+ .put("minecraft:recipes/building_blocks/jungle_wooden_slab", "minecraft:recipes/building_blocks/jungle_slab") -+ .put("minecraft:recipes/building_blocks/oak_wooden_slab", "minecraft:recipes/building_blocks/oak_slab") -+ .put("minecraft:recipes/building_blocks/spruce_wooden_slab", "minecraft:recipes/building_blocks/spruce_slab") -+ .put("minecraft:recipes/building_blocks/brick_block", "minecraft:recipes/building_blocks/bricks") -+ .put("minecraft:recipes/building_blocks/chiseled_stonebrick", "minecraft:recipes/building_blocks/chiseled_stone_bricks") -+ .put("minecraft:recipes/building_blocks/end_bricks", "minecraft:recipes/building_blocks/end_stone_bricks") -+ .put("minecraft:recipes/building_blocks/lit_pumpkin", "minecraft:recipes/building_blocks/jack_o_lantern") -+ .put("minecraft:recipes/building_blocks/magma", "minecraft:recipes/building_blocks/magma_block") -+ .put("minecraft:recipes/building_blocks/melon_block", "minecraft:recipes/building_blocks/melon") -+ .put("minecraft:recipes/building_blocks/mossy_stonebrick", "minecraft:recipes/building_blocks/mossy_stone_bricks") -+ .put("minecraft:recipes/building_blocks/nether_brick", "minecraft:recipes/building_blocks/nether_bricks") -+ .put("minecraft:recipes/building_blocks/pillar_quartz_block", "minecraft:recipes/building_blocks/quartz_pillar") -+ .put("minecraft:recipes/building_blocks/red_nether_brick", "minecraft:recipes/building_blocks/red_nether_bricks") -+ .put("minecraft:recipes/building_blocks/snow", "minecraft:recipes/building_blocks/snow_block") -+ .put("minecraft:recipes/building_blocks/smooth_red_sandstone", "minecraft:recipes/building_blocks/cut_red_sandstone") -+ .put("minecraft:recipes/building_blocks/smooth_sandstone", "minecraft:recipes/building_blocks/cut_sandstone") -+ .put("minecraft:recipes/building_blocks/stonebrick", "minecraft:recipes/building_blocks/stone_bricks") -+ .put("minecraft:recipes/building_blocks/stone_stairs", "minecraft:recipes/building_blocks/cobblestone_stairs") -+ .put("minecraft:recipes/building_blocks/string_to_wool", "minecraft:recipes/building_blocks/white_wool_from_string") -+ .put("minecraft:recipes/decorations/fence", "minecraft:recipes/decorations/oak_fence") -+ .put("minecraft:recipes/decorations/purple_shulker_box", "minecraft:recipes/decorations/shulker_box") -+ .put("minecraft:recipes/decorations/slime", "minecraft:recipes/decorations/slime_block") -+ .put("minecraft:recipes/decorations/snow_layer", "minecraft:recipes/decorations/snow") -+ .put("minecraft:recipes/misc/bone_meal_from_block", "minecraft:recipes/misc/bone_meal_from_bone_block") -+ .put("minecraft:recipes/misc/bone_meal_from_bone", "minecraft:recipes/misc/bone_meal") -+ .put("minecraft:recipes/misc/gold_ingot_from_block", "minecraft:recipes/misc/gold_ingot_from_gold_block") -+ .put("minecraft:recipes/misc/iron_ingot_from_block", "minecraft:recipes/misc/iron_ingot_from_iron_block") -+ .put("minecraft:recipes/redstone/fence_gate", "minecraft:recipes/redstone/oak_fence_gate") -+ .put("minecraft:recipes/redstone/noteblock", "minecraft:recipes/redstone/note_block") -+ .put("minecraft:recipes/redstone/trapdoor", "minecraft:recipes/redstone/oak_trapdoor") -+ .put("minecraft:recipes/redstone/wooden_button", "minecraft:recipes/redstone/oak_button") -+ .put("minecraft:recipes/redstone/wooden_door", "minecraft:recipes/redstone/oak_door") -+ .put("minecraft:recipes/redstone/wooden_pressure_plate", "minecraft:recipes/redstone/oak_pressure_plate") -+ .put("minecraft:recipes/transportation/boat", "minecraft:recipes/transportation/oak_boat") -+ .put("minecraft:recipes/transportation/golden_rail", "minecraft:recipes/transportation/powered_rail") -+ .build() -+ ); -+ -+ public static void register() { -+ ConverterAbstractAdvancementsRename.register(VERSION, RENAMES::get); -+ } -+ -+ private V1501() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1502.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1502.java -new file mode 100644 -index 0000000000000000000000000000000000000000..cd07718649f0e2ca66f1ec3b0aba81611333ba09 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1502.java -@@ -0,0 +1,77 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.recipe.ConverterAbstractRecipeRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V1502 { -+ -+ private static final int VERSION = MCVersions.V1_13_PRE2; -+ -+ private static final Map RECIPES_UPDATES = new HashMap<>( -+ ImmutableMap.builder() -+ .put("minecraft:acacia_wooden_slab", "minecraft:acacia_slab") -+ .put("minecraft:birch_wooden_slab", "minecraft:birch_slab") -+ .put("minecraft:black_stained_hardened_clay", "minecraft:black_terracotta") -+ .put("minecraft:blue_stained_hardened_clay", "minecraft:blue_terracotta") -+ .put("minecraft:boat", "minecraft:oak_boat") -+ .put("minecraft:bone_meal_from_block", "minecraft:bone_meal_from_bone_block") -+ .put("minecraft:bone_meal_from_bone", "minecraft:bone_meal") -+ .put("minecraft:brick_block", "minecraft:bricks") -+ .put("minecraft:brown_stained_hardened_clay", "minecraft:brown_terracotta") -+ .put("minecraft:chiseled_stonebrick", "minecraft:chiseled_stone_bricks") -+ .put("minecraft:cyan_stained_hardened_clay", "minecraft:cyan_terracotta") -+ .put("minecraft:dark_oak_wooden_slab", "minecraft:dark_oak_slab") -+ .put("minecraft:end_bricks", "minecraft:end_stone_bricks") -+ .put("minecraft:fence_gate", "minecraft:oak_fence_gate") -+ .put("minecraft:fence", "minecraft:oak_fence") -+ .put("minecraft:golden_rail", "minecraft:powered_rail") -+ .put("minecraft:gold_ingot_from_block", "minecraft:gold_ingot_from_gold_block") -+ .put("minecraft:gray_stained_hardened_clay", "minecraft:gray_terracotta") -+ .put("minecraft:green_stained_hardened_clay", "minecraft:green_terracotta") -+ .put("minecraft:iron_ingot_from_block", "minecraft:iron_ingot_from_iron_block") -+ .put("minecraft:jungle_wooden_slab", "minecraft:jungle_slab") -+ .put("minecraft:light_blue_stained_hardened_clay", "minecraft:light_blue_terracotta") -+ .put("minecraft:light_gray_stained_hardened_clay", "minecraft:light_gray_terracotta") -+ .put("minecraft:lime_stained_hardened_clay", "minecraft:lime_terracotta") -+ .put("minecraft:lit_pumpkin", "minecraft:jack_o_lantern") -+ .put("minecraft:magenta_stained_hardened_clay", "minecraft:magenta_terracotta") -+ .put("minecraft:magma", "minecraft:magma_block") -+ .put("minecraft:melon_block", "minecraft:melon") -+ .put("minecraft:mossy_stonebrick", "minecraft:mossy_stone_bricks") -+ .put("minecraft:noteblock", "minecraft:note_block") -+ .put("minecraft:oak_wooden_slab", "minecraft:oak_slab") -+ .put("minecraft:orange_stained_hardened_clay", "minecraft:orange_terracotta") -+ .put("minecraft:pillar_quartz_block", "minecraft:quartz_pillar") -+ .put("minecraft:pink_stained_hardened_clay", "minecraft:pink_terracotta") -+ .put("minecraft:purple_shulker_box", "minecraft:shulker_box") -+ .put("minecraft:purple_stained_hardened_clay", "minecraft:purple_terracotta") -+ .put("minecraft:red_nether_brick", "minecraft:red_nether_bricks") -+ .put("minecraft:red_stained_hardened_clay", "minecraft:red_terracotta") -+ .put("minecraft:slime", "minecraft:slime_block") -+ .put("minecraft:smooth_red_sandstone", "minecraft:cut_red_sandstone") -+ .put("minecraft:smooth_sandstone", "minecraft:cut_sandstone") -+ .put("minecraft:snow_layer", "minecraft:snow") -+ .put("minecraft:snow", "minecraft:snow_block") -+ .put("minecraft:speckled_melon", "minecraft:glistering_melon_slice") -+ .put("minecraft:spruce_wooden_slab", "minecraft:spruce_slab") -+ .put("minecraft:stonebrick", "minecraft:stone_bricks") -+ .put("minecraft:stone_stairs", "minecraft:cobblestone_stairs") -+ .put("minecraft:string_to_wool", "minecraft:white_wool_from_string") -+ .put("minecraft:trapdoor", "minecraft:oak_trapdoor") -+ .put("minecraft:white_stained_hardened_clay", "minecraft:white_terracotta") -+ .put("minecraft:wooden_button", "minecraft:oak_button") -+ .put("minecraft:wooden_door", "minecraft:oak_door") -+ .put("minecraft:wooden_pressure_plate", "minecraft:oak_pressure_plate") -+ .put("minecraft:yellow_stained_hardened_clay", "minecraft:yellow_terracotta") -+ .build() -+ ); -+ -+ public static void register() { -+ ConverterAbstractRecipeRename.register(VERSION, RECIPES_UPDATES::get); -+ } -+ -+ private V1502() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1506.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1506.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ce87995961605c80f24371c9c64706ae76e3edea ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1506.java -@@ -0,0 +1,219 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.json.JsonMapType; -+import ca.spottedleaf.dataconverter.types.json.JsonTypeUtil; -+import ca.spottedleaf.dataconverter.types.nbt.NBTMapType; -+import com.google.common.base.Splitter; -+import com.google.common.collect.ImmutableMap; -+import com.google.common.collect.Lists; -+import com.google.common.collect.Maps; -+import com.mojang.datafixers.util.Pair; -+import com.mojang.serialization.Dynamic; -+import com.mojang.serialization.DynamicOps; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.nbt.NbtOps; -+import net.minecraft.nbt.Tag; -+import net.minecraft.util.GsonHelper; -+import java.util.ArrayList; -+import java.util.Collections; -+import java.util.HashMap; -+import java.util.Iterator; -+import java.util.List; -+import java.util.Locale; -+import java.util.Map; -+import java.util.stream.Collectors; -+ -+public final class V1506 { -+ -+ private static final int VERSION = MCVersions.V1_13_PRE4 + 2; -+ -+ static final Map MAP = new HashMap<>(); -+ static { -+ MAP.put("0", "minecraft:ocean"); -+ MAP.put("1", "minecraft:plains"); -+ MAP.put("2", "minecraft:desert"); -+ MAP.put("3", "minecraft:mountains"); -+ MAP.put("4", "minecraft:forest"); -+ MAP.put("5", "minecraft:taiga"); -+ MAP.put("6", "minecraft:swamp"); -+ MAP.put("7", "minecraft:river"); -+ MAP.put("8", "minecraft:nether"); -+ MAP.put("9", "minecraft:the_end"); -+ MAP.put("10", "minecraft:frozen_ocean"); -+ MAP.put("11", "minecraft:frozen_river"); -+ MAP.put("12", "minecraft:snowy_tundra"); -+ MAP.put("13", "minecraft:snowy_mountains"); -+ MAP.put("14", "minecraft:mushroom_fields"); -+ MAP.put("15", "minecraft:mushroom_field_shore"); -+ MAP.put("16", "minecraft:beach"); -+ MAP.put("17", "minecraft:desert_hills"); -+ MAP.put("18", "minecraft:wooded_hills"); -+ MAP.put("19", "minecraft:taiga_hills"); -+ MAP.put("20", "minecraft:mountain_edge"); -+ MAP.put("21", "minecraft:jungle"); -+ MAP.put("22", "minecraft:jungle_hills"); -+ MAP.put("23", "minecraft:jungle_edge"); -+ MAP.put("24", "minecraft:deep_ocean"); -+ MAP.put("25", "minecraft:stone_shore"); -+ MAP.put("26", "minecraft:snowy_beach"); -+ MAP.put("27", "minecraft:birch_forest"); -+ MAP.put("28", "minecraft:birch_forest_hills"); -+ MAP.put("29", "minecraft:dark_forest"); -+ MAP.put("30", "minecraft:snowy_taiga"); -+ MAP.put("31", "minecraft:snowy_taiga_hills"); -+ MAP.put("32", "minecraft:giant_tree_taiga"); -+ MAP.put("33", "minecraft:giant_tree_taiga_hills"); -+ MAP.put("34", "minecraft:wooded_mountains"); -+ MAP.put("35", "minecraft:savanna"); -+ MAP.put("36", "minecraft:savanna_plateau"); -+ MAP.put("37", "minecraft:badlands"); -+ MAP.put("38", "minecraft:wooded_badlands_plateau"); -+ MAP.put("39", "minecraft:badlands_plateau"); -+ MAP.put("40", "minecraft:small_end_islands"); -+ MAP.put("41", "minecraft:end_midlands"); -+ MAP.put("42", "minecraft:end_highlands"); -+ MAP.put("43", "minecraft:end_barrens"); -+ MAP.put("44", "minecraft:warm_ocean"); -+ MAP.put("45", "minecraft:lukewarm_ocean"); -+ MAP.put("46", "minecraft:cold_ocean"); -+ MAP.put("47", "minecraft:deep_warm_ocean"); -+ MAP.put("48", "minecraft:deep_lukewarm_ocean"); -+ MAP.put("49", "minecraft:deep_cold_ocean"); -+ MAP.put("50", "minecraft:deep_frozen_ocean"); -+ MAP.put("127", "minecraft:the_void"); -+ MAP.put("129", "minecraft:sunflower_plains"); -+ MAP.put("130", "minecraft:desert_lakes"); -+ MAP.put("131", "minecraft:gravelly_mountains"); -+ MAP.put("132", "minecraft:flower_forest"); -+ MAP.put("133", "minecraft:taiga_mountains"); -+ MAP.put("134", "minecraft:swamp_hills"); -+ MAP.put("140", "minecraft:ice_spikes"); -+ MAP.put("149", "minecraft:modified_jungle"); -+ MAP.put("151", "minecraft:modified_jungle_edge"); -+ MAP.put("155", "minecraft:tall_birch_forest"); -+ MAP.put("156", "minecraft:tall_birch_hills"); -+ MAP.put("157", "minecraft:dark_forest_hills"); -+ MAP.put("158", "minecraft:snowy_taiga_mountains"); -+ MAP.put("160", "minecraft:giant_spruce_taiga"); -+ MAP.put("161", "minecraft:giant_spruce_taiga_hills"); -+ MAP.put("162", "minecraft:modified_gravelly_mountains"); -+ MAP.put("163", "minecraft:shattered_savanna"); -+ MAP.put("164", "minecraft:shattered_savanna_plateau"); -+ MAP.put("165", "minecraft:eroded_badlands"); -+ MAP.put("166", "minecraft:modified_wooded_badlands_plateau"); -+ MAP.put("167", "minecraft:modified_badlands_plateau"); -+ } -+ -+ public static void register() { -+ MCTypeRegistry.LEVEL.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final String generatorOptions = data.getString("generatorOptions"); -+ final String generatorName = data.getString("generatorName"); -+ if ("flat".equalsIgnoreCase(generatorName)) { -+ data.setMap("generatorOptions", V1506.convert(generatorOptions == null ? "" : generatorOptions)); -+ } else if ("buffet".equalsIgnoreCase(generatorName) && generatorOptions != null) { -+ data.setMap("generatorOptions", JsonTypeUtil.convertJsonToNBT(new JsonMapType(GsonHelper.parse(generatorOptions, true), false))); -+ } -+ return null; -+ } -+ }); -+ } -+ -+ private static MapType convert(final String param0) { -+ final Dynamic dynamic = convert(param0, NbtOps.INSTANCE); -+ -+ return new NBTMapType((CompoundTag)dynamic.getValue()); -+ } -+ -+ // Yeah I ain't touching that. This is basically magic value hell. -+ private static Dynamic convert(final String generatorSettings, final DynamicOps ops) { -+ final Iterator splitSettings = Splitter.on(';').split(generatorSettings).iterator(); -+ String biome = "minecraft:plains"; -+ final Map> structures = Maps.newHashMap(); -+ final List> layers; -+ if (!generatorSettings.isEmpty() && splitSettings.hasNext()) { -+ layers = getLayersInfoFromString(splitSettings.next()); -+ if (!layers.isEmpty()) { -+ // biome is next -+ if (splitSettings.hasNext()) { -+ biome = MAP.getOrDefault(splitSettings.next(), "minecraft:plains"); -+ } -+ -+ // structures is next -+ if (splitSettings.hasNext()) { -+ final String[] structuresSplit = splitSettings.next().toLowerCase(Locale.ROOT).split(","); -+ -+ for (final String structureString : structuresSplit) { -+ final String[] structureInfo = structureString.split("\\(", 2); -+ if (!structureInfo[0].isEmpty()) { -+ structures.put(structureInfo[0], Maps.newHashMap()); -+ if (structureInfo.length > 1 && structureInfo[1].endsWith(")") && structureInfo[1].length() > 1) { -+ // I can't even guess the mappings for these. Not worth my time, it will work regardless of the mappings -+ final String[] var7 = structureInfo[1].substring(0, structureInfo[1].length() - 1).split(" "); -+ -+ for (final String var8 : var7) { -+ String[] var9 = var8.split("=", 2); -+ if (var9.length == 2) { -+ structures.get(structureInfo[0]).put(var9[0], var9[1]); -+ } -+ } -+ } -+ } -+ } -+ } else { -+ structures.put("village", Maps.newHashMap()); -+ } -+ } -+ } else { -+ layers = Lists.newArrayList(); -+ layers.add(Pair.of(1, "minecraft:bedrock")); -+ layers.add(Pair.of(2, "minecraft:dirt")); -+ layers.add(Pair.of(1, "minecraft:grass_block")); -+ structures.put("village", Maps.newHashMap()); -+ } -+ -+ final T layerTag = ops.createList(layers.stream().map((param1x) -> ops.createMap(ImmutableMap.of(ops.createString("height"), ops.createInt(param1x.getFirst()), ops.createString("block"), ops.createString(param1x.getSecond()))))); -+ final T structuresTag = ops.createMap(structures.entrySet().stream().map((param1x) -> Pair.of(ops.createString(param1x.getKey().toLowerCase(Locale.ROOT)), ops.createMap(param1x.getValue().entrySet().stream().map((param1xx) -> Pair.of(ops.createString(param1xx.getKey()), ops.createString(param1xx.getValue()))).collect(Collectors.toMap(Pair::getFirst, Pair::getSecond))))).collect(Collectors.toMap(Pair::getFirst, Pair::getSecond))); -+ return new Dynamic<>(ops, ops.createMap(ImmutableMap.of(ops.createString("layers"), layerTag, ops.createString("biome"), ops.createString(biome), ops.createString("structures"), structuresTag))); -+ } -+ -+ private static Pair getLayerInfoFromString(final String layerString) { -+ final String[] split = layerString.split("\\*", 2); -+ int layerCount; -+ if (split.length == 2) { -+ try { -+ layerCount = Integer.parseInt(split[0]); -+ } catch (final NumberFormatException ex) { -+ return null; -+ } -+ } else { -+ layerCount = 1; -+ } -+ -+ final String blockName = split[split.length - 1]; -+ return Pair.of(layerCount, blockName); -+ } -+ -+ private static List> getLayersInfoFromString(final String layersString) { -+ final List> ret = new ArrayList<>(); -+ final String[] layers = layersString.split(","); -+ -+ for (final String layerString : layers) { -+ final Pair layer = getLayerInfoFromString(layerString); -+ if (layer == null) { -+ return Collections.emptyList(); -+ } -+ -+ ret.add(layer); -+ } -+ -+ return ret; -+ } -+ -+ private V1506() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1510.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1510.java -new file mode 100644 -index 0000000000000000000000000000000000000000..dfc9d1e89983c73e06ce3c8a22c29f49af4a935c ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1510.java -@@ -0,0 +1,111 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterAbstractEntityRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.recipe.ConverterAbstractRecipeRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.stats.ConverterAbstractStatsRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V1510 { -+ -+ public static final Map RENAMED_ENTITY_IDS = new HashMap<>( -+ ImmutableMap.builder() -+ .put("minecraft:commandblock_minecart", "minecraft:command_block_minecart") -+ .put("minecraft:ender_crystal", "minecraft:end_crystal") -+ .put("minecraft:snowman", "minecraft:snow_golem") -+ .put("minecraft:evocation_illager", "minecraft:evoker") -+ .put("minecraft:evocation_fangs", "minecraft:evoker_fangs") -+ .put("minecraft:illusion_illager", "minecraft:illusioner") -+ .put("minecraft:vindication_illager", "minecraft:vindicator") -+ .put("minecraft:villager_golem", "minecraft:iron_golem") -+ .put("minecraft:xp_orb", "minecraft:experience_orb") -+ .put("minecraft:xp_bottle", "minecraft:experience_bottle") -+ .put("minecraft:eye_of_ender_signal", "minecraft:eye_of_ender") -+ .put("minecraft:fireworks_rocket", "minecraft:firework_rocket") -+ .build() -+ ); -+ -+ public static final Map RENAMED_BLOCKS = new HashMap<>( -+ ImmutableMap.builder() -+ .put("minecraft:portal", "minecraft:nether_portal") -+ .put("minecraft:oak_bark", "minecraft:oak_wood") -+ .put("minecraft:spruce_bark", "minecraft:spruce_wood") -+ .put("minecraft:birch_bark", "minecraft:birch_wood") -+ .put("minecraft:jungle_bark", "minecraft:jungle_wood") -+ .put("minecraft:acacia_bark", "minecraft:acacia_wood") -+ .put("minecraft:dark_oak_bark", "minecraft:dark_oak_wood") -+ .put("minecraft:stripped_oak_bark", "minecraft:stripped_oak_wood") -+ .put("minecraft:stripped_spruce_bark", "minecraft:stripped_spruce_wood") -+ .put("minecraft:stripped_birch_bark", "minecraft:stripped_birch_wood") -+ .put("minecraft:stripped_jungle_bark", "minecraft:stripped_jungle_wood") -+ .put("minecraft:stripped_acacia_bark", "minecraft:stripped_acacia_wood") -+ .put("minecraft:stripped_dark_oak_bark", "minecraft:stripped_dark_oak_wood") -+ .put("minecraft:mob_spawner", "minecraft:spawner") -+ .build() -+ ); -+ -+ public static final Map RENAMED_ITEMS = new HashMap<>( -+ ImmutableMap.builder() -+ .putAll(RENAMED_BLOCKS) -+ .put("minecraft:clownfish", "minecraft:tropical_fish") -+ .put("minecraft:chorus_fruit_popped", "minecraft:popped_chorus_fruit") -+ .put("minecraft:evocation_illager_spawn_egg", "minecraft:evoker_spawn_egg") -+ .put("minecraft:vindication_illager_spawn_egg", "minecraft:vindicator_spawn_egg") -+ .build() -+ ); -+ -+ private static final Map RECIPES_UPDATES = new HashMap<>( -+ ImmutableMap.builder() -+ .put("minecraft:acacia_bark", "minecraft:acacia_wood") -+ .put("minecraft:birch_bark", "minecraft:birch_wood") -+ .put("minecraft:dark_oak_bark", "minecraft:dark_oak_wood") -+ .put("minecraft:jungle_bark", "minecraft:jungle_wood") -+ .put("minecraft:oak_bark", "minecraft:oak_wood") -+ .put("minecraft:spruce_bark", "minecraft:spruce_wood") -+ .build() -+ ); -+ -+ private static final int VERSION = MCVersions.V1_13_PRE4 + 6; -+ -+ public static void register() { -+ ConverterAbstractBlockRename.register(VERSION, RENAMED_BLOCKS::get); -+ ConverterAbstractItemRename.register(VERSION, RENAMED_ITEMS::get); -+ ConverterAbstractRecipeRename.register(VERSION, RECIPES_UPDATES::get); -+ -+ ConverterAbstractEntityRename.register(VERSION, (String input) -> { -+ if (input.startsWith("minecraft:bred_")) { -+ input = "minecraft:".concat(input.substring("minecraft:bred_".length())); -+ } -+ -+ return RENAMED_ENTITY_IDS.get(input); -+ }); -+ -+ ConverterAbstractStatsRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:swim_one_cm", "minecraft:walk_on_water_one_cm", -+ "minecraft:dive_one_cm", "minecraft:walk_under_water_one_cm" -+ ) -+ )::get); -+ -+ -+ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:commandblock_minecart", "minecraft:command_block_minecart"); -+ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:ender_crystal", "minecraft:end_crystal"); -+ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:snowman", "minecraft:snow_golem"); -+ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:evocation_illager", "minecraft:evoker"); -+ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:evocation_fangs", "minecraft:evoker_fangs"); -+ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:illusion_illager", "minecraft:illusioner"); -+ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:vindication_illager", "minecraft:vindicator"); -+ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:villager_golem", "minecraft:iron_golem"); -+ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:xp_orb", "minecraft:experience_orb"); -+ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:xp_bottle", "minecraft:experience_bottle"); -+ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:eye_of_ender_signal", "minecraft:eye_of_ender"); -+ MCTypeRegistry.ENTITY.copyWalkers(VERSION, "minecraft:fireworks_rocket", "minecraft:firework_rocket"); -+ } -+ -+ private V1510() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1514.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1514.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6bcc0de5987db4d9ac28fabefbb58c28f2065d96 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1514.java -@@ -0,0 +1,68 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.util.ComponentUtils; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V1514 { -+ -+ private static final int VERSION = MCVersions.V1_13_PRE7 + 1; -+ -+ public static void register() { -+ MCTypeRegistry.OBJECTIVE.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final String displayName = data.getString("DisplayName"); -+ if (displayName == null) { -+ return null; -+ } -+ -+ final String update = ComponentUtils.createPlainTextComponent(displayName); -+ -+ data.setString("DisplayName", update); -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.TEAM.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final String displayName = data.getString("DisplayName"); -+ if (displayName == null) { -+ return null; -+ } -+ -+ final String update = ComponentUtils.createPlainTextComponent(displayName); -+ -+ data.setString("DisplayName", update); -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.OBJECTIVE.addStructureConverter(new DataConverter<>(VERSION) { -+ private static String getRenderType(String string) { -+ return string.equals("health") ? "hearts" : "integer"; -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final String renderType = data.getString("RenderType"); -+ if (renderType != null) { -+ return null; -+ } -+ -+ final String criteriaName = data.getString("CriteriaName", ""); -+ -+ data.setString("RenderType", getRenderType(criteriaName)); -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V1514() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1515.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1515.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d2093732e06ddccdd8a34bbfcaee6ede3aae96d0 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1515.java -@@ -0,0 +1,28 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V1515 { -+ -+ private static final int VERSION = MCVersions.V1_13_PRE7 + 2; -+ -+ public static final Map RENAMED_BLOCK_IDS = new HashMap<>( -+ ImmutableMap.builder() -+ .put("minecraft:tube_coral_fan", "minecraft:tube_coral_wall_fan") -+ .put("minecraft:brain_coral_fan", "minecraft:brain_coral_wall_fan") -+ .put("minecraft:bubble_coral_fan", "minecraft:bubble_coral_wall_fan") -+ .put("minecraft:fire_coral_fan", "minecraft:fire_coral_wall_fan") -+ .put("minecraft:horn_coral_fan", "minecraft:horn_coral_wall_fan") -+ .build() -+ ); -+ -+ public static void register() { -+ ConverterAbstractBlockRename.register(VERSION, RENAMED_BLOCK_IDS::get); -+ } -+ -+ private V1515() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1624.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1624.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f198495e1bad7a1cb84f41c1ea96b1d0e7943c9e ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1624.java -@@ -0,0 +1,110 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import com.mojang.logging.LogUtils; -+import it.unimi.dsi.fastutil.ints.IntOpenHashSet; -+import org.slf4j.Logger; -+ -+public final class V1624 { -+ -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ -+ private static final int VERSION = MCVersions.V18W32A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType level = data.getMap("Level"); -+ -+ if (level == null) { -+ return null; -+ } -+ -+ final ListType sections = level.getList("Sections", ObjectType.MAP); -+ if (sections == null) { -+ return null; -+ } -+ -+ final IntOpenHashSet positionsToLook = new IntOpenHashSet(); -+ -+ for (int i = 0, len = sections.size(); i < len; ++i) { -+ final TrappedChestSection section = new TrappedChestSection(sections.getMap(i)); -+ if (section.isSkippable()) { -+ continue; -+ } -+ -+ for (int index = 0; index < 4096; ++index) { -+ if (section.isTrappedChest(section.getBlock(index))) { -+ positionsToLook.add(section.getSectionY() << 12 | index); -+ } -+ } -+ } -+ -+ final int chunkX = level.getInt("xPos"); -+ final int chunkZ = level.getInt("zPos"); -+ -+ final ListType tileEntities = level.getList("TileEntities", ObjectType.MAP); -+ -+ if (tileEntities != null) { -+ for (int i = 0, len = tileEntities.size(); i < len; ++i) { -+ final MapType tile = tileEntities.getMap(i); -+ -+ final int x = tile.getInt("x"); -+ final int y = tile.getInt("y"); -+ final int z = tile.getInt("z"); -+ -+ final int index = V1496.getIndex(x - (chunkX << 4), y, z - (chunkZ << 4)); -+ if (!positionsToLook.contains(index)) { -+ continue; -+ } -+ -+ final String id = tile.getString("id"); -+ if (!"minecraft:chest".equals(id)) { -+ LOGGER.warn("Block Entity ({},{},{}) was expected to be a chest (V1624)", x, y, z); -+ } -+ -+ tile.setString("id", "minecraft:trapped_chest"); -+ } -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V1624() {} -+ -+ public static final class TrappedChestSection extends V1496.Section { -+ -+ private IntOpenHashSet chestIds; -+ -+ public TrappedChestSection(final MapType section) { -+ super(section); -+ } -+ -+ @Override -+ protected boolean initSkippable() { -+ this.chestIds = new IntOpenHashSet(); -+ -+ for (int i = 0; i < this.palette.size(); ++i) { -+ final MapType blockState = this.palette.getMap(i); -+ final String name = blockState.getString("Name"); -+ if ("minecraft:trapped_chest".equals(name)) { -+ this.chestIds.add(i); -+ } -+ } -+ -+ return this.chestIds.isEmpty(); -+ } -+ -+ public boolean isTrappedChest(final int id) { -+ return this.chestIds.contains(id); -+ } -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V165.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V165.java -new file mode 100644 -index 0000000000000000000000000000000000000000..810a838edeea95bb5d0b4b351e65417b762fc45c ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V165.java -@@ -0,0 +1,41 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.util.ComponentUtils; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V165 { -+ -+ private static final int VERSION = MCVersions.V1_9_PRE2; -+ -+ public static void register() { -+ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType tag = data.getMap("tag"); -+ if (tag == null) { -+ return null; -+ } -+ -+ final ListType pages = tag.getList("pages", ObjectType.STRING); -+ if (pages == null) { -+ return null; -+ } -+ -+ for (int i = 0, len = pages.size(); i < len; ++i) { -+ final String page = pages.getString(i); -+ -+ pages.setString(i, ComponentUtils.convertFromLenient(page)); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V165() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1800.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1800.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7f65def5a0f48af268183d9c3b74937924b47b75 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1800.java -@@ -0,0 +1,36 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V1800 { -+ -+ private static final int VERSION = MCVersions.V1_13_2 + 169; -+ -+ public static final Map RENAMED_ITEM_IDS = new HashMap<>( -+ ImmutableMap.builder() -+ .put("minecraft:cactus_green", "minecraft:green_dye") -+ .put("minecraft:rose_red", "minecraft:red_dye") -+ .put("minecraft:dandelion_yellow", "minecraft:yellow_dye") -+ .build() -+ ); -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ ConverterAbstractItemRename.register(VERSION, RENAMED_ITEM_IDS::get); -+ -+ registerMob("minecraft:panda"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:pillager", new DataWalkerItemLists("Inventory")); -+ V100.registerEquipment(VERSION, "minecraft:pillager"); -+ } -+ -+ private V1800() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1801.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1801.java -new file mode 100644 -index 0000000000000000000000000000000000000000..9e1a3af9fb261e585542495f189f898eaa6d9263 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1801.java -@@ -0,0 +1,18 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+ -+public final class V1801 { -+ -+ private static final int VERSION = MCVersions.V1_13_2 + 170; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ registerMob("minecraft:illager_beast"); -+ } -+ -+ private V1801() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1802.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1802.java -new file mode 100644 -index 0000000000000000000000000000000000000000..aeae0c62efa1e189fe4b0da585c8a2a101bb5ede ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1802.java -@@ -0,0 +1,28 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+ -+public final class V1802 { -+ -+ private static final int VERSION = MCVersions.V1_13_2 + 171; -+ -+ public static void register() { -+ ConverterAbstractBlockRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:stone_slab", "minecraft:smooth_stone_slab", -+ "minecraft:sign", "minecraft:oak_sign", "minecraft:wall_sign", "minecraft:oak_wall_sign" -+ ) -+ )::get); -+ ConverterAbstractItemRename.register(VERSION, new HashMap<>(ImmutableMap.of( -+ "minecraft:stone_slab", "minecraft:smooth_stone_slab", -+ "minecraft:sign", "minecraft:oak_sign" -+ ) -+ )::get); -+ } -+ -+ private V1802() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1803.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1803.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ad12a97fe28b6f05973f0927245c944dcf184c46 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1803.java -@@ -0,0 +1,46 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.util.ComponentUtils; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+ -+public final class V1803 { -+ -+ private static final int VERSION = MCVersions.V1_13_2 + 172; -+ -+ public static void register() { -+ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType tag = data.getMap("tag"); -+ -+ if (tag == null) { -+ return null; -+ } -+ -+ final MapType display = tag.getMap("display"); -+ -+ if (display == null) { -+ return null; -+ } -+ -+ final ListType lore = display.getList("Lore", ObjectType.STRING); -+ if (lore == null) { -+ return null; -+ } -+ -+ for (int i = 0, len = lore.size(); i < len; ++i) { -+ lore.setString(i, ComponentUtils.createPlainTextComponent(lore.getString(i))); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V1803() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1904.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1904.java -new file mode 100644 -index 0000000000000000000000000000000000000000..2066f320d774319bec84007ca7ed137eb78d91d1 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1904.java -@@ -0,0 +1,42 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V1904 { -+ -+ private static final int VERSION = MCVersions.V18W43C + 1; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:ocelot", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final int catType = data.getInt("CatType"); -+ -+ if (catType == 0) { -+ final String owner = data.getString("Owner"); -+ final String ownerUUID = data.getString("OwnerUUID"); -+ if ((owner != null && owner.length() > 0) || (ownerUUID != null && ownerUUID.length() > 0)) { -+ data.setBoolean("Trusting", true); -+ } -+ } else if (catType > 0 && catType < 4) { -+ data.setString("id", "minecraft:cat"); -+ data.setString("OwnerUUID", data.getString("OwnerUUID", "")); -+ } -+ -+ return null; -+ } -+ }); -+ -+ registerMob("minecraft:cat"); -+ } -+ -+ private V1904() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1905.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1905.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a4bd2c65fe5a4b4d3e430e5c7eee79435afac4ee ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1905.java -@@ -0,0 +1,34 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V1905 { -+ -+ private static final int VERSION = MCVersions.V18W43C + 2; -+ -+ public static void register() { -+ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType level = data.getMap("Level"); -+ -+ if (level == null) { -+ return null; -+ } -+ -+ final String status = level.getString("Status"); -+ -+ if ("postprocessed".equals(status)) { -+ level.setString("Status", "fullchunk"); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V1905() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1906.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1906.java -new file mode 100644 -index 0000000000000000000000000000000000000000..dbf3215a781555d048077565851884eeb48402b1 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1906.java -@@ -0,0 +1,20 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; -+ -+public final class V1906 { -+ -+ private static final int VERSION = MCVersions.V18W43C + 3; -+ -+ public static void register() { -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:barrel", new DataWalkerItemLists("Items")); -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:smoker", new DataWalkerItemLists("Items")); -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:blast_furnace", new DataWalkerItemLists("Items")); -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:lectern", new DataWalkerItems("Book")); -+ } -+ -+ private V1906() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1909.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1909.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ede4d0bfc0fe0e4a3a6fb906037a4c964baac6e6 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1909.java -@@ -0,0 +1,16 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; -+ -+public final class V1909 { -+ -+ private static final int VERSION = MCVersions.V18W45A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:jigsaw", new DataWalkerTypePaths<>(MCTypeRegistry.FLAT_BLOCK_STATE, "final_state")); -+ } -+ -+ private V1909() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1911.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1911.java -new file mode 100644 -index 0000000000000000000000000000000000000000..02204cd67dc614e95f2ab95ed413ce62baec296f ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1911.java -@@ -0,0 +1,49 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V1911 { -+ -+ private static final int VERSION = MCVersions.V18W46A + 1; -+ -+ private static final Map CHUNK_STATUS_REMAP = new HashMap<>( -+ ImmutableMap.builder() -+ .put("structure_references", "empty") -+ .put("biomes", "empty") -+ .put("base", "surface") -+ .put("carved", "carvers") -+ .put("liquid_carved", "liquid_carvers") -+ .put("decorated", "features") -+ .put("lighted", "light") -+ .put("mobs_spawned", "spawn") -+ .put("finalized", "heightmaps") -+ .put("fullchunk", "full") -+ .build() -+ ); -+ -+ public static void register() { -+ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType level = data.getMap("Level"); -+ -+ if (level == null) { -+ return null; -+ } -+ -+ final String status = level.getString("Status", "empty"); -+ level.setString("Status", CHUNK_STATUS_REMAP.getOrDefault(status, "empty")); -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V1911() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1914.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1914.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a965a5941e3624db725a4f101405357df11598c8 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1914.java -@@ -0,0 +1,28 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V1914 { -+ -+ private static final int VERSION = MCVersions.V18W48A; -+ -+ public static void register() { -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:chest", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final String lootTable = data.getString("LootTable"); -+ -+ if ("minecraft:chests/village_blacksmith".equals(lootTable)) { -+ data.setString("LootTable", "minecraft:chests/village/village_weaponsmith"); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V1914() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1917.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1917.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f8b5f5818ed4e839b62777a5d5e9baf70b12a6f0 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1917.java -@@ -0,0 +1,25 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V1917 { -+ -+ private static final int VERSION = MCVersions.V18W49A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:cat", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (data.getInt("CatType") == 9) { -+ data.setInt("CatType", 10); -+ } -+ return null; -+ } -+ }); -+ } -+ -+ private V1917() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1918.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1918.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f97f21e12af1e02aacc1591a88b5da3d7e3f4cfa ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1918.java -@@ -0,0 +1,65 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.Types; -+ -+public final class V1918 { -+ -+ private static final int VERSION = MCVersions.V18W49A + 2; -+ -+ private static String getProfessionString(final int professionId, final int careerId) { -+ if (professionId == 0) { -+ if (careerId == 2) { -+ return "minecraft:fisherman"; -+ } else if (careerId == 3) { -+ return "minecraft:shepherd"; -+ } else { -+ return careerId == 4 ? "minecraft:fletcher" : "minecraft:farmer"; -+ } -+ } else if (professionId == 1) { -+ return careerId == 2 ? "minecraft:cartographer" : "minecraft:librarian"; -+ } else if (professionId == 2) { -+ return "minecraft:cleric"; -+ } else if (professionId == 3) { -+ if (careerId == 2) { -+ return "minecraft:weaponsmith"; -+ } else { -+ return careerId == 3 ? "minecraft:toolsmith" : "minecraft:armorer"; -+ } -+ } else if (professionId == 4) { -+ return careerId == 2 ? "minecraft:leatherworker" : "minecraft:butcher"; -+ } else { -+ return professionId == 5 ? "minecraft:nitwit" : "minecraft:none"; -+ } -+ } -+ -+ public static void register() { -+ final DataConverter, MapType> converter = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final int profession = data.getInt("Profession"); -+ final int career = data.getInt("Career"); -+ final int careerLevel = data.getInt("CareerLevel", 1); -+ data.remove("Profession"); -+ data.remove("Career"); -+ data.remove("CareerLevel"); -+ -+ final MapType villagerData = Types.NBT.createEmptyMap(); -+ data.setMap("VillagerData", villagerData); -+ villagerData.setString("type", "minecraft:plains"); -+ villagerData.setString("profession", getProfessionString(profession, career)); -+ villagerData.setInt("level", careerLevel); -+ -+ return null; -+ } -+ }; -+ -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:villager", converter); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:zombie_villager", converter); -+ } -+ -+ private V1918() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1920.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1920.java -new file mode 100644 -index 0000000000000000000000000000000000000000..fe2d58caf2371f1c430dea209210357f36392a96 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1920.java -@@ -0,0 +1,75 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.util.NamespaceUtil; -+ -+public final class V1920 { -+ -+ private static final int VERSION = MCVersions.V18W50A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType level = data.getMap("Level"); -+ if (level == null) { -+ return null; -+ } -+ -+ final MapType structures = level.getMap("Structures"); -+ if (structures == null) { -+ return null; -+ } -+ -+ final MapType starts = structures.getMap("Starts"); -+ if (starts != null) { -+ final MapType village = starts.getMap("New_Village"); -+ if (village != null) { -+ starts.remove("New_Village"); -+ starts.setMap("Village", village); -+ } else { -+ starts.remove("Village"); -+ } -+ } -+ -+ final MapType references = structures.getMap("References"); -+ if (references != null) { -+ final MapType newVillage = references.getMap("New_Village"); -+ // I believe Mojang had a typo here, removing Village from references only made sense -+ // if the new village didn't exist. DFU removes it whether or not it exists, but still relocates -+ // New_Village to Village first. It doesn't make sense to me to relocate it just to remove it, so it -+ // must be a typo. -+ if (newVillage == null) { -+ references.remove("Village"); -+ } else { -+ references.remove("New_Village"); -+ references.setMap("Village", newVillage); -+ } -+ } -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.STRUCTURE_FEATURE.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final String id = data.getString("id"); -+ -+ if ("minecraft:new_village".equals(NamespaceUtil.correctNamespace(id))) { -+ data.setString("id", "minecraft:village"); -+ } -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:campfire", new DataWalkerItemLists("Items")); -+ } -+ -+ private V1920() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1925.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1925.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7f2db47a58baf1851abb9269b13fb08d4740081a ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1925.java -@@ -0,0 +1,30 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.Types; -+ -+public final class V1925 { -+ -+ private static final int VERSION = MCVersions.V19W03C + 1; -+ -+ public static void register() { -+ MCTypeRegistry.SAVED_DATA_MAP_DATA.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType root, final long sourceVersion, final long toVersion) { -+ final MapType data = root.getMap("data"); -+ if (data == null) { -+ final MapType ret = Types.NBT.createEmptyMap(); -+ ret.setMap("data", root); -+ -+ return ret; -+ } -+ return null; -+ } -+ }); -+ } -+ -+ private V1925() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1928.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1928.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f1f7cd60d3fb1d7d3de92091681932607b452d25 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1928.java -@@ -0,0 +1,33 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterAbstractEntityRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+ -+public final class V1928 { -+ -+ private static final int VERSION = MCVersions.V19W04B + 1; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ ConverterAbstractEntityRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:illager_beast", "minecraft:ravager" -+ ) -+ )::get); -+ ConverterAbstractItemRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:illager_beast_spawn_egg", "minecraft:ravager_spawn_egg" -+ ) -+ )::get); -+ -+ registerMob("minecraft:ravager"); -+ } -+ -+ private V1928() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1929.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1929.java -new file mode 100644 -index 0000000000000000000000000000000000000000..cc377819db8182b466b92aba9a9c0d2c483f941d ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1929.java -@@ -0,0 +1,34 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V1929 { -+ -+ private static final int VERSION = MCVersions.V19W04B + 2; -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:wandering_trader", (final MapType data, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, data, "Inventory", fromVersion, toVersion); -+ -+ WalkerUtils.convertList(MCTypeRegistry.VILLAGER_TRADE, data.getMap("Offers"), "Recipes", fromVersion, toVersion); -+ -+ return null; -+ }); -+ V100.registerEquipment(VERSION, "minecraft:wandering_trader"); -+ -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:trader_llama", (final MapType data, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convert(MCTypeRegistry.ITEM_STACK, data, "SaddleItem", fromVersion, toVersion); -+ WalkerUtils.convert(MCTypeRegistry.ITEM_STACK, data, "DecorItem", fromVersion, toVersion); -+ -+ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, data, "Items", fromVersion, toVersion); -+ -+ return null; -+ }); -+ V100.registerEquipment(VERSION, "minecraft:trader_llama"); -+ } -+ -+ private V1929() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1931.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1931.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0ae698a80e81a1648bb90149d9f0effdec8e777c ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1931.java -@@ -0,0 +1,19 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+ -+public final class V1931 { -+ -+ private static final int VERSION = MCVersions.V19W06A; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ registerMob("minecraft:fox"); -+ } -+ -+ private V1931() {} -+ -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1936.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1936.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ddebd1ea2eec5e469d4857503965084d78afce19 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1936.java -@@ -0,0 +1,37 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V1936 { -+ -+ private static final int VERSION = MCVersions.V19W09A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.OPTIONS.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final String chatOpacity = data.getString("chatOpacity"); -+ if (chatOpacity != null) { -+ // Vanilla uses createDouble here, but options is always string -> string. I presume they made -+ // a mistake with this converter. -+ data.setString("textBackgroundOpacity", Double.toString(calculateBackground(chatOpacity))); -+ } -+ return null; -+ } -+ }); -+ } -+ -+ private static double calculateBackground(final String opacity) { -+ try { -+ final double d = 0.9D * Double.parseDouble(opacity) + 0.1D; -+ return d / 2.0D; -+ } catch (final NumberFormatException ex) { -+ return 0.5D; -+ } -+ } -+ -+ private V1936() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1946.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1946.java -new file mode 100644 -index 0000000000000000000000000000000000000000..70d3ab9fe12fab7282edc18938faa94a34d3decb ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1946.java -@@ -0,0 +1,41 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.Types; -+ -+public final class V1946 { -+ -+ private static final int VERSION = MCVersions.V19W14B + 1; -+ -+ public static void register() { -+ MCTypeRegistry.POI_CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType sections = Types.NBT.createEmptyMap(); -+ data.setMap("Sections", sections); -+ -+ for (int y = 0; y < 16; ++y) { -+ final String key = Integer.toString(y); -+ final Object records = data.getGeneric(key); -+ -+ if (records == null) { -+ continue; -+ } -+ -+ data.remove(key); -+ -+ final MapType section = Types.NBT.createEmptyMap(); -+ section.setGeneric("Records", records); -+ sections.setMap(key, section); // integer keys convert to string in DFU (at least for NBT ops) -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V1946() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1948.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1948.java -new file mode 100644 -index 0000000000000000000000000000000000000000..19b0a1197cdf5988f21ba332883b65df646ff0c1 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1948.java -@@ -0,0 +1,39 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V1948 { -+ -+ private static final int VERSION = MCVersions.V1_14_PRE2; -+ -+ public static void register() { -+ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:white_banner", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType tag = data.getMap("tag"); -+ if (tag == null) { -+ return null; -+ } -+ -+ final MapType display = tag.getMap("display"); -+ if (display == null) { -+ return null; -+ } -+ -+ final String name = display.getString("Name"); -+ if (name == null) { -+ return null; -+ } -+ -+ display.setString("Name", name.replace("\"translate\":\"block.minecraft.illager_banner\"", "\"translate\":\"block.minecraft.ominous_banner\"")); -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V1948() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1953.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1953.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c7887c54c85dd7a198aa5c1597c02b2d6887bf71 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1953.java -@@ -0,0 +1,26 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V1953 { -+ -+ private static final int VERSION = MCVersions.V1_14 + 1; -+ -+ public static void register() { -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:banner", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final String name = data.getString("CustomName"); -+ if (name != null) { -+ data.setString("CustomName", name.replace("\"translate\":\"block.minecraft.illager_banner\"", "\"translate\":\"block.minecraft.ominous_banner\"")); -+ } -+ return null; -+ } -+ }); -+ } -+ -+ private V1953() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1955.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1955.java -new file mode 100644 -index 0000000000000000000000000000000000000000..8654f8c7f759720e1e1dd8ae94656699f151c407 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1955.java -@@ -0,0 +1,93 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.Types; -+import net.minecraft.util.Mth; -+ -+public final class V1955 { -+ -+ private static final int VERSION = MCVersions.V1_14_1_PRE1; -+ -+ private static final int[] LEVEL_XP_THRESHOLDS = new int[] { -+ 0, -+ 10, -+ 50, -+ 100, -+ 150 -+ }; -+ -+ private static int getMinXpPerLevel(final int level) { -+ return LEVEL_XP_THRESHOLDS[Mth.clamp(level - 1, 0, LEVEL_XP_THRESHOLDS.length - 1)]; -+ } -+ -+ private static void addLevel(final MapType data, final int level) { -+ MapType villagerData = data.getMap("VillagerData"); -+ if (villagerData == null) { -+ villagerData = Types.NBT.createEmptyMap(); -+ data.setMap("VillagerData", villagerData); -+ } -+ villagerData.setInt("level", level); -+ } -+ -+ private static void addXpFromLevel(final MapType data, final int level) { -+ data.setInt("Xp", getMinXpPerLevel(level)); -+ } -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:villager", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType villagerData = data.getMap("VillagerData"); -+ int level = villagerData == null ? 0 : villagerData.getInt("level"); -+ if (level == 0 || level == 1) { -+ // count recipes -+ final MapType offers = data.getMap("Offers"); -+ final ListType recipes = offers == null ? null : offers.getList("Recipes", ObjectType.MAP); -+ final int recipeCount; -+ if (recipes != null) { -+ recipeCount = recipes.size(); -+ } else { -+ recipeCount = 0; -+ } -+ -+ level = Mth.clamp(recipeCount / 2, 1, 5); -+ if (level > 1) { -+ addLevel(data, level); -+ } -+ } -+ -+ if (!data.hasKey("Xp", ObjectType.NUMBER)) { -+ addXpFromLevel(data, level); -+ } -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:zombie_villager", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final Number xp = data.getNumber("Xp"); -+ if (xp == null) { -+ final int level; -+ final MapType villagerData = data.getMap("VillagerData"); -+ if (villagerData == null) { -+ level = 1; -+ } else { -+ level = villagerData.getInt("level", 1); -+ } -+ -+ data.setInt("Xp", getMinXpPerLevel(level)); -+ } -+ return null; -+ } -+ }); -+ } -+ -+ private V1955() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1961.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1961.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4b1b9b55e2491bd98efddfb28e2aa1074140a1c2 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1961.java -@@ -0,0 +1,29 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V1961 { -+ -+ private static final int VERSION = MCVersions.V1_14_2_PRE3 + 1; -+ -+ public static void register() { -+ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType level = data.getMap("Level"); -+ if (level == null) { -+ return null; -+ } -+ -+ level.remove("isLightOn"); -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V1961() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1963.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1963.java -new file mode 100644 -index 0000000000000000000000000000000000000000..023d8b9aa7d95c674847d9c5dbe0061adcbdc4d3 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V1963.java -@@ -0,0 +1,39 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+ -+public final class V1963 { -+ -+ private static final int VERSION = MCVersions.V1_14_2; -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:villager", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final ListType gossips = data.getList("Gossips", ObjectType.MAP); -+ if (gossips == null) { -+ return null; -+ } -+ -+ for (int i = 0; i < gossips.size();) { -+ final MapType gossip = gossips.getMap(i); -+ if ("golem".equals(gossip.getString("Type"))) { -+ gossips.remove(i); -+ continue; -+ } -+ -+ ++i; -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V1963() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2100.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2100.java -new file mode 100644 -index 0000000000000000000000000000000000000000..cec032b20e834a8c6c8901e6fb2d127d7c80b353 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2100.java -@@ -0,0 +1,51 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.advancements.ConverterAbstractAdvancementsRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.recipe.ConverterAbstractRecipeRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V2100 { -+ -+ private static final int VERSION = MCVersions.V1_14_4 + 124; -+ private static final Map RECIPE_RENAMES = new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:sugar", "minecraft:sugar_from_sugar_cane" -+ ) -+ ); -+ private static final Map ADVANCEMENT_RENAMES = new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:recipes/misc/sugar", "minecraft:recipes/misc/sugar_from_sugar_cane" -+ ) -+ ); -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ ConverterAbstractRecipeRename.register(VERSION, RECIPE_RENAMES::get); -+ ConverterAbstractAdvancementsRename.register(VERSION, ADVANCEMENT_RENAMES::get); -+ -+ registerMob("minecraft:bee"); -+ registerMob("minecraft:bee_stinger"); -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:beehive", (data, fromVersion, toVersion) -> { -+ final ListType bees = data.getList("Bees", ObjectType.MAP); -+ if (bees != null) { -+ for (int i = 0, len = bees.size(); i < len; ++i) { -+ WalkerUtils.convert(MCTypeRegistry.ENTITY, bees.getMap(i), "EntityData", fromVersion, toVersion); -+ } -+ } -+ -+ return null; -+ }); -+ } -+ -+ private V2100() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2202.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2202.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c9a23cf055353ee49f07263ea01161de2c035138 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2202.java -@@ -0,0 +1,49 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V2202 { -+ -+ private static final int VERSION = MCVersions.V19W35A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType level = data.getMap("Level"); -+ if (level == null) { -+ return null; -+ } -+ -+ final int[] oldBiomes = level.getInts("Biomes"); -+ -+ if (oldBiomes == null || oldBiomes.length != 256) { -+ return null; -+ } -+ -+ final int[] newBiomes = new int[1024]; -+ level.setInts("Biomes", newBiomes); -+ -+ for (int i = 0; i < 4; ++i) { -+ for (int j = 0; j < 4; ++j) { -+ int k = (j << 2) + 2; -+ int l = (i << 2) + 2; -+ int m = l << 4 | k; -+ newBiomes[i << 2 | j] = oldBiomes[m]; -+ } -+ } -+ -+ for (int i = 1; i < 64; ++i) { -+ System.arraycopy(newBiomes, 0, newBiomes, i * 16, 16); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V2202() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2209.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2209.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7439d0e948f144d93a1fa7b57c2b478a54835d6d ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2209.java -@@ -0,0 +1,28 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.poi.ConverterAbstractPOIRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V2209 { -+ -+ private static final int VERSION = MCVersions.V19W40A + 1; -+ -+ public static void register() { -+ final Map renamedIds = new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:bee_hive", "minecraft:beehive" -+ ) -+ ); -+ -+ ConverterAbstractBlockRename.register(VERSION, renamedIds::get); -+ ConverterAbstractItemRename.register(VERSION, renamedIds::get); -+ ConverterAbstractPOIRename.register(VERSION, renamedIds::get); -+ } -+ -+ private V2209() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2211.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2211.java -new file mode 100644 -index 0000000000000000000000000000000000000000..20904d3e18b317a2f7e5d6063fcf94dda27b5768 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2211.java -@@ -0,0 +1,31 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+ -+public final class V2211 { -+ -+ private static final int VERSION = MCVersions.V19W41A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.STRUCTURE_FEATURE.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (!data.hasKey("references", ObjectType.NUMBER)) { -+ return null; -+ } -+ -+ final int references = data.getInt("references"); -+ if (references <= 0) { -+ data.setInt("references", 1); -+ } -+ return null; -+ } -+ }); -+ } -+ -+ private V2211() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2218.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2218.java -new file mode 100644 -index 0000000000000000000000000000000000000000..8297fe9ab7007399847f3e7ac84519f0dec08576 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2218.java -@@ -0,0 +1,33 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V2218 { -+ -+ private static final int VERSION = MCVersions.V1_15_PRE1; -+ -+ public static void register() { -+ MCTypeRegistry.POI_CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType sections = data.getMap("Sections"); -+ if (sections == null) { -+ return null; -+ } -+ -+ for (final String key : sections.keys()) { -+ final MapType section = sections.getMap(key); -+ -+ section.remove("Valid"); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V2218() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2501.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2501.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f2be8817fe733ae30729952a2aae13d2396b8111 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2501.java -@@ -0,0 +1,65 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.Types; -+ -+public final class V2501 { -+ -+ private static final int VERSION = MCVersions.V1_15_2 + 271; -+ -+ private static void registerFurnace(final String id) { -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, id, (data, fromVersion, toVersion) -> { -+ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, data, "Items", fromVersion, toVersion); -+ -+ WalkerUtils.convertKeys(MCTypeRegistry.RECIPE, data, "RecipesUsed", fromVersion, toVersion); -+ -+ return null; -+ }); -+ } -+ -+ public static void register() { -+ final DataConverter, MapType> converter = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final int recipesUsedSize = data.getInt("RecipesUsedSize"); -+ data.remove("RecipesUsedSize"); -+ -+ if (recipesUsedSize <= 0) { -+ return null; -+ } -+ -+ final MapType newRecipes = Types.NBT.createEmptyMap(); -+ data.setMap("RecipesUsed", newRecipes); -+ -+ for (int i = 0; i < recipesUsedSize; ++i) { -+ final String recipeKey = data.getString("RecipeLocation" + i); -+ data.remove("RecipeLocation" + i); -+ final int recipeAmount = data.getInt("RecipeAmount" + i); -+ data.remove("RecipeAmount" + i); -+ -+ if (i <= 0 || recipeKey == null) { -+ continue; -+ } -+ -+ newRecipes.setInt(recipeKey, recipeAmount); -+ } -+ -+ return null; -+ } -+ }; -+ -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:furnace", converter); -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:blast_furnace", converter); -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:smoker", converter); -+ -+ registerFurnace("minecraft:furnace"); -+ registerFurnace("minecraft:smoker"); -+ registerFurnace("minecraft:blast_furnace"); -+ } -+ -+ private V2501() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2502.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2502.java -new file mode 100644 -index 0000000000000000000000000000000000000000..540ae9aab0acdfbd3800db0468c52e973cb8d93f ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2502.java -@@ -0,0 +1,18 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+ -+public final class V2502 { -+ -+ private static final int VERSION = MCVersions.V1_15_2 + 272; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ registerMob("minecraft:hoglin"); -+ } -+ -+ private V2502() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2503.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2503.java -new file mode 100644 -index 0000000000000000000000000000000000000000..994960d0e67ed0af48d33e9a3db5d1757d85eac5 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2503.java -@@ -0,0 +1,73 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.advancements.ConverterAbstractAdvancementsRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import com.google.common.collect.ImmutableMap; -+import com.google.common.collect.ImmutableSet; -+import java.util.HashMap; -+import java.util.HashSet; -+import java.util.Set; -+ -+public final class V2503 { -+ -+ private static final int VERSION = MCVersions.V1_15_2 + 273; -+ -+ private static final Set WALL_BLOCKS = new HashSet<>( -+ ImmutableSet.of( -+ "minecraft:andesite_wall", -+ "minecraft:brick_wall", -+ "minecraft:cobblestone_wall", -+ "minecraft:diorite_wall", -+ "minecraft:end_stone_brick_wall", -+ "minecraft:granite_wall", -+ "minecraft:mossy_cobblestone_wall", -+ "minecraft:mossy_stone_brick_wall", -+ "minecraft:nether_brick_wall", -+ "minecraft:prismarine_wall", -+ "minecraft:red_nether_brick_wall", -+ "minecraft:red_sandstone_wall", -+ "minecraft:sandstone_wall", -+ "minecraft:stone_brick_wall" -+ ) -+ ); -+ -+ private static void changeWallProperty(final MapType properties, final String path) { -+ final String property = properties.getString(path); -+ if (property != null) { -+ properties.setString(path, "true".equals(property) ? "low" : "none"); -+ } -+ } -+ -+ public static void register() { -+ MCTypeRegistry.BLOCK_STATE.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (!WALL_BLOCKS.contains(data.getString("Name"))) { -+ return null; -+ } -+ -+ final MapType properties = data.getMap("Properties"); -+ if (properties == null) { -+ return null; -+ } -+ -+ changeWallProperty(properties, "east"); -+ changeWallProperty(properties, "west"); -+ changeWallProperty(properties, "north"); -+ changeWallProperty(properties, "south"); -+ -+ return null; -+ } -+ }); -+ ConverterAbstractAdvancementsRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:recipes/misc/composter", "minecraft:recipes/decorations/composter" -+ ) -+ )::get); -+ } -+ -+ private V2503() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2505.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2505.java -new file mode 100644 -index 0000000000000000000000000000000000000000..9342d9efeb1980c7cb67bf0620d12bd9f71165ee ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2505.java -@@ -0,0 +1,48 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.Types; -+ -+public final class V2505 { -+ -+ private static final int VERSION = MCVersions.V20W06A + 1; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:villager", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType brain = data.getMap("Brain"); -+ if (brain == null) { -+ return null; -+ } -+ -+ final MapType memories = brain.getMap("memories"); -+ if (memories == null) { -+ return null; -+ } -+ -+ for (final String key : memories.keys()) { -+ final Object value = memories.getGeneric(key); -+ -+ final MapType wrapped = Types.NBT.createEmptyMap(); -+ wrapped.setGeneric("value", value); -+ -+ memories.setMap(key, wrapped); -+ } -+ -+ return null; -+ } -+ }); -+ -+ registerMob("minecraft:piglin"); -+ } -+ -+ private V2505() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2508.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2508.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f9e9d88e4cca15d2d4fdcbc0dbcae4c35c02284a ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2508.java -@@ -0,0 +1,27 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V2508 { -+ -+ private static final int VERSION = MCVersions.V20W08A + 1; -+ -+ public static void register() { -+ final Map remap = new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:warped_fungi", "minecraft:warped_fungus", -+ "minecraft:crimson_fungi", "minecraft:crimson_fungus" -+ ) -+ ); -+ -+ ConverterAbstractBlockRename.register(VERSION, remap::get); -+ ConverterAbstractItemRename.register(VERSION, remap::get); -+ } -+ -+ private V2508() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2509.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2509.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b948564d01726d9891a0733896b3e5cec937bd6d ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2509.java -@@ -0,0 +1,33 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterAbstractEntityRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+ -+public final class V2509 { -+ -+ private static final int VERSION = MCVersions.V20W08A + 2; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ ConverterAbstractItemRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:zombie_pigman_spawn_egg", "minecraft:zombified_piglin_spawn_egg" -+ ) -+ )::get); -+ ConverterAbstractEntityRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:zombie_pigman", "minecraft:zombified_piglin" -+ ) -+ )::get); -+ -+ registerMob("minecraft:zombified_piglin"); -+ } -+ -+ private V2509() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2511.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2511.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a640878469c7ea155cde1cca728b15f2a4bacd73 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2511.java -@@ -0,0 +1,97 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.Types; -+ -+public final class V2511 { -+ -+ private static final int VERSION = MCVersions.V20W09A + 1; -+ -+ private static int[] createUUIDArray(final long most, final long least) { -+ return new int[] { -+ (int)(most >>> 32), -+ (int)most, -+ (int)(least >>> 32), -+ (int)least -+ }; -+ } -+ -+ private static void setUUID(final MapType data, final long most, final long least) { -+ if (most != 0L && least != 0L) { -+ data.setInts("OwnerUUID", createUUIDArray(most, least)); -+ } -+ } -+ -+ public static void register() { -+ final DataConverter, MapType> throwableConverter = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType owner = data.getMap("owner"); -+ data.remove("owner"); -+ if (owner == null) { -+ return null; -+ } -+ -+ setUUID(data, owner.getLong("M"), owner.getLong("L")); -+ -+ return null; -+ } -+ }; -+ final DataConverter, MapType> potionConverter = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType potion = data.getMap("Potion"); -+ data.remove("Potion"); -+ -+ data.setMap("Item", potion == null ? Types.NBT.createEmptyMap() : potion); -+ -+ return null; -+ } -+ }; -+ final DataConverter, MapType> llamaSpitConverter = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType owner = data.getMap("Owner"); -+ data.remove("Owner"); -+ if (owner == null) { -+ return null; -+ } -+ -+ setUUID(data, owner.getLong("OwnerUUIDMost"), owner.getLong("OwnerUUIDLeast")); -+ -+ return null; -+ } -+ }; -+ final DataConverter, MapType> arrowConverter = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ setUUID(data, data.getLong("OwnerUUIDMost"), data.getLong("OwnerUUIDLeast")); -+ -+ data.remove("OwnerUUIDMost"); -+ data.remove("OwnerUUIDLeast"); -+ -+ return null; -+ } -+ }; -+ -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:egg", throwableConverter); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:ender_pearl", throwableConverter); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:experience_bottle", throwableConverter); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:snowball", throwableConverter); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:potion", throwableConverter); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:potion", potionConverter); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:llama_spit", llamaSpitConverter); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:arrow", arrowConverter); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:spectral_arrow", arrowConverter); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:trident", arrowConverter); -+ -+ // Vanilla migrates the potion item but does not change the schema. -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:potion", new DataWalkerItems("Item")); -+ } -+ -+ private V2511() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2514.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2514.java -new file mode 100644 -index 0000000000000000000000000000000000000000..dcd2b1689bbd845238c86cea9dae0c5153d01499 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2514.java -@@ -0,0 +1,590 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.Types; -+import com.google.common.collect.Sets; -+import java.util.Set; -+import java.util.UUID; -+ -+public final class V2514 { -+ -+ private static final int VERSION = MCVersions.V20W11A + 1; -+ -+ private static final Set ABSTRACT_HORSES = Sets.newHashSet(); -+ private static final Set TAMEABLE_ANIMALS = Sets.newHashSet(); -+ private static final Set ANIMALS = Sets.newHashSet(); -+ private static final Set MOBS = Sets.newHashSet(); -+ private static final Set LIVING_ENTITIES = Sets.newHashSet(); -+ private static final Set PROJECTILES = Sets.newHashSet(); -+ static { -+ ABSTRACT_HORSES.add("minecraft:donkey"); -+ ABSTRACT_HORSES.add("minecraft:horse"); -+ ABSTRACT_HORSES.add("minecraft:llama"); -+ ABSTRACT_HORSES.add("minecraft:mule"); -+ ABSTRACT_HORSES.add("minecraft:skeleton_horse"); -+ ABSTRACT_HORSES.add("minecraft:trader_llama"); -+ ABSTRACT_HORSES.add("minecraft:zombie_horse"); -+ -+ TAMEABLE_ANIMALS.add("minecraft:cat"); -+ TAMEABLE_ANIMALS.add("minecraft:parrot"); -+ TAMEABLE_ANIMALS.add("minecraft:wolf"); -+ -+ ANIMALS.add("minecraft:bee"); -+ ANIMALS.add("minecraft:chicken"); -+ ANIMALS.add("minecraft:cow"); -+ ANIMALS.add("minecraft:fox"); -+ ANIMALS.add("minecraft:mooshroom"); -+ ANIMALS.add("minecraft:ocelot"); -+ ANIMALS.add("minecraft:panda"); -+ ANIMALS.add("minecraft:pig"); -+ ANIMALS.add("minecraft:polar_bear"); -+ ANIMALS.add("minecraft:rabbit"); -+ ANIMALS.add("minecraft:sheep"); -+ ANIMALS.add("minecraft:turtle"); -+ ANIMALS.add("minecraft:hoglin"); -+ -+ MOBS.add("minecraft:bat"); -+ MOBS.add("minecraft:blaze"); -+ MOBS.add("minecraft:cave_spider"); -+ MOBS.add("minecraft:cod"); -+ MOBS.add("minecraft:creeper"); -+ MOBS.add("minecraft:dolphin"); -+ MOBS.add("minecraft:drowned"); -+ MOBS.add("minecraft:elder_guardian"); -+ MOBS.add("minecraft:ender_dragon"); -+ MOBS.add("minecraft:enderman"); -+ MOBS.add("minecraft:endermite"); -+ MOBS.add("minecraft:evoker"); -+ MOBS.add("minecraft:ghast"); -+ MOBS.add("minecraft:giant"); -+ MOBS.add("minecraft:guardian"); -+ MOBS.add("minecraft:husk"); -+ MOBS.add("minecraft:illusioner"); -+ MOBS.add("minecraft:magma_cube"); -+ MOBS.add("minecraft:pufferfish"); -+ MOBS.add("minecraft:zombified_piglin"); -+ MOBS.add("minecraft:salmon"); -+ MOBS.add("minecraft:shulker"); -+ MOBS.add("minecraft:silverfish"); -+ MOBS.add("minecraft:skeleton"); -+ MOBS.add("minecraft:slime"); -+ MOBS.add("minecraft:snow_golem"); -+ MOBS.add("minecraft:spider"); -+ MOBS.add("minecraft:squid"); -+ MOBS.add("minecraft:stray"); -+ MOBS.add("minecraft:tropical_fish"); -+ MOBS.add("minecraft:vex"); -+ MOBS.add("minecraft:villager"); -+ MOBS.add("minecraft:iron_golem"); -+ MOBS.add("minecraft:vindicator"); -+ MOBS.add("minecraft:pillager"); -+ MOBS.add("minecraft:wandering_trader"); -+ MOBS.add("minecraft:witch"); -+ MOBS.add("minecraft:wither"); -+ MOBS.add("minecraft:wither_skeleton"); -+ MOBS.add("minecraft:zombie"); -+ MOBS.add("minecraft:zombie_villager"); -+ MOBS.add("minecraft:phantom"); -+ MOBS.add("minecraft:ravager"); -+ MOBS.add("minecraft:piglin"); -+ -+ LIVING_ENTITIES.add("minecraft:armor_stand"); -+ -+ PROJECTILES.add("minecraft:arrow"); -+ PROJECTILES.add("minecraft:dragon_fireball"); -+ PROJECTILES.add("minecraft:firework_rocket"); -+ PROJECTILES.add("minecraft:fireball"); -+ PROJECTILES.add("minecraft:llama_spit"); -+ PROJECTILES.add("minecraft:small_fireball"); -+ PROJECTILES.add("minecraft:snowball"); -+ PROJECTILES.add("minecraft:spectral_arrow"); -+ PROJECTILES.add("minecraft:egg"); -+ PROJECTILES.add("minecraft:ender_pearl"); -+ PROJECTILES.add("minecraft:experience_bottle"); -+ PROJECTILES.add("minecraft:potion"); -+ PROJECTILES.add("minecraft:trident"); -+ PROJECTILES.add("minecraft:wither_skull"); -+ } -+ -+ static int[] createUUIDArray(final long most, final long least) { -+ return new int[] { -+ (int)(most >>> 32), -+ (int)most, -+ (int)(least >>> 32), -+ (int)least -+ }; -+ } -+ -+ static int[] createUUIDFromString(final MapType data, final String path) { -+ if (data == null) { -+ return null; -+ } -+ -+ final String uuidString = data.getString(path); -+ if (uuidString == null) { -+ return null; -+ } -+ -+ try { -+ final UUID uuid = UUID.fromString(uuidString); -+ return createUUIDArray(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits()); -+ } catch (final IllegalArgumentException ignore) { -+ return null; -+ } -+ } -+ -+ static int[] createUUIDFromLongs(final MapType data, final String most, final String least) { -+ if (data == null) { -+ return null; -+ } -+ -+ final long mostBits = data.getLong(most); -+ final long leastBits = data.getLong(least); -+ -+ return (mostBits != 0 || leastBits != 0) ? createUUIDArray(mostBits, leastBits) : null; -+ } -+ -+ static void replaceUUIDString(final MapType data, final String oldPath, final String newPath) { -+ final int[] newUUID = createUUIDFromString(data, oldPath); -+ if (newUUID != null) { -+ data.remove(oldPath); -+ data.setInts(newPath, newUUID); -+ } -+ } -+ -+ static void replaceUUIDMLTag(final MapType data, final String oldPath, final String newPath) { -+ final int[] uuid = createUUIDFromLongs(data.getMap(oldPath), "M", "L"); -+ if (uuid != null) { -+ data.remove(oldPath); -+ data.setInts(newPath, uuid); -+ } -+ } -+ -+ static void replaceUUIDLeastMost(final MapType data, final String prefix, final String newPath) { -+ final String mostPath = prefix.concat("Most"); -+ final String leastPath = prefix.concat("Least"); -+ -+ final int[] uuid = createUUIDFromLongs(data, mostPath, leastPath); -+ if (uuid != null) { -+ data.remove(mostPath); -+ data.remove(leastPath); -+ data.setInts(newPath, uuid); -+ } -+ } -+ -+ private static void updatePiglin(final MapType data) { -+ final MapType brain = data.getMap("Brain"); -+ if (brain == null) { -+ return; -+ } -+ -+ final MapType memories = brain.getMap("memories"); -+ if (memories == null) { -+ return; -+ } -+ -+ final MapType angryAt = memories.getMap("minecraft:angry_at"); -+ -+ replaceUUIDString(angryAt, "value", "value"); -+ } -+ -+ private static void updateEvokerFangs(final MapType data) { -+ replaceUUIDLeastMost(data, "OwnerUUID", "Owner"); -+ } -+ -+ private static void updateZombieVillager(final MapType data) { -+ replaceUUIDLeastMost(data, "ConversionPlayer", "ConversionPlayer"); -+ } -+ -+ private static void updateAreaEffectCloud(final MapType data) { -+ replaceUUIDLeastMost(data, "OwnerUUID", "Owner"); -+ } -+ -+ private static void updateShulkerBullet(final MapType data) { -+ replaceUUIDMLTag(data, "Owner", "Owner"); -+ replaceUUIDMLTag(data, "Target", "Target"); -+ } -+ -+ private static void updateItem(final MapType data) { -+ replaceUUIDMLTag(data, "Owner", "Owner"); -+ replaceUUIDMLTag(data, "Thrower", "Thrower"); -+ } -+ -+ private static void updateFox(final MapType data) { -+ final ListType trustedUUIDS = data.getList("TrustedUUIDs", ObjectType.MAP); -+ if (trustedUUIDS == null) { -+ return; -+ } -+ -+ final ListType newUUIDs = Types.NBT.createEmptyList(); -+ data.remove("TrustedUUIDs"); -+ data.setList("Trusted", newUUIDs); -+ -+ for (int i = 0, len = trustedUUIDS.size(); i < len; ++i) { -+ final MapType uuid = trustedUUIDS.getMap(i); -+ final int[] newUUID = createUUIDFromLongs(uuid, "M", "L"); -+ if (newUUID != null) { -+ newUUIDs.addIntArray(newUUID); -+ } -+ } -+ } -+ -+ private static void updateHurtBy(final MapType data) { -+ replaceUUIDString(data, "HurtBy", "HurtBy"); -+ } -+ -+ private static void updateAnimalOwner(final MapType data) { -+ updateAnimal(data); -+ -+ replaceUUIDString(data, "OwnerUUID", "Owner"); -+ } -+ -+ private static void updateAnimal(final MapType data) { -+ updateMob(data); -+ -+ replaceUUIDLeastMost(data, "LoveCause", "LoveCause"); -+ } -+ -+ private static void updateMob(final MapType data) { -+ updateLivingEntity(data); -+ -+ final MapType leash = data.getMap("Leash"); -+ if (leash == null) { -+ return; -+ } -+ -+ replaceUUIDLeastMost(leash, "UUID", "UUID"); -+ } -+ -+ private static void updateLivingEntity(final MapType data) { -+ final ListType attributes = data.getList("Attributes", ObjectType.MAP); -+ if (attributes == null) { -+ return; -+ } -+ -+ for (int i = 0, len = attributes.size(); i < len; ++i) { -+ final MapType attribute = attributes.getMap(i); -+ -+ final ListType modifiers = attribute.getList("Modifiers", ObjectType.MAP); -+ if (modifiers == null) { -+ continue; -+ } -+ -+ for (int k = 0; k < modifiers.size(); ++k) { -+ replaceUUIDLeastMost(modifiers.getMap(k), "UUID", "UUID"); -+ } -+ } -+ } -+ -+ private static void updateProjectile(final MapType data) { -+ final Object ownerUUID = data.getGeneric("OwnerUUID"); -+ if (ownerUUID != null) { -+ data.remove("OwnerUUID"); -+ data.setGeneric("Owner", ownerUUID); -+ } -+ } -+ -+ private static void updateEntityUUID(final MapType data) { -+ replaceUUIDLeastMost(data, "UUID", "UUID"); -+ } -+ -+ public static void register() { -+ // Entity UUID fixes -+ -+ MCTypeRegistry.ENTITY.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ updateEntityUUID(data); -+ return null; -+ } -+ }); -+ -+ final DataConverter, MapType> animalOwnerConverter = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ updateAnimalOwner(data); -+ return null; -+ } -+ }; -+ final DataConverter, MapType> animalConverter = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ updateAnimal(data); -+ return null; -+ } -+ }; -+ final DataConverter, MapType> mobConverter = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ updateMob(data); -+ return null; -+ } -+ }; -+ final DataConverter, MapType> livingEntityConverter = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ updateLivingEntity(data); -+ return null; -+ } -+ }; -+ final DataConverter, MapType> projectileConverter = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ updateProjectile(data); -+ return null; -+ } -+ }; -+ for (final String id : ABSTRACT_HORSES) { -+ MCTypeRegistry.ENTITY.addConverterForId(id, animalOwnerConverter); -+ } -+ for (final String id : TAMEABLE_ANIMALS) { -+ MCTypeRegistry.ENTITY.addConverterForId(id, animalOwnerConverter); -+ } -+ for (final String id : ANIMALS) { -+ MCTypeRegistry.ENTITY.addConverterForId(id, animalConverter); -+ } -+ for (final String id : MOBS) { -+ MCTypeRegistry.ENTITY.addConverterForId(id, mobConverter); -+ } -+ for (final String id : LIVING_ENTITIES) { -+ MCTypeRegistry.ENTITY.addConverterForId(id, livingEntityConverter); -+ } -+ for (final String id : PROJECTILES) { -+ MCTypeRegistry.ENTITY.addConverterForId(id, projectileConverter); -+ } -+ -+ -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:bee", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ updateHurtBy(data); -+ return null; -+ } -+ }); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:zombified_piglin", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ updateHurtBy(data); -+ return null; -+ } -+ }); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:fox", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ updateFox(data); -+ return null; -+ } -+ }); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:item", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ updateItem(data); -+ return null; -+ } -+ }); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:shulker_bullet", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ updateShulkerBullet(data); -+ return null; -+ } -+ }); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:area_effect_cloud", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ updateAreaEffectCloud(data); -+ return null; -+ } -+ }); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:zombie_villager", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ updateZombieVillager(data); -+ return null; -+ } -+ }); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:evoker_fangs", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ updateEvokerFangs(data); -+ return null; -+ } -+ }); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:piglin", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ updatePiglin(data); -+ return null; -+ } -+ }); -+ -+ -+ // Update TE -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:conduit", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ replaceUUIDMLTag(data, "target_uuid", "Target"); -+ return null; -+ } -+ }); -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:skull", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType owner = data.getMap("Owner"); -+ if (owner == null) { -+ return null; -+ } -+ -+ data.remove("Owner"); -+ -+ replaceUUIDString(owner, "Id", "Id"); -+ -+ data.setMap("SkullOwner", owner); -+ -+ return null; -+ } -+ }); -+ -+ // Player UUID -+ MCTypeRegistry.PLAYER.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ updateLivingEntity(data); -+ updateEntityUUID(data); -+ -+ final MapType rootVehicle = data.getMap("RootVehicle"); -+ if (rootVehicle == null) { -+ return null; -+ } -+ -+ replaceUUIDLeastMost(rootVehicle, "Attach", "Attach"); -+ -+ return null; -+ } -+ }); -+ -+ // Level.dat -+ MCTypeRegistry.LEVEL.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ replaceUUIDString(data, "WanderingTraderId", "WanderingTraderId"); -+ -+ final MapType dimensionData = data.getMap("DimensionData"); -+ if (dimensionData != null) { -+ for (final String key : dimensionData.keys()) { -+ final MapType dimension = dimensionData.getMap(key); -+ -+ final MapType dragonFight = dimension.getMap("DragonFight"); -+ if (dragonFight == null) { -+ continue; -+ } -+ -+ replaceUUIDLeastMost(dragonFight, "DragonUUID", "Dragon"); -+ } -+ } -+ -+ final MapType customBossEvents = data.getMap("CustomBossEvents"); -+ if (customBossEvents != null) { -+ for (final String key : customBossEvents.keys()) { -+ final MapType customBossEvent = customBossEvents.getMap(key); -+ -+ final ListType players = customBossEvent.getList("Players", ObjectType.MAP); -+ if (players == null) { -+ continue; -+ } -+ -+ final ListType newPlayers = Types.NBT.createEmptyList(); -+ customBossEvent.setList("Players", newPlayers); -+ -+ for (int i = 0, len = players.size(); i < len; ++i) { -+ final int[] newUUID = createUUIDFromLongs(players.getMap(i), "M", "L"); -+ if (newUUID != null) { -+ newPlayers.addIntArray(newUUID); -+ } -+ } -+ } -+ } -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.SAVED_DATA_RAIDS.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType root, final long sourceVersion, final long toVersion) { -+ final MapType data = root.getMap("data"); -+ if (data == null) { -+ return null; -+ } -+ -+ final ListType raids = data.getList("Raids", ObjectType.MAP); -+ if (raids == null) { -+ return null; -+ } -+ -+ for (int i = 0, len = raids.size(); i < len; ++i) { -+ final MapType raid = raids.getMap(i); -+ -+ final ListType heros = raid.getList("HeroesOfTheVillage", ObjectType.MAP); -+ -+ if (heros == null) { -+ continue; -+ } -+ -+ final ListType newHeros = Types.NBT.createEmptyList(); -+ raid.setList("HeroesOfTheVillage", newHeros); -+ -+ for (int k = 0, klen = heros.size(); k < klen; ++k) { -+ final MapType uuidOld = heros.getMap(i); -+ final int[] uuidNew = createUUIDFromLongs(uuidOld, "UUIDMost", "UUIDLeast"); -+ if (uuidNew != null) { -+ newHeros.addIntArray(uuidNew); -+ } -+ } -+ } -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType tag = data.getMap("tag"); -+ if (tag == null) { -+ return null; -+ } -+ -+ updateAttributeModifiers(tag); -+ -+ if ("minecraft:player_head".equals(data.getString("id"))) { -+ updateSkullOwner(tag); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private static void updateAttributeModifiers(final MapType tag) { -+ final ListType attributes = tag.getList("AttributeModifiers", ObjectType.MAP); -+ if (attributes == null) { -+ return; -+ } -+ -+ for (int i = 0, len = attributes.size(); i < len; ++i) { -+ replaceUUIDLeastMost(attributes.getMap(i), "UUID", "UUID"); -+ } -+ } -+ -+ private static void updateSkullOwner(final MapType tag) { -+ replaceUUIDString(tag.getMap("SkullOwner"), "Id", "Id"); -+ } -+ -+ private V2514() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2516.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2516.java -new file mode 100644 -index 0000000000000000000000000000000000000000..99f65d84ffaa75db3d2b4568c92d85d3ef20b77f ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2516.java -@@ -0,0 +1,37 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+ -+public final class V2516 { -+ -+ private static final int VERSION = MCVersions.V20W12A + 1; -+ -+ public static void register() { -+ final DataConverter, MapType> gossipUUIDConverter = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final ListType gossips = data.getList("Gossips", ObjectType.MAP); -+ -+ if (gossips == null) { -+ return null; -+ } -+ -+ for (int i = 0, len = gossips.size(); i < len; ++i) { -+ V2514.replaceUUIDLeastMost(gossips.getMap(i), "Target", "Target"); -+ } -+ -+ return null; -+ } -+ }; -+ -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:villager", gossipUUIDConverter); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:zombie_villager", gossipUUIDConverter); -+ } -+ -+ private V2516() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2518.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2518.java -new file mode 100644 -index 0000000000000000000000000000000000000000..35eccf43fd7e31071a9d64883212cddf021ae861 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2518.java -@@ -0,0 +1,65 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V2518 { -+ -+ private static final int VERSION = MCVersions.V20W12A + 3; -+ -+ private static final Map FACING_RENAMES = new HashMap<>( -+ ImmutableMap.builder() -+ .put("down", "down_south") -+ .put("up", "up_north") -+ .put("north", "north_up") -+ .put("south", "south_up") -+ .put("west", "west_up") -+ .put("east", "east_up") -+ .build() -+ ); -+ -+ public static void register() { -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:jigsaw", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final String type = data.getString("attachement_type", "minecraft:empty"); -+ final String pool = data.getString("target_pool", "minecraft:empty"); -+ data.remove("attachement_type"); -+ data.remove("target_pool"); -+ -+ data.setString("name", type); -+ data.setString("target", type); -+ data.setString("pool", pool); -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.BLOCK_STATE.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (!"minecraft:jigsaw".equals(data.getString("Name"))) { -+ return null; -+ } -+ -+ final MapType properties = data.getMap("Properties"); -+ if (properties == null) { -+ return null; -+ } -+ -+ final String facing = properties.getString("facing", "north"); -+ properties.remove("facing"); -+ properties.setString("orientation", FACING_RENAMES.getOrDefault(facing, facing)); -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V2518() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2519.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2519.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7cb7106037b18c0cf8ddff1f9ba25d4f987a6326 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2519.java -@@ -0,0 +1,18 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+ -+public final class V2519 { -+ -+ private static final int VERSION = MCVersions.V20W12A + 4; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ registerMob("minecraft:strider"); -+ } -+ -+ private V2519() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2522.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2522.java -new file mode 100644 -index 0000000000000000000000000000000000000000..9a4d47d78596e2275745673f31f772f0252f2cda ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2522.java -@@ -0,0 +1,18 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+ -+public final class V2522 { -+ -+ private static final int VERSION = MCVersions.V20W13B + 1; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ registerMob("minecraft:zoglin"); -+ } -+ -+ private V2522() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2523.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2523.java -new file mode 100644 -index 0000000000000000000000000000000000000000..af295e8800b6eb5c13bbb88d21b391e5cc4bee1a ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2523.java -@@ -0,0 +1,41 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.attributes.ConverterAbstractAttributesRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V2523 { -+ -+ private static final int VERSION = MCVersions.V20W13B + 2; -+ -+ private static final Map RENAMES = new HashMap<>( -+ ImmutableMap.builder() -+ .put("generic.maxHealth", "minecraft:generic.max_health") -+ .put("Max Health", "minecraft:generic.max_health") -+ .put("zombie.spawnReinforcements", "minecraft:zombie.spawn_reinforcements") -+ .put("Spawn Reinforcements Chance", "minecraft:zombie.spawn_reinforcements") -+ .put("horse.jumpStrength", "minecraft:horse.jump_strength") -+ .put("Jump Strength", "minecraft:horse.jump_strength") -+ .put("generic.followRange", "minecraft:generic.follow_range") -+ .put("Follow Range", "minecraft:generic.follow_range") -+ .put("generic.knockbackResistance", "minecraft:generic.knockback_resistance") -+ .put("Knockback Resistance", "minecraft:generic.knockback_resistance") -+ .put("generic.movementSpeed", "minecraft:generic.movement_speed") -+ .put("Movement Speed", "minecraft:generic.movement_speed") -+ .put("generic.flyingSpeed", "minecraft:generic.flying_speed") -+ .put("Flying Speed", "minecraft:generic.flying_speed") -+ .put("generic.attackDamage", "minecraft:generic.attack_damage") -+ .put("generic.attackKnockback", "minecraft:generic.attack_knockback") -+ .put("generic.attackSpeed", "minecraft:generic.attack_speed") -+ .put("generic.armorToughness", "minecraft:generic.armor_toughness") -+ .build() -+ ); -+ -+ public static void register() { -+ ConverterAbstractAttributesRename.register(VERSION, RENAMES::get); -+ } -+ -+ private V2523() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2527.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2527.java -new file mode 100644 -index 0000000000000000000000000000000000000000..157f4b1673f7b71942949d979890b30a5f9e2ca3 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2527.java -@@ -0,0 +1,123 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import com.mojang.datafixers.DataFixUtils; -+import net.minecraft.util.Mth; -+ -+public final class V2527 { -+ -+ private static final int VERSION = MCVersions.V20W16A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType level = data.getMap("Level"); -+ -+ if (level == null) { -+ return null; -+ } -+ -+ final ListType sections = level.getList("Sections", ObjectType.MAP); -+ if (sections != null) { -+ for (int i = 0, len = sections.size(); i < len; ++i) { -+ final MapType section = sections.getMap(i); -+ -+ final ListType palette = section.getList("Palette", ObjectType.MAP); -+ -+ if (palette == null) { -+ continue; -+ } -+ -+ final int bits = Math.max(4, DataFixUtils.ceillog2(palette.size())); -+ -+ if (Mth.isPowerOfTwo(bits)) { -+ // fits perfectly -+ continue; -+ } -+ -+ final long[] states = section.getLongs("BlockStates"); -+ if (states == null) { -+ // wat -+ continue; -+ } -+ -+ section.setLongs("BlockStates", addPadding(4096, bits, states)); -+ } -+ } -+ -+ final MapType heightMaps = level.getMap("Heightmaps"); -+ if (heightMaps != null) { -+ for (final String key : heightMaps.keys()) { -+ final long[] old = heightMaps.getLongs(key); -+ heightMaps.setLongs(key, addPadding(256, 9, old)); -+ } -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ public static long[] addPadding(final int indices, final int bits, final long[] old) { -+ int k = old.length; -+ if (k == 0) { -+ return old; -+ } else { -+ long l = (1L << bits) - 1L; -+ int m = 64 / bits; -+ int n = (indices + m - 1) / m; -+ long[] padded = new long[n]; -+ int o = 0; -+ int p = 0; -+ long q = 0L; -+ int r = 0; -+ long s = old[0]; -+ long t = k > 1 ? old[1] : 0L; -+ -+ for(int u = 0; u < indices; ++u) { -+ int v = u * bits; -+ int w = v >> 6; -+ int x = (u + 1) * bits - 1 >> 6; -+ int y = v ^ w << 6; -+ if (w != r) { -+ s = t; -+ t = w + 1 < k ? old[w + 1] : 0L; -+ r = w; -+ } -+ -+ long ab; -+ int ac; -+ if (w == x) { -+ ab = s >>> y & l; -+ } else { -+ ac = 64 - y; -+ ab = (s >>> y | t << ac) & l; -+ } -+ -+ ac = p + bits; -+ if (ac >= 64) { -+ padded[o++] = q; -+ q = ab; -+ p = bits; -+ } else { -+ q |= ab << p; -+ p = ac; -+ } -+ } -+ -+ if (q != 0L) { -+ padded[o] = q; -+ } -+ -+ return padded; -+ } -+ } -+ -+ private V2527() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2528.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2528.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e7197d098b3d6269d3a4fd9be0432d85f0504dfd ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2528.java -@@ -0,0 +1,30 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+ -+public final class V2528 { -+ -+ private static final int VERSION = MCVersions.V20W16A + 2; -+ -+ public static void register() { -+ ConverterAbstractItemRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:soul_fire_torch", "minecraft:soul_torch", -+ "minecraft:soul_fire_lantern", "minecraft:soul_lantern" -+ ) -+ )::get); -+ ConverterAbstractBlockRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:soul_fire_torch", "minecraft:soul_torch", -+ "minecraft:soul_fire_wall_torch", "minecraft:soul_wall_torch", -+ "minecraft:soul_fire_lantern", "minecraft:soul_lantern" -+ ) -+ )::get); -+ } -+ -+ private V2528() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2529.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2529.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4e54a4ee0c14109609d8d8f1bc6c0c5dabf4fb07 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2529.java -@@ -0,0 +1,25 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V2529 { -+ -+ private static final int VERSION = MCVersions.V20W17A; -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:strider", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (data.getBoolean("NoGravity")) { -+ data.setBoolean("NoGravity", false); -+ } -+ return null; -+ } -+ }); -+ } -+ -+ private V2529() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2531.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2531.java -new file mode 100644 -index 0000000000000000000000000000000000000000..9306ab25feae6315e48aeeb71de960bdf62bcf76 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2531.java -@@ -0,0 +1,63 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V2531 { -+ -+ private static final int VERSION = MCVersions.V20W17A + 2; -+ -+ private static boolean isConnected(final String facing) { -+ return !"none".equals(facing); -+ } -+ -+ public static void register() { -+ MCTypeRegistry.BLOCK_STATE.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (!"minecraft:redstone_wire".equals(data.getString("Name"))) { -+ return null; -+ } -+ -+ final MapType properties = data.getMap("Properties"); -+ -+ if (properties == null) { -+ return null; -+ } -+ -+ -+ final String east = properties.getString("east", "none"); -+ final String west = properties.getString("west", "none"); -+ final String north = properties.getString("north", "none"); -+ final String south = properties.getString("south", "none"); -+ -+ final boolean connectedX = isConnected(east) || isConnected(west); -+ final boolean connectedZ = isConnected(north) || isConnected(south); -+ -+ final String newEast = !isConnected(east) && !connectedZ ? "side" : east; -+ final String newWest = !isConnected(west) && !connectedZ ? "side" : west; -+ final String newNorth = !isConnected(north) && !connectedX ? "side" : north; -+ final String newSouth = !isConnected(south) && !connectedX ? "side" : south; -+ -+ if (properties.hasKey("east")) { -+ properties.setString("east", newEast); -+ } -+ if (properties.hasKey("west")) { -+ properties.setString("west", newWest); -+ } -+ if (properties.hasKey("north")) { -+ properties.setString("north", newNorth); -+ } -+ if (properties.hasKey("south")) { -+ properties.setString("south", newSouth); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V2531() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2533.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2533.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f8d493674380d53398c853899da76024c9d84984 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2533.java -@@ -0,0 +1,42 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+ -+public final class V2533 { -+ -+ private static final int VERSION = MCVersions.V20W18A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:villager", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final ListType attributes = data.getList("Attributes", ObjectType.MAP); -+ -+ if (attributes == null) { -+ return null; -+ } -+ -+ for (int i = 0, len = attributes.size(); i < len; ++i) { -+ final MapType attribute = attributes.getMap(i); -+ -+ if (!"generic.follow_range".equals(attribute.getString("Name"))) { -+ continue; -+ } -+ -+ if (attribute.getDouble("Base") == 16.0) { -+ attribute.setDouble("Base", 48.0); -+ } -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V2533() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2535.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2535.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c0f6135fff38100c1955d64ee3f4ff984308e503 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2535.java -@@ -0,0 +1,34 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+ -+public final class V2535 { -+ -+ private static final int VERSION = MCVersions.V20W19A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:shulker", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ // Mojang uses doubles for whatever reason... rotation is in FLOAT. by using double here -+ // the entity load will just ignore rotation and set it to 0... -+ final ListType rotation = data.getList("Rotation", ObjectType.FLOAT); -+ -+ if (rotation == null || rotation.size() == 0) { -+ return null; -+ } -+ -+ rotation.setFloat(0, rotation.getFloat(0) - 180.0F); -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V2535() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2538.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2538.java -new file mode 100644 -index 0000000000000000000000000000000000000000..99d1df6362b290fdaa65385168ff6588647a8056 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2538.java -@@ -0,0 +1,43 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V2538 { -+ -+ private static final int VERSION = MCVersions.V20W20B + 1; -+ private static final String[] MERGE_KEYS = new String[] { -+ "RandomSeed", -+ "generatorName", -+ "generatorOptions", -+ "generatorVersion", -+ "legacy_custom_options", -+ "MapFeatures", -+ "BonusChest" -+ }; -+ -+ public static void register() { -+ MCTypeRegistry.LEVEL.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType worldGenSettings = data.getOrCreateMap("WorldGenSettings"); -+ -+ for (final String key : MERGE_KEYS) { -+ final Object value = data.getGeneric(key); -+ if (value == null) { -+ continue; -+ } -+ -+ data.remove(key); -+ worldGenSettings.setGeneric(key, value); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V2538() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2550.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2550.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f64f2c2d6051b7e7024a0ebc42c1dd8dc6434cf9 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2550.java -@@ -0,0 +1,346 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.Types; -+import com.google.common.collect.ImmutableMap; -+import org.apache.commons.lang3.math.NumberUtils; -+import java.util.HashMap; -+import java.util.Locale; -+import java.util.Map; -+ -+public final class V2550 { -+ -+ private static final int VERSION = MCVersions.V20W20B + 13; -+ -+ private static final Map DEFAULTS = new HashMap<>( -+ ImmutableMap.builder() -+ .put("minecraft:village", new StructureFeatureConfiguration(32, 8, 10387312)) -+ .put("minecraft:desert_pyramid", new StructureFeatureConfiguration(32, 8, 14357617)) -+ .put("minecraft:igloo", new StructureFeatureConfiguration(32, 8, 14357618)) -+ .put("minecraft:jungle_pyramid", new StructureFeatureConfiguration(32, 8, 14357619)) -+ .put("minecraft:swamp_hut", new StructureFeatureConfiguration(32, 8, 14357620)) -+ .put("minecraft:pillager_outpost", new StructureFeatureConfiguration(32, 8, 165745296)) -+ .put("minecraft:monument", new StructureFeatureConfiguration(32, 5, 10387313)) -+ .put("minecraft:endcity", new StructureFeatureConfiguration(20, 11, 10387313)) -+ .put("minecraft:mansion", new StructureFeatureConfiguration(80, 20, 10387319)) -+ .build() -+ ); -+ -+ private static record StructureFeatureConfiguration(int spacing, int separation, int salt) { -+ -+ public MapType serialize() { -+ final MapType ret = Types.NBT.createEmptyMap(); -+ -+ ret.setInt("spacing", this.spacing); -+ ret.setInt("separation", this.separation); -+ ret.setInt("salt", this.salt); -+ -+ return ret; -+ } -+ } -+ -+ public static void register() { -+ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final long seed = data.getLong("RandomSeed"); -+ String generatorName = data.getString("generatorName"); -+ if (generatorName != null) { -+ generatorName = generatorName.toLowerCase(Locale.ROOT); -+ } -+ String legacyCustomOptions = data.getString("legacy_custom_options"); -+ if (legacyCustomOptions == null) { -+ legacyCustomOptions = "customized".equals(generatorName) ? data.getString("generatorOptions") : null; -+ } -+ -+ final MapType generator; -+ boolean caves = false; -+ -+ if ("customized".equals(generatorName) || generatorName == null) { -+ generator = defaultOverworld(seed); -+ } else { -+ switch (generatorName) { -+ case "flat": { -+ final MapType generatorOptions = data.getMap("generatorOptions"); -+ -+ final MapType structures = fixFlatStructures(generatorOptions); -+ final MapType settings = Types.NBT.createEmptyMap(); -+ generator = Types.NBT.createEmptyMap(); -+ generator.setString("type", "minecraft:flat"); -+ generator.setMap("settings", settings); -+ -+ settings.setMap("structures", structures); -+ -+ ListType layers = generatorOptions.getList("layers", ObjectType.MAP); -+ if (layers == null) { -+ layers = Types.NBT.createEmptyList(); -+ -+ final int[] heights = new int[] { 1, 2, 1 }; -+ final String[] blocks = new String[] { "minecraft:bedrock", "minecraft:dirt", "minecraft:grass_block" }; -+ for (int i = 0; i < 3; ++i) { -+ final MapType layer = Types.NBT.createEmptyMap(); -+ layer.setInt("height", heights[i]); -+ layer.setString("block", blocks[i]); -+ layers.addMap(layer); -+ } -+ } -+ -+ settings.setList("layers", layers); -+ settings.setString("biome", generatorOptions.getString("biome", "minecraft:plains")); -+ -+ break; -+ } -+ -+ case "debug_all_block_states": { -+ generator = Types.NBT.createEmptyMap(); -+ generator.setString("type", "minecraft:debug"); -+ break; -+ } -+ -+ case "buffet": { -+ final MapType generatorOptions = data.getMap("generatorOptions"); -+ final MapType chunkGenerator = generatorOptions == null ? null : generatorOptions.getMap("chunk_generator"); -+ final String chunkGeneratorType = chunkGenerator == null ? null : chunkGenerator.getString("type"); -+ -+ final String newType; -+ if ("minecraft:caves".equals(chunkGeneratorType)) { -+ newType = "minecraft:caves"; -+ caves = true; -+ } else if ("minecraft:floating_islands".equals(chunkGeneratorType)) { -+ newType = "minecraft:floating_islands"; -+ } else { -+ newType = "minecraft:overworld"; -+ } -+ -+ MapType biomeSource = generatorOptions == null ? null : generatorOptions.getMap("biome_source"); -+ if (biomeSource == null) { -+ biomeSource = Types.NBT.createEmptyMap(); -+ biomeSource.setString("type", "minecraft:fixed"); -+ } -+ -+ if ("minecraft:fixed".equals(biomeSource.getString("type"))) { -+ final MapType options = biomeSource.getMap("options"); -+ final ListType biomes = options == null ? null : options.getList("biomes", ObjectType.STRING); -+ final String biome = biomes == null || biomes.size() == 0 ? "minecraft:ocean" : biomes.getString(0); -+ biomeSource.remove("options"); -+ biomeSource.setString("biome", biome); -+ } -+ -+ generator = noise(seed, newType, biomeSource); -+ break; -+ } -+ -+ default: { -+ boolean defaultGen = generatorName.equals("default"); -+ boolean default11Gen = generatorName.equals("default_1_1") || defaultGen && data.getInt("generatorVersion") == 0; -+ boolean amplified = generatorName.equals("amplified"); -+ boolean largeBiomes = generatorName.equals("largebiomes"); -+ -+ generator = noise(seed, amplified ? "minecraft:amplified" : "minecraft:overworld", -+ vanillaBiomeSource(seed, default11Gen, largeBiomes)); -+ break; -+ } -+ } -+ } -+ -+ final boolean mapFeatures = data.getBoolean("MapFeatures", true); -+ final boolean bonusChest = data.getBoolean("BonusChest", false); -+ -+ final MapType ret = Types.NBT.createEmptyMap(); -+ -+ ret.setLong("seed", seed); -+ ret.setBoolean("generate_features", mapFeatures); -+ ret.setBoolean("bonus_chest", bonusChest); -+ ret.setMap("dimensions", vanillaLevels(seed, generator, caves)); -+ if (legacyCustomOptions != null) { -+ ret.setString("legacy_custom_options", legacyCustomOptions); -+ } -+ -+ return ret; -+ } -+ }); -+ } -+ -+ public static MapType noise(final long seed, final String worldType, final MapType biomeSource) { -+ final MapType ret = Types.NBT.createEmptyMap(); -+ -+ ret.setString("type", "minecraft:noise"); -+ ret.setMap("biome_source", biomeSource); -+ ret.setLong("seed", seed); -+ ret.setString("settings", worldType); -+ -+ return ret; -+ } -+ -+ public static MapType vanillaBiomeSource(final long seed, final boolean default11Gen, final boolean largeBiomes) { -+ final MapType ret = Types.NBT.createEmptyMap(); -+ -+ ret.setString("type", "minecraft:vanilla_layered"); -+ ret.setLong("seed", seed); -+ ret.setBoolean("large_biomes", largeBiomes); -+ if (default11Gen) { -+ ret.setBoolean("legacy_biome_init_layer", default11Gen); -+ } -+ -+ return ret; -+ } -+ -+ public static MapType fixFlatStructures(final MapType generatorOptions) { -+ int distance = 32; -+ int spread = 3; -+ int count = 128; -+ boolean stronghold = false; -+ final Map newStructures = new HashMap<>(); -+ -+ if (generatorOptions == null) { -+ stronghold = true; -+ newStructures.put("minecraft:village", DEFAULTS.get("minecraft:village")); -+ } -+ -+ final MapType oldStructures = generatorOptions == null ? null : generatorOptions.getMap("structures"); -+ if (oldStructures != null) { -+ for (final String structureName : oldStructures.keys()) { -+ final MapType structureValues = oldStructures.getMap(structureName); -+ if (structureValues == null) { -+ continue; -+ } -+ -+ for (final String structureValueKey : structureValues.keys()) { -+ final String structureValue = structureValues.getString(structureValueKey); -+ -+ if ("stronghold".equals(structureName)) { -+ stronghold = true; -+ switch (structureValueKey) { -+ case "distance": -+ distance = getInt(structureValue, distance, 1); -+ break; -+ case "spread": -+ spread = getInt(structureValue, spread, 1); -+ break; -+ case "count": -+ count = getInt(structureValue, count, 1); -+ break; -+ } -+ } else { -+ switch (structureValueKey) { -+ case "distance": -+ switch (structureName) { -+ case "village": -+ setSpacing(newStructures, "minecraft:village", structureValue, 9); -+ break; -+ case "biome_1": -+ setSpacing(newStructures, "minecraft:desert_pyramid", structureValue, 9); -+ setSpacing(newStructures, "minecraft:igloo", structureValue, 9); -+ setSpacing(newStructures, "minecraft:jungle_pyramid", structureValue, 9); -+ setSpacing(newStructures, "minecraft:swamp_hut", structureValue, 9); -+ setSpacing(newStructures, "minecraft:pillager_outpost", structureValue, 9); -+ break; -+ case "endcity": -+ setSpacing(newStructures, "minecraft:endcity", structureValue, 1); -+ break; -+ case "mansion": -+ setSpacing(newStructures, "minecraft:mansion", structureValue, 1); -+ break; -+ default: -+ break; -+ } -+ case "separation": -+ if ("oceanmonument".equals(structureName)) { -+ final StructureFeatureConfiguration structure = newStructures.getOrDefault("minecraft:monument", DEFAULTS.get("minecraft:monument")); -+ final int newSpacing = getInt(structureValue, structure.separation, 1); -+ newStructures.put("minecraft:monument", new StructureFeatureConfiguration(newSpacing, structure.separation, structure.salt)); -+ } -+ -+ break; -+ case "spacing": -+ if ("oceanmonument".equals(structureName)) { -+ setSpacing(newStructures, "minecraft:monument", structureValue, 1); -+ } -+ -+ break; -+ } -+ } -+ } -+ } -+ } -+ -+ final MapType ret = Types.NBT.createEmptyMap(); -+ final MapType structuresSerialized = Types.NBT.createEmptyMap(); -+ ret.setMap("structures", structuresSerialized); -+ for (final String key : newStructures.keySet()) { -+ structuresSerialized.setMap(key, newStructures.get(key).serialize()); -+ } -+ -+ if (stronghold) { -+ final MapType strongholdData = Types.NBT.createEmptyMap(); -+ ret.setMap("stronghold", strongholdData); -+ -+ strongholdData.setInt("distance", distance); -+ strongholdData.setInt("spread", spread); -+ strongholdData.setInt("count", count); -+ } -+ -+ return ret; -+ } -+ -+ public static MapType vanillaLevels(final long seed, final MapType generator, final boolean caves) { -+ final MapType ret = Types.NBT.createEmptyMap(); -+ -+ final MapType overworld = Types.NBT.createEmptyMap(); -+ final MapType nether = Types.NBT.createEmptyMap(); -+ final MapType end = Types.NBT.createEmptyMap(); -+ -+ ret.setMap("minecraft:overworld", overworld); -+ ret.setMap("minecraft:the_nether", nether); -+ ret.setMap("minecraft:the_end", end); -+ -+ // overworld -+ overworld.setString("type", caves ? "minecraft:overworld_caves" : "minecraft:overworld"); -+ overworld.setMap("generator", generator); -+ -+ // nether -+ nether.setString("type", "minecraft:the_nether"); -+ final MapType netherBiomeSource = Types.NBT.createEmptyMap(); -+ netherBiomeSource.setString("type", "minecraft:multi_noise"); -+ netherBiomeSource.setLong("seed", seed); -+ netherBiomeSource.setString("preset", "minecraft:nether"); -+ -+ nether.setMap("generator", noise(seed, "minecraft:nether", netherBiomeSource)); -+ -+ // end -+ end.setString("type", "minecraft:the_end"); -+ final MapType endBiomeSource = Types.NBT.createEmptyMap(); -+ endBiomeSource.setString("type", "minecraft:the_end"); -+ endBiomeSource.setLong("seed", seed); -+ end.setMap("generator", noise(seed,"minecraft:end", endBiomeSource)); -+ -+ return ret; -+ } -+ -+ public static MapType defaultOverworld(final long seed) { -+ return noise(seed, "minecraft:overworld", vanillaBiomeSource(seed, false, false)); -+ } -+ -+ private static int getInt(final String value, final int dfl) { -+ return NumberUtils.toInt(value, dfl); -+ } -+ -+ private static int getInt(final String value, final int dfl, final int minVal) { -+ return Math.max(minVal, getInt(value, dfl)); -+ } -+ -+ private static void setSpacing(final Map structures, final String structureName, -+ final String value, final int minVal) { -+ final StructureFeatureConfiguration structure = structures.getOrDefault(structureName, DEFAULTS.get(structureName)); -+ final int newSpacing = getInt(value, structure.spacing, minVal); -+ -+ structures.put(structureName, new StructureFeatureConfiguration(newSpacing, structure.separation, structure.salt)); -+ } -+ -+ private V2550() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2551.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2551.java -new file mode 100644 -index 0000000000000000000000000000000000000000..9cfecd222ef41fdb4f31517a0821d7532386285f ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2551.java -@@ -0,0 +1,103 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+ -+public final class V2551 { -+ -+ private static final int VERSION = MCVersions.V20W20B + 14; -+ -+ public static void register() { -+ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ final MapType dimensions = data.getMap("dimensions"); -+ -+ if (dimensions == null) { -+ return null; -+ } -+ -+ for (final String dimension : dimensions.keys()) { -+ final MapType dimensionData = dimensions.getMap(dimension); -+ if (dimensionData == null) { -+ continue; -+ } -+ -+ final MapType generator = dimensionData.getMap("generator"); -+ if (generator == null) { -+ continue; -+ } -+ -+ final String type = generator.getString("type"); -+ if (type == null) { -+ continue; -+ } -+ -+ switch (type) { -+ case "minecraft:flat": { -+ final MapType settings = generator.getMap("settings"); -+ if (settings == null) { -+ continue; -+ } -+ -+ WalkerUtils.convert(MCTypeRegistry.BIOME, settings, "biome", fromVersion, toVersion); -+ -+ final ListType layers = settings.getList("layers", ObjectType.MAP); -+ if (layers != null) { -+ for (int i = 0, len = layers.size(); i < len; ++i) { -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, layers.getMap(i), "block", fromVersion, toVersion); -+ } -+ } -+ -+ break; -+ } -+ case "minecraft:noise": { -+ final MapType settings = generator.getMap("settings"); -+ if (settings != null) { -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, settings, "default_block", fromVersion, toVersion); -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, settings, "default_fluid", fromVersion, toVersion); -+ } -+ -+ final MapType biomeSource = generator.getMap("biome_source"); -+ if (biomeSource != null) { -+ final String biomeSourceType = biomeSource.getString("type", ""); -+ switch (biomeSourceType) { -+ case "minecraft:fixed": { -+ WalkerUtils.convert(MCTypeRegistry.BIOME, biomeSource, "biome", fromVersion, toVersion); -+ break; -+ } -+ -+ case "minecraft:multi_noise": { -+ // Vanilla's schema is wrong. It should be DSL.fields("biomes", DSL.list(DSL.fields("biome"))) -+ // But it just contains the list part. That obviously can never be the case, because -+ // the root object is a compound, not a list. -+ -+ final ListType biomes = biomeSource.getList("biomes", ObjectType.MAP); -+ if (biomes != null) { -+ for (int i = 0, len = biomes.size(); i < len; ++i) { -+ WalkerUtils.convert(MCTypeRegistry.BIOME, biomes.getMap(i), "biome", fromVersion, toVersion); -+ } -+ } -+ break; -+ } -+ -+ case "minecraft:checkerboard": { -+ WalkerUtils.convertList(MCTypeRegistry.BIOME, biomeSource, "biomes", fromVersion, toVersion); -+ break; -+ } -+ } -+ } -+ -+ break; -+ } -+ } -+ } -+ -+ return null; -+ }); -+ } -+ -+ private V2551() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2552.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2552.java -new file mode 100644 -index 0000000000000000000000000000000000000000..9e6c7dc40d509cf424976831382425ab7eceb024 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2552.java -@@ -0,0 +1,22 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+ -+public final class V2552 { -+ -+ private static final int VERSION = MCVersions.V20W20B + 15; -+ -+ public static void register() { -+ ConverterAbstractStringValueTypeRename.register(VERSION, MCTypeRegistry.BIOME, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:nether", "minecraft:nether_wastes" -+ ) -+ )::get); -+ } -+ -+ private V2552() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2553.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2553.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f019774923bf08fc0f7dc7cafd5fb66fdd7427f8 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2553.java -@@ -0,0 +1,77 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V2553 { -+ -+ private static final int VERSION = MCVersions.V20W20B + 16; -+ -+ public static final Map BIOME_RENAMES = new HashMap<>( -+ ImmutableMap.builder() -+ .put("minecraft:extreme_hills", "minecraft:mountains") -+ .put("minecraft:swampland", "minecraft:swamp") -+ .put("minecraft:hell", "minecraft:nether_wastes") -+ .put("minecraft:sky", "minecraft:the_end") -+ .put("minecraft:ice_flats", "minecraft:snowy_tundra") -+ .put("minecraft:ice_mountains", "minecraft:snowy_mountains") -+ .put("minecraft:mushroom_island", "minecraft:mushroom_fields") -+ .put("minecraft:mushroom_island_shore", "minecraft:mushroom_field_shore") -+ .put("minecraft:beaches", "minecraft:beach") -+ .put("minecraft:forest_hills", "minecraft:wooded_hills") -+ .put("minecraft:smaller_extreme_hills", "minecraft:mountain_edge") -+ .put("minecraft:stone_beach", "minecraft:stone_shore") -+ .put("minecraft:cold_beach", "minecraft:snowy_beach") -+ .put("minecraft:roofed_forest", "minecraft:dark_forest") -+ .put("minecraft:taiga_cold", "minecraft:snowy_taiga") -+ .put("minecraft:taiga_cold_hills", "minecraft:snowy_taiga_hills") -+ .put("minecraft:redwood_taiga", "minecraft:giant_tree_taiga") -+ .put("minecraft:redwood_taiga_hills", "minecraft:giant_tree_taiga_hills") -+ .put("minecraft:extreme_hills_with_trees", "minecraft:wooded_mountains") -+ .put("minecraft:savanna_rock", "minecraft:savanna_plateau") -+ .put("minecraft:mesa", "minecraft:badlands") -+ .put("minecraft:mesa_rock", "minecraft:wooded_badlands_plateau") -+ .put("minecraft:mesa_clear_rock", "minecraft:badlands_plateau") -+ .put("minecraft:sky_island_low", "minecraft:small_end_islands") -+ .put("minecraft:sky_island_medium", "minecraft:end_midlands") -+ .put("minecraft:sky_island_high", "minecraft:end_highlands") -+ .put("minecraft:sky_island_barren", "minecraft:end_barrens") -+ .put("minecraft:void", "minecraft:the_void") -+ .put("minecraft:mutated_plains", "minecraft:sunflower_plains") -+ .put("minecraft:mutated_desert", "minecraft:desert_lakes") -+ .put("minecraft:mutated_extreme_hills", "minecraft:gravelly_mountains") -+ .put("minecraft:mutated_forest", "minecraft:flower_forest") -+ .put("minecraft:mutated_taiga", "minecraft:taiga_mountains") -+ .put("minecraft:mutated_swampland", "minecraft:swamp_hills") -+ .put("minecraft:mutated_ice_flats", "minecraft:ice_spikes") -+ .put("minecraft:mutated_jungle", "minecraft:modified_jungle") -+ .put("minecraft:mutated_jungle_edge", "minecraft:modified_jungle_edge") -+ .put("minecraft:mutated_birch_forest", "minecraft:tall_birch_forest") -+ .put("minecraft:mutated_birch_forest_hills", "minecraft:tall_birch_hills") -+ .put("minecraft:mutated_roofed_forest", "minecraft:dark_forest_hills") -+ .put("minecraft:mutated_taiga_cold", "minecraft:snowy_taiga_mountains") -+ .put("minecraft:mutated_redwood_taiga", "minecraft:giant_spruce_taiga") -+ .put("minecraft:mutated_redwood_taiga_hills", "minecraft:giant_spruce_taiga_hills") -+ .put("minecraft:mutated_extreme_hills_with_trees", "minecraft:modified_gravelly_mountains") -+ .put("minecraft:mutated_savanna", "minecraft:shattered_savanna") -+ .put("minecraft:mutated_savanna_rock", "minecraft:shattered_savanna_plateau") -+ .put("minecraft:mutated_mesa", "minecraft:eroded_badlands") -+ .put("minecraft:mutated_mesa_rock", "minecraft:modified_wooded_badlands_plateau") -+ .put("minecraft:mutated_mesa_clear_rock", "minecraft:modified_badlands_plateau") -+ .put("minecraft:warm_deep_ocean", "minecraft:deep_warm_ocean") -+ .put("minecraft:lukewarm_deep_ocean", "minecraft:deep_lukewarm_ocean") -+ .put("minecraft:cold_deep_ocean", "minecraft:deep_cold_ocean") -+ .put("minecraft:frozen_deep_ocean", "minecraft:deep_frozen_ocean") -+ .build() -+ ); -+ -+ public static void register() { -+ ConverterAbstractStringValueTypeRename.register(VERSION, MCTypeRegistry.BIOME, BIOME_RENAMES::get); -+ } -+ -+ private V2553() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2558.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2558.java -new file mode 100644 -index 0000000000000000000000000000000000000000..137a530c1b979e7257b77f405885aa9f4d376c11 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2558.java -@@ -0,0 +1,48 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.options.ConverterAbstractOptionsRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.Types; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+ -+public final class V2558 { -+ -+ private static final int VERSION = MCVersions.V1_16_PRE2 + 1; -+ -+ public static void register() { -+ ConverterAbstractOptionsRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "key_key.swapHands", "key_key.swapOffhand" -+ ) -+ )::get); -+ -+ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ MapType dimensions = data.getMap("dimensions"); -+ if (dimensions == null) { -+ dimensions = Types.NBT.createEmptyMap(); -+ data.setMap("dimensions", dimensions); -+ } -+ -+ if (dimensions.isEmpty()) { -+ data.setMap("dimensions", recreateSettings(data)); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private static MapType recreateSettings(final MapType data) { -+ final long seed = data.getLong("seed"); -+ -+ return V2550.vanillaLevels(seed, V2550.defaultOverworld(seed), false); -+ } -+ -+ private V2558() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2568.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2568.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e50fbc38dbf9198c0c652b506e50780eca368bb0 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2568.java -@@ -0,0 +1,18 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+ -+public final class V2568 { -+ -+ private static final int VERSION = MCVersions.V1_16_1 + 1; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ registerMob("minecraft:piglin_brute"); -+ } -+ -+ private V2568() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2671.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2671.java -new file mode 100644 -index 0000000000000000000000000000000000000000..140bfff947e540452f3794eda2f1e2122f8d3f27 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2671.java -@@ -0,0 +1,18 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+ -+public final class V2671 { -+ -+ private static final int VERSION = MCVersions.V1_16_5 + 85; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ registerMob("minecraft:goat"); -+ } -+ -+ private V2671() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2679.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2679.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7ec79da7e8871d6beca05a25c70d8c6811531faa ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2679.java -@@ -0,0 +1,38 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V2679 { -+ -+ private static final int VERSION = MCVersions.V1_16_5 + 93; -+ -+ public static void register() { -+ MCTypeRegistry.BLOCK_STATE.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (!"minecraft:cauldron".equals(data.getString("Name"))) { -+ return null; -+ } -+ -+ final MapType properties = data.getMap("Properties"); -+ -+ if (properties == null) { -+ return null; -+ } -+ -+ if (properties.getString("level", "0").equals("0")) { -+ data.remove("Properties"); -+ return null; -+ } else { -+ data.setString("Name", "minecraft:water_cauldron"); -+ return null; -+ } -+ } -+ }); -+ } -+ -+ private V2679() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2680.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2680.java -new file mode 100644 -index 0000000000000000000000000000000000000000..87cbe1c717635908a30c57028346a1abeb21e6a6 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2680.java -@@ -0,0 +1,27 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+ -+public final class V2680 { -+ -+ private static final int VERSION = MCVersions.V1_16_5 + 94; -+ -+ public static void register() { -+ ConverterAbstractItemRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:grass_path", "minecraft:dirt_path" -+ ) -+ )::get); -+ ConverterAbstractBlockRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:grass_path", "minecraft:dirt_path" -+ ) -+ )::get); -+ } -+ -+ private V2680() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2684.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2684.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0c996642f561d2471a506a34f5efe6dae5cd1fb3 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2684.java -@@ -0,0 +1,16 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.game_event.GameEventListenerWalker; -+ -+public final class V2684 { -+ -+ private static final int VERSION = MCVersions.V20W48A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:sculk_sensor", new GameEventListenerWalker()); -+ } -+ -+ private V2684() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2686.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2686.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1f1685cb0e1427e88dc5c970b0cb58aae0393396 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2686.java -@@ -0,0 +1,18 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+ -+public final class V2686 { -+ -+ private static final int VERSION = MCVersions.V20W49A + 1; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ registerMob("minecraft:axolotl"); -+ } -+ -+ private V2686() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2688.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2688.java -new file mode 100644 -index 0000000000000000000000000000000000000000..15a7bf7b7ea883d7a3cee9183b92e12838efd690 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2688.java -@@ -0,0 +1,22 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; -+ -+public final class V2688 { -+ -+ private static final int VERSION = MCVersions.V20W51A + 1; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ registerMob("minecraft:glow_squid"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:glow_item_frame", new DataWalkerItems("Item")); -+ } -+ -+ private V2688() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2690.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2690.java -new file mode 100644 -index 0000000000000000000000000000000000000000..39ffcec6e78229dd62abfd42c1ac64c3ccccc6dc ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2690.java -@@ -0,0 +1,45 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V2690 { -+ -+ private static final int VERSION = MCVersions.V21W05A; -+ -+ private static final Map RENAMES = new HashMap<>( -+ ImmutableMap.builder() -+ .put("minecraft:weathered_copper_block", "minecraft:oxidized_copper_block") -+ .put("minecraft:semi_weathered_copper_block", "minecraft:weathered_copper_block") -+ .put("minecraft:lightly_weathered_copper_block", "minecraft:exposed_copper_block") -+ .put("minecraft:weathered_cut_copper", "minecraft:oxidized_cut_copper") -+ .put("minecraft:semi_weathered_cut_copper", "minecraft:weathered_cut_copper") -+ .put("minecraft:lightly_weathered_cut_copper", "minecraft:exposed_cut_copper") -+ .put("minecraft:weathered_cut_copper_stairs", "minecraft:oxidized_cut_copper_stairs") -+ .put("minecraft:semi_weathered_cut_copper_stairs", "minecraft:weathered_cut_copper_stairs") -+ .put("minecraft:lightly_weathered_cut_copper_stairs", "minecraft:exposed_cut_copper_stairs") -+ .put("minecraft:weathered_cut_copper_slab", "minecraft:oxidized_cut_copper_slab") -+ .put("minecraft:semi_weathered_cut_copper_slab", "minecraft:weathered_cut_copper_slab") -+ .put("minecraft:lightly_weathered_cut_copper_slab", "minecraft:exposed_cut_copper_slab") -+ .put("minecraft:waxed_semi_weathered_copper", "minecraft:waxed_weathered_copper") -+ .put("minecraft:waxed_lightly_weathered_copper", "minecraft:waxed_exposed_copper") -+ .put("minecraft:waxed_semi_weathered_cut_copper", "minecraft:waxed_weathered_cut_copper") -+ .put("minecraft:waxed_lightly_weathered_cut_copper", "minecraft:waxed_exposed_cut_copper") -+ .put("minecraft:waxed_semi_weathered_cut_copper_stairs", "minecraft:waxed_weathered_cut_copper_stairs") -+ .put("minecraft:waxed_lightly_weathered_cut_copper_stairs", "minecraft:waxed_exposed_cut_copper_stairs") -+ .put("minecraft:waxed_semi_weathered_cut_copper_slab", "minecraft:waxed_weathered_cut_copper_slab") -+ .put("minecraft:waxed_lightly_weathered_cut_copper_slab", "minecraft:waxed_exposed_cut_copper_slab") -+ .build() -+ ); -+ -+ public static void register() { -+ ConverterAbstractItemRename.register(VERSION, RENAMES::get); -+ ConverterAbstractBlockRename.register(VERSION, RENAMES::get); -+ } -+ -+ private V2690() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2691.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2691.java -new file mode 100644 -index 0000000000000000000000000000000000000000..bb87bcedfc2ed8d19b266e925beca0f54b50a0ab ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2691.java -@@ -0,0 +1,29 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V2691 { -+ -+ private static final int VERSION = MCVersions.V21W05A + 1; -+ -+ private static final Map RENAMES = new HashMap<>( -+ ImmutableMap.builder() -+ .put("minecraft:waxed_copper", "minecraft:waxed_copper_block") -+ .put("minecraft:oxidized_copper_block", "minecraft:oxidized_copper") -+ .put("minecraft:weathered_copper_block", "minecraft:weathered_copper") -+ .put("minecraft:exposed_copper_block", "minecraft:exposed_copper") -+ .build() -+ ); -+ -+ public static void register() { -+ ConverterAbstractItemRename.register(VERSION, RENAMES::get); -+ ConverterAbstractBlockRename.register(VERSION, RENAMES::get); -+ } -+ -+ private V2691() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2693.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2693.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a242e8e9a7a7c80c00ec0d64542b3d7dc3103e24 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2693.java -@@ -0,0 +1,16 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.AddFlagIfAbsent; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+ -+public final class V2693 { -+ -+ private static final int VERSION = MCVersions.V21W05B + 1; -+ -+ public static void register() { -+ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureConverter(new AddFlagIfAbsent(VERSION, "has_increased_height_already", false)); -+ } -+ -+ private V2693() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2696.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2696.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ce568002e54924e001c12271f0bde7183bc23c61 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2696.java -@@ -0,0 +1,42 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V2696 { -+ -+ private static final int VERSION = MCVersions.V21W07A + 1; -+ -+ private static final Map RENAMES = new HashMap<>( -+ ImmutableMap.builder() -+ .put("minecraft:grimstone", "minecraft:deepslate") -+ .put("minecraft:grimstone_slab", "minecraft:cobbled_deepslate_slab") -+ .put("minecraft:grimstone_stairs", "minecraft:cobbled_deepslate_stairs") -+ .put("minecraft:grimstone_wall", "minecraft:cobbled_deepslate_wall") -+ .put("minecraft:polished_grimstone", "minecraft:polished_deepslate") -+ .put("minecraft:polished_grimstone_slab", "minecraft:polished_deepslate_slab") -+ .put("minecraft:polished_grimstone_stairs", "minecraft:polished_deepslate_stairs") -+ .put("minecraft:polished_grimstone_wall", "minecraft:polished_deepslate_wall") -+ .put("minecraft:grimstone_tiles", "minecraft:deepslate_tiles") -+ .put("minecraft:grimstone_tile_slab", "minecraft:deepslate_tile_slab") -+ .put("minecraft:grimstone_tile_stairs", "minecraft:deepslate_tile_stairs") -+ .put("minecraft:grimstone_tile_wall", "minecraft:deepslate_tile_wall") -+ .put("minecraft:grimstone_bricks", "minecraft:deepslate_bricks") -+ .put("minecraft:grimstone_brick_slab", "minecraft:deepslate_brick_slab") -+ .put("minecraft:grimstone_brick_stairs", "minecraft:deepslate_brick_stairs") -+ .put("minecraft:grimstone_brick_wall", "minecraft:deepslate_brick_wall") -+ .put("minecraft:chiseled_grimstone", "minecraft:chiseled_deepslate") -+ .build() -+ ); -+ -+ public static void register() { -+ ConverterAbstractItemRename.register(VERSION, RENAMES::get); -+ ConverterAbstractBlockRename.register(VERSION, RENAMES::get); -+ } -+ -+ private V2696() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2700.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2700.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e6a2f29b20aa6d7cd431fc63c2d8ed70dc9a2ab8 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2700.java -@@ -0,0 +1,22 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+ -+public final class V2700 { -+ -+ private static final int VERSION = MCVersions.V21W10A + 1; -+ -+ public static void register() { -+ ConverterAbstractBlockRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:cave_vines_head", "minecraft:cave_vines", -+ "minecraft:cave_vines_body", "minecraft:cave_vines_plant" -+ ) -+ )::get); -+ } -+ -+ private V2700() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2701.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2701.java -new file mode 100644 -index 0000000000000000000000000000000000000000..dc1604f48a9f15721e709f2e128210085520c15e ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2701.java -@@ -0,0 +1,205 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import com.google.common.collect.Sets; -+import java.util.Set; -+import java.util.regex.Matcher; -+import java.util.regex.Pattern; -+ -+public final class V2701 { -+ -+ private static final int VERSION = MCVersions.V21W10A + 2; -+ -+ private static final Pattern INDEX_PATTERN = Pattern.compile("\\[(\\d+)\\]"); -+ -+ private static final Set PIECE_TYPE = Sets.newHashSet( -+ "minecraft:jigsaw", -+ "minecraft:nvi", -+ "minecraft:pcp", -+ "minecraft:bastionremnant", -+ "minecraft:runtime" -+ ); -+ private static final Set FEATURES = Sets.newHashSet( -+ "minecraft:tree", -+ "minecraft:flower", -+ "minecraft:block_pile", -+ "minecraft:random_patch" -+ ); -+ -+ public static void register() { -+ MCTypeRegistry.STRUCTURE_FEATURE.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final ListType children = data.getList("Children", ObjectType.MAP); -+ -+ if (children == null) { -+ return null; -+ } -+ -+ for (int i = 0, len = children.size(); i < len; ++i) { -+ final MapType child = children.getMap(i); -+ -+ if (!PIECE_TYPE.contains(child.getString("id"))) { -+ continue; -+ } -+ -+ final String poolElement = child.getString("pool_element"); -+ if (!"minecraft:feature_pool_element".equals(poolElement)) { -+ continue; -+ } -+ -+ final MapType feature = child.getMap("feature"); -+ if (feature == null) { -+ continue; -+ } -+ -+ final String replacement = convertToString(feature); -+ -+ if (replacement != null) { -+ child.setString("feature", replacement); -+ } -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private static String getNestedString(final MapType root, final String... paths) { -+ if (paths.length == 0) { -+ throw new IllegalArgumentException("Missing path"); -+ } -+ -+ Object current = root.getGeneric(paths[0]); -+ -+ for (int i = 1; i < paths.length; ++i) { -+ final String path = paths[i]; -+ -+ final Matcher indexMatcher = INDEX_PATTERN.matcher(path); -+ if (!indexMatcher.matches()) { -+ current = (current instanceof MapType) ? ((MapType)current).getGeneric(path) : null; -+ if (current == null) { -+ break; -+ } -+ continue; -+ } -+ -+ final int index = Integer.parseInt(indexMatcher.group(1)); -+ if (!(current instanceof ListType)) { -+ current = null; -+ break; -+ } else { -+ final ListType list = (ListType)current; -+ if (index >= 0 && index < list.size()) { -+ current = list.getGeneric(index); -+ } else { -+ current = null; -+ break; -+ } -+ } -+ } -+ -+ return current instanceof String ? (String)current : ""; -+ } -+ -+ private static String convertToString(final MapType feature) { -+ return getReplacement( -+ getNestedString(feature, "type"), -+ getNestedString(feature, "name"), -+ getNestedString(feature, "config", "state_provider", "type"), -+ getNestedString(feature, "config", "state_provider", "state", "Name"), -+ getNestedString(feature, "config", "state_provider", "entries", "[0]", "data", "Name"), -+ getNestedString(feature, "config", "foliage_placer", "type"), -+ getNestedString(feature, "config", "leaves_provider", "state", "Name") -+ ); -+ } -+ -+ private static String getReplacement(final String type, final String name, final String stateType, final String stateName, -+ final String firstEntryName, final String foliageName, final String leavesName) { -+ final String actualType; -+ if (!type.isEmpty()) { -+ actualType = type; -+ } else { -+ if (name.isEmpty()) { -+ return null; -+ } -+ -+ if ("minecraft:normal_tree".equals(name)) { -+ actualType = "minecraft:tree"; -+ } else { -+ actualType = name; -+ } -+ } -+ -+ if (FEATURES.contains(actualType)) { -+ if ("minecraft:random_patch".equals(actualType)) { -+ if ("minecraft:simple_state_provider".equals(stateType)) { -+ if ("minecraft:sweet_berry_bush".equals(stateName)) { -+ return "minecraft:patch_berry_bush"; -+ } -+ -+ if ("minecraft:cactus".equals(stateName)) { -+ return "minecraft:patch_cactus"; -+ } -+ } else if ("minecraft:weighted_state_provider".equals(stateType) && ("minecraft:grass".equals(firstEntryName) || "minecraft:fern".equals(firstEntryName))) { -+ return "minecraft:patch_taiga_grass"; -+ } -+ } else if ("minecraft:block_pile".equals(actualType)) { -+ if (!"minecraft:simple_state_provider".equals(stateType) && !"minecraft:rotated_block_provider".equals(stateType)) { -+ if ("minecraft:weighted_state_provider".equals(stateType)) { -+ if ("minecraft:packed_ice".equals(firstEntryName) || "minecraft:blue_ice".equals(firstEntryName)) { -+ return "minecraft:pile_ice"; -+ } -+ -+ if ("minecraft:jack_o_lantern".equals(firstEntryName) || "minecraft:pumpkin".equals(firstEntryName)) { -+ return "minecraft:pile_pumpkin"; -+ } -+ } -+ } else { -+ if ("minecraft:hay_block".equals(stateName)) { -+ return "minecraft:pile_hay"; -+ } -+ -+ if ("minecraft:melon".equals(stateName)) { -+ return "minecraft:pile_melon"; -+ } -+ -+ if ("minecraft:snow".equals(stateName)) { -+ return "minecraft:pile_snow"; -+ } -+ } -+ } else { -+ if ("minecraft:flower".equals(actualType)) { -+ return "minecraft:flower_plain"; -+ } -+ -+ if ("minecraft:tree".equals(actualType)) { -+ if ("minecraft:acacia_foliage_placer".equals(foliageName)) { -+ return "minecraft:acacia"; -+ } -+ -+ if ("minecraft:blob_foliage_placer".equals(foliageName) && "minecraft:oak_leaves".equals(leavesName)) { -+ return "minecraft:oak"; -+ } -+ -+ if ("minecraft:pine_foliage_placer".equals(foliageName)) { -+ return "minecraft:pine"; -+ } -+ -+ if ("minecraft:spruce_foliage_placer".equals(foliageName)) { -+ return "minecraft:spruce"; -+ } -+ } -+ } -+ } -+ -+ return null; -+ } -+ -+ private V2701() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2702.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2702.java -new file mode 100644 -index 0000000000000000000000000000000000000000..cc89ca8a01c2589c807be2a7560bcc6051417379 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2702.java -@@ -0,0 +1,35 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V2702 { -+ -+ private static final int VERSION = MCVersions.V21W10A + 3; -+ -+ public static void register() { -+ final DataConverter, MapType> arrowConverter = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (data.hasKey("pickup")) { -+ return null; -+ } -+ -+ final boolean player = data.getBoolean("player", true); -+ data.remove("player"); -+ -+ data.setByte("pickup", player ? (byte)1 : (byte)0); -+ -+ return null; -+ } -+ }; -+ -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:arrow", arrowConverter); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:spectral_arrow", arrowConverter); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:trident", arrowConverter); -+ } -+ -+ private V2702() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2707.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2707.java -new file mode 100644 -index 0000000000000000000000000000000000000000..3c5fc48f39c08249a61199c7f72dddee65fd98af ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2707.java -@@ -0,0 +1,22 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.AddFlagIfAbsent; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+ -+public final class V2707 { -+ -+ private static final int VERSION = MCVersions.V21W14A + 1; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureConverter(new AddFlagIfAbsent(VERSION, "has_increased_height_already", true)); -+ -+ registerMob("minecraft:marker"); // ????????????? -+ } -+ -+ private V2707() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2710.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2710.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0967c8c794869a972c1283cab6b3f3cef1d77aec ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2710.java -@@ -0,0 +1,21 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.stats.ConverterAbstractStatsRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+ -+public final class V2710 { -+ -+ private static final int VERSION = MCVersions.V21W15A + 1; -+ -+ public static void register() { -+ ConverterAbstractStatsRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:play_one_minute", "minecraft:play_time" -+ ) -+ )::get); -+ } -+ -+ private V2710() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2717.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2717.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e0d6b2f6b00e0bfce205efa889de1765ef22793a ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2717.java -@@ -0,0 +1,25 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V2717 { -+ -+ private static final int VERSION = MCVersions.V1_17_PRE1 + 1; -+ -+ public static void register() { -+ final Map rename = new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:azalea_leaves_flowers", "minecraft:flowering_azalea_leaves" -+ ) -+ ); -+ ConverterAbstractItemRename.register(VERSION, rename::get); -+ ConverterAbstractBlockRename.register(VERSION, rename::get); -+ } -+ -+ private V2717() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2825.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2825.java -new file mode 100644 -index 0000000000000000000000000000000000000000..cd00c9398791967be6dd10f7183c61902431b27a ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2825.java -@@ -0,0 +1,16 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.AddFlagIfAbsent; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+ -+public final class V2825 { -+ -+ private static final int VERSION = MCVersions.V1_17_1 + 95; -+ -+ public static void register() { -+ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureConverter(new AddFlagIfAbsent(VERSION, "has_increased_height_already", false)); -+ } -+ -+ private V2825() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2831.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2831.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1b692e4866d99c89705289ad1f386f467382b1c7 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2831.java -@@ -0,0 +1,71 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.Types; -+ -+public final class V2831 { -+ -+ private static final int VERSION = MCVersions.V1_17_1 + 101; -+ -+ public static void register() { -+ MCTypeRegistry.UNTAGGED_SPAWNER.addStructureWalker(VERSION, (final MapType root, final long fromVersion, final long toVersion) -> { -+ final ListType spawnPotentials = root.getList("SpawnPotentials", ObjectType.MAP); -+ if (spawnPotentials != null) { -+ for (int i = 0, len = spawnPotentials.size(); i < len; ++i) { -+ final MapType spawnPotential = spawnPotentials.getMap(i); -+ -+ WalkerUtils.convert(MCTypeRegistry.ENTITY, spawnPotential.getMap("data"), "entity", fromVersion, toVersion); -+ } -+ } -+ -+ WalkerUtils.convert(MCTypeRegistry.ENTITY, root.getMap("SpawnData"), "entity", fromVersion, toVersion); -+ -+ return null; -+ }); -+ -+ MCTypeRegistry.UNTAGGED_SPAWNER.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType root, final long sourceVersion, final long toVersion) { -+ final MapType spawnData = root.getMap("SpawnData"); -+ if (spawnData != null) { -+ final MapType wrapped = Types.NBT.createEmptyMap(); -+ root.setMap("SpawnData", wrapped); -+ -+ wrapped.setMap("entity", spawnData); -+ } -+ -+ final ListType spawnPotentials = root.getList("SpawnPotentials", ObjectType.MAP); -+ if (spawnPotentials != null) { -+ for (int i = 0, len = spawnPotentials.size(); i < len; ++i) { -+ final MapType spawnPotential = spawnPotentials.getMap(i); -+ -+ // new format of weighted list (SpawnPotentials): -+ // root.data -> data -+ // root.weight -> weight -+ -+ final MapType entity = spawnPotential.getMap("Entity"); -+ final int weight = spawnPotential.getInt("Weight", 1); -+ spawnPotential.remove("Entity"); -+ spawnPotential.remove("Weight"); -+ spawnPotential.setInt("weight", weight); -+ -+ final MapType data = Types.NBT.createEmptyMap(); -+ spawnPotential.setMap("data", data); -+ -+ data.setMap("entity", entity); -+ } -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V2831() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2832.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2832.java -new file mode 100644 -index 0000000000000000000000000000000000000000..21d1617d222d0b82b1c5222a0ef1a1fa9da02ab8 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2832.java -@@ -0,0 +1,929 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.Types; -+import com.mojang.logging.LogUtils; -+import it.unimi.dsi.fastutil.ints.Int2IntLinkedOpenHashMap; -+import it.unimi.dsi.fastutil.ints.IntIterator; -+import it.unimi.dsi.fastutil.ints.IntOpenHashSet; -+import org.apache.commons.lang3.mutable.MutableBoolean; -+import org.slf4j.Logger; -+import java.util.Arrays; -+import java.util.BitSet; -+import java.util.HashSet; -+import java.util.Set; -+ -+public final class V2832 { -+ -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ -+ private static final int VERSION = MCVersions.V1_17_1 + 102; -+ -+ private static final String[] BIOMES_BY_ID = new String[256]; // rip datapacks -+ static { -+ BIOMES_BY_ID[0] = "minecraft:ocean"; -+ BIOMES_BY_ID[1] = "minecraft:plains"; -+ BIOMES_BY_ID[2] = "minecraft:desert"; -+ BIOMES_BY_ID[3] = "minecraft:mountains"; -+ BIOMES_BY_ID[4] = "minecraft:forest"; -+ BIOMES_BY_ID[5] = "minecraft:taiga"; -+ BIOMES_BY_ID[6] = "minecraft:swamp"; -+ BIOMES_BY_ID[7] = "minecraft:river"; -+ BIOMES_BY_ID[8] = "minecraft:nether_wastes"; -+ BIOMES_BY_ID[9] = "minecraft:the_end"; -+ BIOMES_BY_ID[10] = "minecraft:frozen_ocean"; -+ BIOMES_BY_ID[11] = "minecraft:frozen_river"; -+ BIOMES_BY_ID[12] = "minecraft:snowy_tundra"; -+ BIOMES_BY_ID[13] = "minecraft:snowy_mountains"; -+ BIOMES_BY_ID[14] = "minecraft:mushroom_fields"; -+ BIOMES_BY_ID[15] = "minecraft:mushroom_field_shore"; -+ BIOMES_BY_ID[16] = "minecraft:beach"; -+ BIOMES_BY_ID[17] = "minecraft:desert_hills"; -+ BIOMES_BY_ID[18] = "minecraft:wooded_hills"; -+ BIOMES_BY_ID[19] = "minecraft:taiga_hills"; -+ BIOMES_BY_ID[20] = "minecraft:mountain_edge"; -+ BIOMES_BY_ID[21] = "minecraft:jungle"; -+ BIOMES_BY_ID[22] = "minecraft:jungle_hills"; -+ BIOMES_BY_ID[23] = "minecraft:jungle_edge"; -+ BIOMES_BY_ID[24] = "minecraft:deep_ocean"; -+ BIOMES_BY_ID[25] = "minecraft:stone_shore"; -+ BIOMES_BY_ID[26] = "minecraft:snowy_beach"; -+ BIOMES_BY_ID[27] = "minecraft:birch_forest"; -+ BIOMES_BY_ID[28] = "minecraft:birch_forest_hills"; -+ BIOMES_BY_ID[29] = "minecraft:dark_forest"; -+ BIOMES_BY_ID[30] = "minecraft:snowy_taiga"; -+ BIOMES_BY_ID[31] = "minecraft:snowy_taiga_hills"; -+ BIOMES_BY_ID[32] = "minecraft:giant_tree_taiga"; -+ BIOMES_BY_ID[33] = "minecraft:giant_tree_taiga_hills"; -+ BIOMES_BY_ID[34] = "minecraft:wooded_mountains"; -+ BIOMES_BY_ID[35] = "minecraft:savanna"; -+ BIOMES_BY_ID[36] = "minecraft:savanna_plateau"; -+ BIOMES_BY_ID[37] = "minecraft:badlands"; -+ BIOMES_BY_ID[38] = "minecraft:wooded_badlands_plateau"; -+ BIOMES_BY_ID[39] = "minecraft:badlands_plateau"; -+ BIOMES_BY_ID[40] = "minecraft:small_end_islands"; -+ BIOMES_BY_ID[41] = "minecraft:end_midlands"; -+ BIOMES_BY_ID[42] = "minecraft:end_highlands"; -+ BIOMES_BY_ID[43] = "minecraft:end_barrens"; -+ BIOMES_BY_ID[44] = "minecraft:warm_ocean"; -+ BIOMES_BY_ID[45] = "minecraft:lukewarm_ocean"; -+ BIOMES_BY_ID[46] = "minecraft:cold_ocean"; -+ BIOMES_BY_ID[47] = "minecraft:deep_warm_ocean"; -+ BIOMES_BY_ID[48] = "minecraft:deep_lukewarm_ocean"; -+ BIOMES_BY_ID[49] = "minecraft:deep_cold_ocean"; -+ BIOMES_BY_ID[50] = "minecraft:deep_frozen_ocean"; -+ BIOMES_BY_ID[127] = "minecraft:the_void"; -+ BIOMES_BY_ID[129] = "minecraft:sunflower_plains"; -+ BIOMES_BY_ID[130] = "minecraft:desert_lakes"; -+ BIOMES_BY_ID[131] = "minecraft:gravelly_mountains"; -+ BIOMES_BY_ID[132] = "minecraft:flower_forest"; -+ BIOMES_BY_ID[133] = "minecraft:taiga_mountains"; -+ BIOMES_BY_ID[134] = "minecraft:swamp_hills"; -+ BIOMES_BY_ID[140] = "minecraft:ice_spikes"; -+ BIOMES_BY_ID[149] = "minecraft:modified_jungle"; -+ BIOMES_BY_ID[151] = "minecraft:modified_jungle_edge"; -+ BIOMES_BY_ID[155] = "minecraft:tall_birch_forest"; -+ BIOMES_BY_ID[156] = "minecraft:tall_birch_hills"; -+ BIOMES_BY_ID[157] = "minecraft:dark_forest_hills"; -+ BIOMES_BY_ID[158] = "minecraft:snowy_taiga_mountains"; -+ BIOMES_BY_ID[160] = "minecraft:giant_spruce_taiga"; -+ BIOMES_BY_ID[161] = "minecraft:giant_spruce_taiga_hills"; -+ BIOMES_BY_ID[162] = "minecraft:modified_gravelly_mountains"; -+ BIOMES_BY_ID[163] = "minecraft:shattered_savanna"; -+ BIOMES_BY_ID[164] = "minecraft:shattered_savanna_plateau"; -+ BIOMES_BY_ID[165] = "minecraft:eroded_badlands"; -+ BIOMES_BY_ID[166] = "minecraft:modified_wooded_badlands_plateau"; -+ BIOMES_BY_ID[167] = "minecraft:modified_badlands_plateau"; -+ BIOMES_BY_ID[168] = "minecraft:bamboo_jungle"; -+ BIOMES_BY_ID[169] = "minecraft:bamboo_jungle_hills"; -+ BIOMES_BY_ID[170] = "minecraft:soul_sand_valley"; -+ BIOMES_BY_ID[171] = "minecraft:crimson_forest"; -+ BIOMES_BY_ID[172] = "minecraft:warped_forest"; -+ BIOMES_BY_ID[173] = "minecraft:basalt_deltas"; -+ BIOMES_BY_ID[174] = "minecraft:dripstone_caves"; -+ BIOMES_BY_ID[175] = "minecraft:lush_caves"; -+ BIOMES_BY_ID[177] = "minecraft:meadow"; -+ BIOMES_BY_ID[178] = "minecraft:grove"; -+ BIOMES_BY_ID[179] = "minecraft:snowy_slopes"; -+ BIOMES_BY_ID[180] = "minecraft:snowcapped_peaks"; -+ BIOMES_BY_ID[181] = "minecraft:lofty_peaks"; -+ BIOMES_BY_ID[182] = "minecraft:stony_peaks"; -+ } -+ -+ private static final String[] HEIGHTMAP_TYPES = new String[] { -+ "WORLD_SURFACE_WG", -+ "WORLD_SURFACE", -+ "WORLD_SURFACE_IGNORE_SNOW", -+ "OCEAN_FLOOR_WG", -+ "OCEAN_FLOOR", -+ "MOTION_BLOCKING", -+ "MOTION_BLOCKING_NO_LEAVES" -+ }; -+ -+ private static final Set STATUS_IS_OR_AFTER_SURFACE = new HashSet<>(Arrays.asList( -+ "surface", -+ "carvers", -+ "liquid_carvers", -+ "features", -+ "light", -+ "spawn", -+ "heightmaps", -+ "full" -+ )); -+ private static final Set STATUS_IS_OR_AFTER_NOISE = new HashSet<>(Arrays.asList( -+ "noise", -+ "surface", -+ "carvers", -+ "liquid_carvers", -+ "features", -+ "light", -+ "spawn", -+ "heightmaps", -+ "full" -+ )); -+ private static final Set BLOCKS_BEFORE_FEATURE_STATUS = new HashSet<>(Arrays.asList( -+ "minecraft:air", -+ "minecraft:basalt", -+ "minecraft:bedrock", -+ "minecraft:blackstone", -+ "minecraft:calcite", -+ "minecraft:cave_air", -+ "minecraft:coarse_dirt", -+ "minecraft:crimson_nylium", -+ "minecraft:dirt", -+ "minecraft:end_stone", -+ "minecraft:grass_block", -+ "minecraft:gravel", -+ "minecraft:ice", -+ "minecraft:lava", -+ "minecraft:mycelium", -+ "minecraft:nether_wart_block", -+ "minecraft:netherrack", -+ "minecraft:orange_terracotta", -+ "minecraft:packed_ice", -+ "minecraft:podzol", -+ "minecraft:powder_snow", -+ "minecraft:red_sand", -+ "minecraft:red_sandstone", -+ "minecraft:sand", -+ "minecraft:sandstone", -+ "minecraft:snow_block", -+ "minecraft:soul_sand", -+ "minecraft:soul_soil", -+ "minecraft:stone", -+ "minecraft:terracotta", -+ "minecraft:warped_nylium", -+ "minecraft:warped_wart_block", -+ "minecraft:water", -+ "minecraft:white_terracotta" -+ )); -+ -+ private static int getObjectsPerValue(final long[] val) { -+ return (4096 + val.length - 1) / (val.length); // expression is invalid if it returns > 64 -+ } -+ -+ private static long[] resize(final long[] val, final int oldBitsPerObject, final int newBitsPerObject) { -+ final long oldMask = (1L << oldBitsPerObject) - 1; // works even if bitsPerObject == 64 -+ final long newMask = (1L << newBitsPerObject) - 1; -+ final int oldObjectsPerValue = 64 / oldBitsPerObject; -+ final int newObjectsPerValue = 64 / newBitsPerObject; -+ -+ if (newBitsPerObject == oldBitsPerObject) { -+ return val; -+ } -+ -+ final int items = 4096; -+ -+ final long[] ret = new long[(items + newObjectsPerValue - 1) / newObjectsPerValue]; -+ -+ final int expectedSize = ((items + oldObjectsPerValue - 1) / oldObjectsPerValue); -+ if (val.length != expectedSize) { -+ throw new IllegalStateException("Expected size: " + expectedSize + ", got: " + val.length); -+ } -+ -+ int shift = 0; -+ int idx = 0; -+ long newCurr = 0L; -+ -+ int currItem = 0; -+ for (int i = 0; i < val.length; ++i) { -+ final long oldCurr = val[i]; -+ -+ for (int objIdx = 0; currItem < items && objIdx + oldBitsPerObject <= 64; objIdx += oldBitsPerObject, ++currItem) { -+ final long value = (oldCurr >> objIdx) & oldMask; -+ -+ if ((value & newMask) != value) { -+ throw new IllegalStateException("Old data storage has values that cannot be moved into new palette (would erase data)!"); -+ } -+ -+ newCurr |= value << shift; -+ shift += newBitsPerObject; -+ -+ if (shift + newBitsPerObject > 64) { // will next write overflow? -+ // must move to next idx -+ ret[idx++] = newCurr; -+ shift = 0; -+ newCurr = 0L; -+ } -+ } -+ } -+ -+ // don't forget to write the last one -+ if (shift != 0) { -+ ret[idx] = newCurr; -+ } -+ -+ return ret; -+ } -+ -+ private static void fixLithiumChunks(final MapType data) { -+ // See https://github.com/CaffeineMC/lithium-fabric/issues/279 -+ final MapType level = data.getMap("Level"); -+ if (level == null) { -+ return; -+ } -+ -+ final int chunkX = level.getInt("xPos"); -+ final int chunkZ = level.getInt("zPos"); -+ -+ final ListType sections = level.getList("Sections", ObjectType.MAP); -+ if (sections == null) { -+ return; -+ } -+ -+ for (int i = 0, len = sections.size(); i < len; ++i) { -+ final MapType section = sections.getMap(i); -+ -+ final int sectionY = section.getInt("Y"); -+ -+ final ListType palette = section.getList("Palette", ObjectType.MAP); -+ final long[] blockStates = section.getLongs("BlockStates"); -+ -+ if (palette == null || blockStates == null) { -+ continue; -+ } -+ -+ final int expectedBits = Math.max(4, ceilLog2(palette.size())); -+ final int gotObjectsPerValue = getObjectsPerValue(blockStates); -+ final int gotBits = 64 / gotObjectsPerValue; -+ -+ if (expectedBits == gotBits) { -+ continue; -+ } -+ -+ try { -+ section.setLongs("BlockStates", resize(blockStates, gotBits, expectedBits)); -+ } catch (final Exception ex) { -+ LOGGER.error("Failed to rewrite mismatched palette and data storage for section y: " + sectionY -+ + " for chunk [" + chunkX + "," + chunkZ + "], palette entries: " + palette.size() + ", data storage size: " -+ + blockStates.length, -+ ex -+ ); -+ } -+ } -+ } -+ -+ public static void register() { -+ // See V2551 for the layout of world gen settings -+ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ // converters were added to older versions note whether the world has increased height already or not -+ final boolean noHeightFlag = !data.hasKey("has_increased_height_already"); -+ final boolean hasIncreasedHeight = data.getBoolean("has_increased_height_already", true); -+ data.remove("has_increased_height_already"); -+ -+ final MapType dimensions = data.getMap("dimensions"); -+ if (dimensions == null) { -+ // wat -+ return null; -+ } -+ -+ // only care about overworld -+ final MapType overworld = dimensions.getMap("minecraft:overworld"); -+ if (overworld == null) { -+ // wat -+ return null; -+ } -+ -+ final MapType generator = overworld.getMap("generator"); -+ if (generator == null) { -+ // wat -+ return null; -+ } -+ -+ final String type = generator.getString("type", ""); -+ switch (type) { -+ case "minecraft:noise": { -+ final MapType biomeSource = generator.getMap("biome_source"); -+ final String sourceType = biomeSource.getString("type"); -+ -+ boolean largeBiomes = false; -+ -+ if ("minecraft:vanilla_layered".equals(sourceType) || (noHeightFlag && "minecraft:multi_noise".equals(sourceType))) { -+ largeBiomes = biomeSource.getBoolean("large_biomes"); -+ -+ final MapType newBiomeSource = Types.NBT.createEmptyMap(); -+ generator.setMap("biome_source", newBiomeSource); -+ -+ newBiomeSource.setString("preset", "minecraft:overworld"); -+ newBiomeSource.setString("type", "minecraft:multi_noise"); -+ } -+ -+ if (largeBiomes) { -+ if ("minecraft:overworld".equals(generator.getString("settings"))) { -+ generator.setString("settings", "minecraft:large_biomes"); -+ } -+ } -+ -+ break; -+ } -+ case "minecraft:flat": { -+ if (!hasIncreasedHeight) { -+ final MapType settings = generator.getMap("settings"); -+ if (settings == null) { -+ break; -+ } -+ -+ updateLayers(settings.getList("layers", ObjectType.MAP)); -+ } -+ break; -+ } -+ default: -+ // do nothing -+ break; -+ } -+ -+ return null; -+ } -+ }); -+ -+ -+ // It looks like DFU will only support worlds in the old height format or the new one, any custom one isn't supported -+ // and by not supported I mean it will just treat it as the old format... maybe at least throw in that case? -+ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ // The below covers padPaletteEntries - this was written BEFORE that code was added to the datafixer - -+ // and this still works, so I'm keeping it. Don't fix what isn't broken. -+ fixLithiumChunks(data); // See https://github.com/CaffeineMC/lithium-fabric/issues/279 -+ -+ final MapType level = data.getMap("Level"); -+ -+ if (level == null) { -+ return null; -+ } -+ -+ final MapType context = data.getMap("__context"); // Passed through by ChunkStorage -+ final String dimension = context == null ? "" : context.getString("dimension", ""); -+ final String generator = context == null ? "" : context.getString("generator", ""); -+ final boolean isOverworld = "minecraft:overworld".equals(dimension); -+ final int minSection = isOverworld ? -4 : 0; -+ final MutableBoolean isAlreadyExtended = new MutableBoolean(); -+ -+ final MapType[] newBiomes = createBiomeSections(level, isOverworld, minSection, isAlreadyExtended); -+ final MapType wrappedEmptyBlockPalette = getEmptyBlockPalette(); -+ -+ ListType sections = level.getList("Sections", ObjectType.MAP); -+ if (sections == null) { -+ level.setList("Sections", sections = Types.NBT.createEmptyList()); -+ } -+ -+ // must update sections for two things: -+ // 1. the biomes are now stored per section, so we must insert the biomes palette into each section (and create them if they don't exist) -+ // 2. each section must now have block states (or at least DFU is ensuring they do, but current code does not require) -+ V2841.SimplePaletteReader bottomSection = null; -+ final Set allBlocks = new HashSet<>(); -+ final IntOpenHashSet existingSections = new IntOpenHashSet(); -+ -+ for (int i = 0, len = sections.size(); i < len; ++i) { -+ final MapType section = sections.getMap(i); -+ -+ final int y = section.getInt("Y"); -+ final int sectionIndex = y - minSection; -+ -+ existingSections.add(y); -+ -+ // add in relevant biome section -+ if (sectionIndex >= 0 && sectionIndex < newBiomes.length) { -+ // exclude out of bounds sections (i.e the light sections above and below the world) -+ section.setMap("biomes", newBiomes[sectionIndex]); -+ } -+ -+ // update palette -+ final ListType palette = section.getList("Palette", ObjectType.MAP); -+ final long[] blockStates = section.getLongs("BlockStates"); -+ -+ section.remove("Palette"); -+ section.remove("BlockStates"); -+ -+ if (palette != null) { -+ for (int j = 0, len2 = palette.size(); j < len2; ++j) { -+ allBlocks.add(V2841.getBlockId(palette.getMap(j))); -+ } -+ } -+ -+ final MapType palettedContainer; -+ if (palette != null && blockStates != null) { -+ // only if both exist, same as DFU, same as legacy chunk loading code -+ section.setMap("block_states", palettedContainer = wrapPaletteOptimised(palette, blockStates)); -+ } else { -+ section.setMap("block_states", palettedContainer = wrappedEmptyBlockPalette.copy()); // must write a palette now, copy so that later edits do not edit them all -+ } -+ -+ if (section.getInt("Y", Integer.MAX_VALUE) == 0) { -+ bottomSection = new V2841.SimplePaletteReader(palettedContainer.getList("palette", ObjectType.MAP), palettedContainer.getLongs("data")); -+ } -+ } -+ -+ // all existing sections updated, now we must create new sections just for the biomes migration -+ for (int sectionIndex = 0; sectionIndex < newBiomes.length; ++sectionIndex) { -+ final int sectionY = sectionIndex + minSection; -+ if (!existingSections.add(sectionY)) { -+ // exists already -+ continue; -+ } -+ -+ final MapType newSection = Types.NBT.createEmptyMap(); -+ sections.addMap(newSection); -+ -+ newSection.setByte("Y", (byte)sectionY); -+ // must write a palette now, copy so that later edits do not edit them all -+ newSection.setMap("block_states", wrappedEmptyBlockPalette.copy()); -+ -+ newSection.setGeneric("biomes", newBiomes[sectionIndex]); -+ } -+ -+ // update status so interpolation can take place -+ predictChunkStatusBeforeSurface(level, allBlocks); -+ -+ // done with sections, update the rest of the chunk -+ updateChunkData(level, isOverworld, isAlreadyExtended.getValue(), "minecraft:noise".equals(generator), bottomSection); -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ final MapType dimensions = data.getMap("dimensions"); -+ -+ if (dimensions == null) { -+ return null; -+ } -+ -+ for (final String dimension : dimensions.keys()) { -+ final MapType dimensionData = dimensions.getMap(dimension); -+ if (dimensionData == null) { -+ continue; -+ } -+ -+ final MapType generator = dimensionData.getMap("generator"); -+ if (generator == null) { -+ continue; -+ } -+ -+ final String type = generator.getString("type"); -+ if (type == null) { -+ continue; -+ } -+ -+ switch (type) { -+ case "minecraft:flat": { -+ final MapType settings = generator.getMap("settings"); -+ if (settings == null) { -+ continue; -+ } -+ -+ WalkerUtils.convert(MCTypeRegistry.BIOME, settings, "biome", fromVersion, toVersion); -+ -+ final ListType layers = settings.getList("layers", ObjectType.MAP); -+ if (layers != null) { -+ for (int i = 0, len = layers.size(); i < len; ++i) { -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, layers.getMap(i), "block", fromVersion, toVersion); -+ } -+ } -+ -+ break; -+ } -+ case "minecraft:noise": { -+ final MapType settings = generator.getMap("settings"); -+ if (settings != null) { -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, settings, "default_block", fromVersion, toVersion); -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, settings, "default_fluid", fromVersion, toVersion); -+ } -+ -+ final MapType biomeSource = generator.getMap("biome_source"); -+ if (biomeSource != null) { -+ final String biomeSourceType = biomeSource.getString("type", ""); -+ switch (biomeSourceType) { -+ case "minecraft:fixed": { -+ WalkerUtils.convert(MCTypeRegistry.BIOME, biomeSource, "biome", fromVersion, toVersion); -+ break; -+ } -+ -+ case "minecraft:multi_noise": { -+ WalkerUtils.convert(MCTypeRegistry.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST, biomeSource, "preset", fromVersion, toVersion); -+ -+ // Vanilla's schema is _still_ wrong. It should be DSL.fields("biomes", DSL.list(DSL.fields("biome"))) -+ // But it just contains the list part. That obviously can never be the case, because -+ // the root object is a compound, not a list. -+ -+ final ListType biomes = biomeSource.getList("biomes", ObjectType.MAP); -+ if (biomes != null) { -+ for (int i = 0, len = biomes.size(); i < len; ++i) { -+ WalkerUtils.convert(MCTypeRegistry.BIOME, biomes.getMap(i), "biome", fromVersion, toVersion); -+ } -+ } -+ -+ break; -+ } -+ -+ case "minecraft:checkerboard": { -+ WalkerUtils.convertList(MCTypeRegistry.BIOME, biomeSource, "biomes", fromVersion, toVersion); -+ break; -+ } -+ } -+ } -+ -+ break; -+ } -+ } -+ } -+ -+ return null; -+ }); -+ -+ MCTypeRegistry.CHUNK.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ final MapType level = data.getMap("Level"); -+ if (level == null) { -+ return null; -+ } -+ -+ WalkerUtils.convertList(MCTypeRegistry.ENTITY, level, "Entities", fromVersion, toVersion); -+ WalkerUtils.convertList(MCTypeRegistry.TILE_ENTITY, level, "TileEntities", fromVersion, toVersion); -+ -+ final ListType tileTicks = level.getList("TileTicks", ObjectType.MAP); -+ if (tileTicks != null) { -+ for (int i = 0, len = tileTicks.size(); i < len; ++i) { -+ final MapType tileTick = tileTicks.getMap(i); -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, tileTick, "i", fromVersion, toVersion); -+ } -+ } -+ -+ final ListType sections = level.getList("Sections", ObjectType.MAP); -+ if (sections != null) { -+ for (int i = 0, len = sections.size(); i < len; ++i) { -+ final MapType section = sections.getMap(i); -+ -+ WalkerUtils.convertList(MCTypeRegistry.BIOME, section.getMap("biomes"), "palette", fromVersion, toVersion); -+ WalkerUtils.convertList(MCTypeRegistry.BLOCK_STATE, section.getMap("block_states"), "palette", fromVersion, toVersion); -+ } -+ } -+ -+ WalkerUtils.convertValues(MCTypeRegistry.STRUCTURE_FEATURE, level.getMap("Structures"), "Starts", fromVersion, toVersion); -+ -+ return null; -+ }); -+ } -+ -+ private static void predictChunkStatusBeforeSurface(final MapType level, final Set chunkBlocks) { -+ final String status = level.getString("Status", "empty"); -+ if (STATUS_IS_OR_AFTER_SURFACE.contains(status)) { -+ return; -+ } -+ -+ chunkBlocks.remove("minecraft:air"); -+ final boolean chunkNotEmpty = !chunkBlocks.isEmpty(); -+ chunkBlocks.removeAll(BLOCKS_BEFORE_FEATURE_STATUS); -+ final boolean chunkFeatureStatus = !chunkBlocks.isEmpty(); -+ -+ final String update; -+ if (chunkFeatureStatus) { -+ update = "liquid_carvers"; -+ } else if (!"noise".equals(status) && !chunkNotEmpty) { -+ update = "biomes".equals(status) ? "structure_references" : status; -+ } else { -+ update = "noise"; -+ } -+ -+ level.setString("Status", update); -+ } -+ -+ private static MapType getEmptyBlockPalette() { -+ final MapType airBlockState = Types.NBT.createEmptyMap(); -+ airBlockState.setString("Name", "minecraft:air"); -+ -+ final ListType emptyBlockPalette = Types.NBT.createEmptyList(); -+ emptyBlockPalette.addMap(airBlockState); -+ -+ return V2832.wrapPalette(emptyBlockPalette); -+ } -+ -+ private static void shiftUpgradeData(final MapType upgradeData, final int shift) { -+ if (upgradeData == null) { -+ return; -+ } -+ -+ final MapType indices = upgradeData.getMap("Indices"); -+ if (indices == null) { -+ return; -+ } -+ -+ RenameHelper.renameKeys(indices, (final String input) -> { -+ return Integer.toString(Integer.parseInt(input) + shift); -+ }); -+ } -+ -+ private static void updateChunkData(final MapType level, final boolean wantExtendedHeight, final boolean isAlreadyExtended, -+ final boolean onNoiseGenerator, final V2841.SimplePaletteReader bottomSection) { -+ level.remove("Biomes"); -+ if (!wantExtendedHeight) { -+ padCarvingMasks(level, 16, 0); -+ return; -+ } -+ -+ if (isAlreadyExtended) { -+ padCarvingMasks(level, 24, 0); -+ return; -+ } -+ -+ offsetHeightmaps(level); -+ // Difference from DFU: Still convert the Lights data. Just because it's being removed in a later version doesn't mean -+ // that it should be removed here. -+ // Generally, converters act only on the current version to bring it to the next. This principle allows the converter -+ // for the next version to assume that it acts on its current version, not some in-between of the current version -+ // and some future version that did not exist at the time it was written. This allows converters to be written and tested -+ // only with knowledge of the current version and the next version. -+ addEmptyListPadding(level, "Lights"); -+ addEmptyListPadding(level, "LiquidsToBeTicked"); -+ addEmptyListPadding(level, "PostProcessing"); -+ addEmptyListPadding(level, "ToBeTicked"); -+ shiftUpgradeData(level.getMap("UpgradeData"), 4); // https://bugs.mojang.com/browse/MC-238076 - fixed now, Mojang fix is identical. No change required. -+ padCarvingMasks(level, 24, 4); -+ -+ if (!onNoiseGenerator) { -+ return; -+ } -+ -+ final String status = level.getString("Status"); -+ if (status == null || "empty".equals(status)) { -+ return; -+ } -+ -+ final MapType blendingData = Types.NBT.createEmptyMap(); -+ level.setMap("blending_data", blendingData); -+ -+ blendingData.setBoolean("old_noise", STATUS_IS_OR_AFTER_NOISE.contains(status)); -+ -+ if (bottomSection == null) { -+ return; -+ } -+ -+ final BitSet missingBedrock = new BitSet(256); -+ boolean hasBedrock = status.equals("noise"); -+ -+ for (int z = 0; z <= 15; ++z) { -+ for (int x = 0; x <= 15; ++x) { -+ final MapType state = bottomSection.getState(x, 0, z); -+ final String blockId = V2841.getBlockId(state); -+ final boolean isBedrock = state != null && "minecraft:bedrock".equals(blockId); -+ final boolean isAir = state != null && "minecraft:air".equals(blockId); -+ if (isAir) { -+ missingBedrock.set((z << 4) | x); -+ } -+ -+ hasBedrock |= isBedrock; -+ } -+ } -+ -+ if (hasBedrock && missingBedrock.cardinality() != missingBedrock.size()) { -+ final String targetStatus = "full".equals(status) ? "heightmaps" : status; -+ -+ final MapType belowZeroRetrogen = Types.NBT.createEmptyMap(); -+ level.setMap("below_zero_retrogen", belowZeroRetrogen); -+ -+ belowZeroRetrogen.setString("target_status", targetStatus); -+ belowZeroRetrogen.setLongs("missing_bedrock", missingBedrock.toLongArray()); -+ -+ level.setString("Status", "empty"); -+ } -+ -+ level.setBoolean("isLightOn", false); -+ } -+ -+ private static void padCarvingMasks(final MapType level, final int newSize, final int offset) { -+ final MapType carvingMasks = level.getMap("CarvingMasks"); -+ if (carvingMasks == null) { -+ // if empty, DFU still writes -+ level.setMap("CarvingMasks", Types.NBT.createEmptyMap()); -+ return; -+ } -+ -+ for (final String key : carvingMasks.keys()) { -+ final long[] old = BitSet.valueOf(carvingMasks.getBytes(key)).toLongArray(); -+ final long[] newVal = new long[64 * newSize]; -+ -+ System.arraycopy(old, 0, newVal, 64 * offset, old.length); -+ -+ carvingMasks.setLongs(key, newVal); // no CME: key exists already -+ } -+ } -+ -+ private static void addEmptyListPadding(final MapType level, final String path) { -+ ListType list = level.getListUnchecked(path); -+ if (list != null && list.size() == 24) { -+ return; -+ } -+ -+ if (list == null) { -+ // difference from DFU: Don't create the damn thing! -+ return; -+ } -+ -+ -+ // offset the section array to the new format -+ for (int i = 0; i < 4; ++i) { -+ // always create new copies, so that modifying one doesn't modify ALL of them! -+ list.addList(0, Types.NBT.createEmptyList()); // add below -+ list.addList(Types.NBT.createEmptyList()); // add above -+ } -+ } -+ -+ private static void offsetHeightmaps(final MapType level) { -+ final MapType heightmaps = level.getMap("Heightmaps"); -+ if (heightmaps == null) { -+ return; -+ } -+ -+ for (final String key : HEIGHTMAP_TYPES) { -+ offsetHeightmap(heightmaps.getLongs(key)); -+ } -+ } -+ -+ private static void offsetHeightmap(final long[] heightmap) { -+ if (heightmap == null) { -+ return; -+ } -+ -+ // heightmaps are configured to have 9 bits per value, with 256 total values -+ // heightmaps are also relative to the lowest position -+ for (int idx = 0, len = heightmap.length; idx < len; ++idx) { -+ long curr = heightmap[idx]; -+ long next = 0L; -+ -+ for (int objIdx = 0; objIdx + 9 <= 64; objIdx += 9) { -+ final long value = (curr >> objIdx) & 511L; -+ if (value != 0L) { -+ final long offset = Math.min(511L, value + 64L); -+ -+ next |= (offset << objIdx); -+ } -+ } -+ -+ heightmap[idx] = next; -+ } -+ } -+ -+ private static MapType[] createBiomeSections(final MapType level, final boolean wantExtendedHeight, -+ final int minSection, final MutableBoolean isAlreadyExtended) { -+ final MapType[] ret = new MapType[wantExtendedHeight ? 24 : 16]; -+ -+ final int[] biomes = level.getInts("Biomes"); -+ -+ if (biomes != null && biomes.length == 1536) { // magic value for 24 sections of biomes (24 * 4^3) -+ isAlreadyExtended.setValue(true); -+ for (int sectionIndex = 0; sectionIndex < 24; ++sectionIndex) { -+ ret[sectionIndex] = createBiomeSection(biomes, sectionIndex * 64, -1); // -1 is all 1s -+ } -+ } else if (biomes != null && biomes.length == 1024) { // magic value for 24 sections of biomes (16 * 4^3) -+ for (int sectionY = 0; sectionY < 16; ++sectionY) { -+ ret[sectionY - minSection] = createBiomeSection(biomes, sectionY * 64, -1); // -1 is all 1s -+ } -+ -+ if (wantExtendedHeight) { -+ // must set the new sections at top and bottom -+ final MapType bottomCopy = createBiomeSection(biomes, 0, 15); // just want the biomes at y = 0 -+ final MapType topCopy = createBiomeSection(biomes, 1008, 15); // just want the biomes at y = 252 -+ -+ for (int sectionIndex = 0; sectionIndex < 4; ++sectionIndex) { -+ ret[sectionIndex] = bottomCopy.copy(); // copy palette so that later possible modifications don't trash all sections -+ } -+ -+ for (int sectionIndex = 20; sectionIndex < 24; ++sectionIndex) { -+ ret[sectionIndex] = topCopy.copy(); // copy palette so that later possible modifications don't trash all sections -+ } -+ } -+ } else { -+ final ListType palette = Types.NBT.createEmptyList(); -+ palette.addString("minecraft:plains"); -+ -+ for (int i = 0; i < ret.length; ++i) { -+ ret[i] = wrapPalette(palette.copy()); // copy palette so that later possible modifications don't trash all sections -+ } -+ } -+ -+ return ret; -+ } -+ -+ private static MapType createBiomeSection(final int[] biomes, final int offset, final int mask) { -+ final Int2IntLinkedOpenHashMap paletteId = new Int2IntLinkedOpenHashMap(); -+ -+ for (int idx = 0; idx < 64; ++idx) { -+ final int biome = biomes[offset + (idx & mask)]; -+ paletteId.putIfAbsent(biome, paletteId.size()); -+ } -+ -+ final ListType paletteString = Types.NBT.createEmptyList(); -+ for (final IntIterator iterator = paletteId.keySet().iterator(); iterator.hasNext();) { -+ final int biomeId = iterator.nextInt(); -+ final String biome = biomeId >= 0 && biomeId < BIOMES_BY_ID.length ? BIOMES_BY_ID[biomeId] : null; -+ paletteString.addString(biome == null ? "minecraft:plains" : biome); -+ } -+ -+ final int bitsPerObject = ceilLog2(paletteString.size()); -+ if (bitsPerObject == 0) { -+ return wrapPalette(paletteString); -+ } -+ -+ // manually create packed integer data -+ final int objectsPerValue = 64 / bitsPerObject; -+ final long[] packed = new long[(64 + objectsPerValue - 1) / objectsPerValue]; -+ -+ int shift = 0; -+ int idx = 0; -+ long curr = 0; -+ -+ for (int biome_idx = 0; biome_idx < 64; ++biome_idx) { -+ final int biome = biomes[offset + (biome_idx & mask)]; -+ -+ curr |= ((long)paletteId.get(biome)) << shift; -+ -+ shift += bitsPerObject; -+ -+ if (shift + bitsPerObject > 64) { // will next write overflow? -+ // must move to next idx -+ packed[idx++] = curr; -+ shift = 0; -+ curr = 0L; -+ } -+ } -+ -+ // don't forget to write the last one -+ if (shift != 0) { -+ packed[idx] = curr; -+ } -+ -+ return wrapPalette(paletteString, packed); -+ } -+ -+ private static MapType wrapPalette(final ListType palette) { -+ return wrapPalette(palette, null); -+ } -+ -+ private static MapType wrapPalette(final ListType palette, final long[] blockStates) { -+ final MapType ret = Types.NBT.createEmptyMap(); -+ ret.setList("palette", palette); -+ if (blockStates != null) { -+ ret.setLongs("data", blockStates); -+ } -+ -+ return ret; -+ } -+ -+ private static MapType wrapPaletteOptimised(final ListType palette, final long[] blockStates) { -+ if (palette.size() == 1) { -+ return wrapPalette(palette); -+ } -+ -+ return wrapPalette(palette, blockStates); -+ } -+ -+ public static int ceilLog2(final int value) { -+ return value == 0 ? 0 : Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros -+ } -+ -+ private static void updateLayers(final ListType layers) { -+ if (layers == null) { -+ return; -+ } -+ -+ layers.addMap(0, createEmptyLayer()); // add at the bottom -+ } -+ -+ private static MapType createEmptyLayer() { -+ final MapType ret = Types.NBT.createEmptyMap(); -+ ret.setInt("height", 64); -+ ret.setString("block", "minecraft:air"); -+ -+ return ret; -+ } -+ -+ private V2832() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2833.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2833.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4c58b3b53d8526cbde6cf1e8c90cabdaceaf2a03 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2833.java -@@ -0,0 +1,31 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V2833 { -+ -+ private static final int VERSION = MCVersions.V1_17_1 + 103; -+ -+ public static void register() { -+ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType dimensions = data.getMap("dimensions"); -+ -+ for (final String dimensionKey : dimensions.keys()) { -+ final MapType dimension = dimensions.getMap(dimensionKey); -+ if (!dimension.hasKey("type")) { -+ throw new IllegalStateException("Unable load old custom worlds."); -+ } -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V2833() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2838.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2838.java -new file mode 100644 -index 0000000000000000000000000000000000000000..356963228d884a0a74e6d7c9922b4ced627bbfb0 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2838.java -@@ -0,0 +1,62 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V2838 { -+ -+ private static final int VERSION = MCVersions.V21W40A; -+ -+ public static final Map BIOME_UPDATE = new HashMap<>( -+ ImmutableMap.builder() -+ .put("minecraft:badlands_plateau", "minecraft:badlands") -+ .put("minecraft:bamboo_jungle_hills", "minecraft:bamboo_jungle") -+ .put("minecraft:birch_forest_hills", "minecraft:birch_forest") -+ .put("minecraft:dark_forest_hills", "minecraft:dark_forest") -+ .put("minecraft:desert_hills", "minecraft:desert") -+ .put("minecraft:desert_lakes", "minecraft:desert") -+ .put("minecraft:giant_spruce_taiga_hills", "minecraft:old_growth_spruce_taiga") -+ .put("minecraft:giant_spruce_taiga", "minecraft:old_growth_spruce_taiga") -+ .put("minecraft:giant_tree_taiga_hills", "minecraft:old_growth_pine_taiga") -+ .put("minecraft:giant_tree_taiga", "minecraft:old_growth_pine_taiga") -+ .put("minecraft:gravelly_mountains", "minecraft:windswept_gravelly_hills") -+ .put("minecraft:jungle_edge", "minecraft:sparse_jungle") -+ .put("minecraft:jungle_hills", "minecraft:jungle") -+ .put("minecraft:modified_badlands_plateau", "minecraft:badlands") -+ .put("minecraft:modified_gravelly_mountains", "minecraft:windswept_gravelly_hills") -+ .put("minecraft:modified_jungle_edge", "minecraft:sparse_jungle") -+ .put("minecraft:modified_jungle", "minecraft:jungle") -+ .put("minecraft:modified_wooded_badlands_plateau", "minecraft:wooded_badlands") -+ .put("minecraft:mountain_edge", "minecraft:windswept_hills") -+ .put("minecraft:mountains", "minecraft:windswept_hills") -+ .put("minecraft:mushroom_field_shore", "minecraft:mushroom_fields") -+ .put("minecraft:shattered_savanna", "minecraft:windswept_savanna") -+ .put("minecraft:shattered_savanna_plateau", "minecraft:windswept_savanna") -+ .put("minecraft:snowy_mountains", "minecraft:snowy_plains") -+ .put("minecraft:snowy_taiga_hills", "minecraft:snowy_taiga") -+ .put("minecraft:snowy_taiga_mountains", "minecraft:snowy_taiga") -+ .put("minecraft:snowy_tundra", "minecraft:snowy_plains") -+ .put("minecraft:stone_shore", "minecraft:stony_shore") -+ .put("minecraft:swamp_hills", "minecraft:swamp") -+ .put("minecraft:taiga_hills", "minecraft:taiga") -+ .put("minecraft:taiga_mountains", "minecraft:taiga") -+ .put("minecraft:tall_birch_forest", "minecraft:old_growth_birch_forest") -+ .put("minecraft:tall_birch_hills", "minecraft:old_growth_birch_forest") -+ .put("minecraft:wooded_badlands_plateau", "minecraft:wooded_badlands") -+ .put("minecraft:wooded_hills", "minecraft:forest") -+ .put("minecraft:wooded_mountains", "minecraft:windswept_forest") -+ .put("minecraft:lofty_peaks", "minecraft:jagged_peaks") -+ .put("minecraft:snowcapped_peaks", "minecraft:frozen_peaks") -+ .build() -+ ); -+ -+ public static void register() { -+ ConverterAbstractStringValueTypeRename.register(VERSION, MCTypeRegistry.BIOME, BIOME_UPDATE::get); -+ } -+ -+ private V2838() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2841.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2841.java -new file mode 100644 -index 0000000000000000000000000000000000000000..bd8117a101d308e59251f927feb0692a6b22f547 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2841.java -@@ -0,0 +1,210 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.Types; -+import ca.spottedleaf.dataconverter.util.IntegerUtil; -+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -+import java.util.Arrays; -+import java.util.HashSet; -+import java.util.Set; -+ -+public final class V2841 { -+ -+ private static final int VERSION = MCVersions.V21W42A + 1; -+ -+ private static final Set ALWAYS_WATERLOGGED = new HashSet<>( -+ Arrays.asList( -+ "minecraft:bubble_column", -+ "minecraft:kelp", -+ "minecraft:kelp_plant", -+ "minecraft:seagrass", -+ "minecraft:tall_seagrass" -+ ) -+ ); -+ -+ public static void register() { -+ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType root, final long sourceVersion, final long toVersion) { -+ final MapType level = root.getMap("Level"); -+ if (level == null) { -+ return null; -+ } -+ -+ { -+ // Why it's renamed here and not the next data version is beyond me. -+ final MapType liquidTicks = level.getMap("LiquidTicks"); -+ if (liquidTicks != null) { -+ level.remove("LiquidTicks"); -+ level.setMap("fluid_ticks", liquidTicks); -+ } -+ } -+ -+ final Int2ObjectOpenHashMap sectionBlocks = new Int2ObjectOpenHashMap<>(); -+ final ListType sections = level.getList("Sections", ObjectType.MAP); -+ int minSection = 0; // TODO wtf is this -+ if (sections != null) { -+ for (int i = 0, len = sections.size(); i < len; ++i) { -+ final MapType section = sections.getMap(i); -+ -+ final int sectionY = section.getInt("Y"); -+ if (sectionY < minSection && section.hasKey("biomes")) { -+ minSection = sectionY; -+ } -+ -+ final MapType blockStates = section.getMap("block_states"); -+ if (blockStates == null) { -+ continue; -+ } -+ -+ sectionBlocks.put(sectionY, new SimplePaletteReader(section.getList("palette", ObjectType.MAP), section.getLongs("data"))); -+ } -+ } -+ -+ level.setByte("yPos", (byte)minSection); // TODO ??????????????????????????????????????? -+ -+ if (level.hasKey("fluid_ticks") || level.hasKey("TileTicks")) { -+ return null; -+ } -+ -+ final int sectionX = level.getInt("xPos"); -+ final int sectionZ = level.getInt("zPos"); -+ -+ final ListType fluidTicks = level.getList("LiquidsToBeTicked", ObjectType.LIST); -+ final ListType blockTicks = level.getList("ToBeTicked", ObjectType.LIST); -+ level.remove("LiquidsToBeTicked"); -+ level.remove("ToBeTicked"); -+ -+ level.setList("fluid_ticks", migrateTickList(fluidTicks, false, sectionBlocks, sectionX, minSection, sectionZ)); -+ level.setList("TileTicks", migrateTickList(blockTicks, true, sectionBlocks, sectionX, minSection, sectionZ)); -+ -+ return null; -+ } -+ }); -+ } -+ -+ public static ListType migrateTickList(final ListType ticks, final boolean blockTicks, final Int2ObjectOpenHashMap sectionBlocks, -+ final int sectionX, final int minSection, final int sectionZ) { -+ final ListType ret = Types.NBT.createEmptyList(); -+ -+ if (ticks == null) { -+ return ret; -+ } -+ -+ for (int sectionIndex = 0, totalSections = ticks.size(); sectionIndex < totalSections; ++sectionIndex) { -+ final int sectionY = sectionIndex + minSection; -+ final ListType sectionTicks = ticks.getList(sectionIndex); -+ final SimplePaletteReader palette = sectionBlocks.get(sectionY); -+ -+ for (int i = 0, len = sectionTicks.size(); i < len; ++i) { -+ final int localIndex = sectionTicks.getShort(i) & 0xFFFF; -+ final MapType blockState = palette == null ? null : palette.getState(localIndex); -+ final String subjectId = blockTicks ? getBlockId(blockState) : getLiquidId(blockState); -+ -+ ret.addMap(createNewTick(subjectId, localIndex, sectionX, sectionY, sectionZ)); -+ } -+ } -+ -+ return ret; -+ } -+ -+ public static MapType createNewTick(final String subjectId, final int localIndex, final int sectionX, final int sectionY, final int sectionZ) { -+ final int newX = (localIndex & 15) + (sectionX << 4); -+ final int newZ = ((localIndex >> 4) & 15) + (sectionZ << 4); -+ final int newY = ((localIndex >> 8) & 15) + (sectionY << 4); -+ -+ final MapType ret = Types.NBT.createEmptyMap(); -+ -+ ret.setString("i", subjectId); -+ ret.setInt("x", newX); -+ ret.setInt("y", newY); -+ ret.setInt("z", newZ); -+ ret.setInt("t", 0); -+ ret.setInt("p", 0); -+ -+ return ret; -+ } -+ -+ public static String getBlockId(final MapType blockState) { -+ return blockState == null ? "minecraft:air" : blockState.getString("Name", "minecraft:air"); -+ } -+ -+ private static String getLiquidId(final MapType blockState) { -+ if (blockState == null) { -+ return "minecraft:empty"; -+ } -+ -+ final String name = blockState.getString("Name"); -+ if (ALWAYS_WATERLOGGED.contains(name)) { -+ return "minecraft:water"; -+ } -+ -+ final MapType properties = blockState.getMap("Properties"); -+ // Correctly read block state properties as strings - https://github.com/PaperMC/DataConverter/issues/6 -+ if ("minecraft:water".equals(name)) { -+ return properties != null && "0".equals(properties.getString("level")) ? "minecraft:water" : "minecraft:flowing_water"; -+ } else if ("minecraft:lava".equals(name)) { -+ return properties != null && "0".equals(properties.getString("level")) ? "minecraft:lava" : "minecraft:flowing_lava"; -+ } -+ -+ return (properties != null && "true".equals(properties.getString("waterlogged"))) ? "minecraft:water" : "minecraft:empty"; -+ } -+ -+ private V2841() {} -+ -+ public static final class SimplePaletteReader { -+ -+ public final ListType palette; -+ public final long[] data; -+ private final int bitsPerValue; -+ private final long mask; -+ private final int valuesPerLong; -+ -+ public SimplePaletteReader(final ListType palette, final long[] data) { -+ this.palette = palette == null ? null : (palette.size() == 0 ? null : palette); -+ this.data = data; -+ this.bitsPerValue = Math.max(4, IntegerUtil.ceilLog2(this.palette == null ? 0 : this.palette.size())); -+ this.mask = (1L << this.bitsPerValue) - 1L; -+ this.valuesPerLong = (int)(64L / this.bitsPerValue); -+ } -+ -+ public MapType getState(final int x, final int y, final int z) { -+ final int index = x | (z << 4) | (y << 8); -+ return this.getState(index); -+ } -+ -+ public MapType getState(final int index) { -+ final ListType palette = this.palette; -+ if (palette == null) { -+ return null; -+ } -+ -+ final int paletteSize = palette.size(); -+ if (paletteSize == 1) { -+ return palette.getMap(0); -+ } -+ -+ // x86 div computes mod. no loss here using mod -+ // if needed, can compute magic mul and shift for div values using IntegerUtil -+ final int dataIndex = index / this.valuesPerLong; -+ final int localIndex = (index % this.valuesPerLong) * this.bitsPerValue; -+ final long[] data = this.data; -+ if (dataIndex < 0 || dataIndex >= data.length) { -+ return null; -+ } -+ -+ final long value = data[dataIndex]; -+ final int paletteIndex = (int)((value >>> localIndex) & this.mask); -+ if (paletteIndex < 0 || paletteIndex >= paletteSize) { -+ return null; -+ } -+ -+ return palette.getMap(paletteIndex); -+ } -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2842.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2842.java -new file mode 100644 -index 0000000000000000000000000000000000000000..03b3d8e2b97a346a45e6c57cb07474baa3bb6096 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2842.java -@@ -0,0 +1,78 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+ -+public final class V2842 { -+ -+ private static final int VERSION = MCVersions.V21W42A + 2; -+ -+ public static void register() { -+ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType root, final long sourceVersion, final long toVersion) { -+ final MapType level = root.getMap("Level"); -+ root.remove("Level"); -+ -+ if (!root.isEmpty()) { -+ for (final String key : root.keys()) { -+ if (level.hasKey(key)) { -+ // Don't clobber Level's data -+ continue; -+ } -+ level.setGeneric(key, root.getGeneric(key)); -+ } -+ } -+ -+ // Rename top level first -+ RenameHelper.renameSingle(level, "TileEntities", "block_entities"); -+ RenameHelper.renameSingle(level, "TileTicks", "block_ticks"); -+ RenameHelper.renameSingle(level, "Entities", "entities"); -+ RenameHelper.renameSingle(level, "Sections", "sections"); -+ RenameHelper.renameSingle(level, "Structures", "structures"); -+ -+ // 2nd level -+ final MapType structures = level.getMap("structures"); -+ if (structures != null) { -+ RenameHelper.renameSingle(structures, "Starts", "starts"); -+ } -+ -+ return level; // Level is now root tag. -+ } -+ }); -+ -+ MCTypeRegistry.CHUNK.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convertList(MCTypeRegistry.ENTITY, data, "entities", fromVersion, toVersion); -+ WalkerUtils.convertList(MCTypeRegistry.TILE_ENTITY, data, "block_entities", fromVersion, toVersion); -+ -+ final ListType blockTicks = data.getList("block_ticks", ObjectType.MAP); -+ if (blockTicks != null) { -+ for (int i = 0, len = blockTicks.size(); i < len; ++i) { -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, blockTicks.getMap(i), "i", fromVersion, toVersion); -+ } -+ } -+ -+ final ListType sections = data.getList("sections", ObjectType.MAP); -+ if (sections != null) { -+ for (int i = 0, len = sections.size(); i < len; ++i) { -+ final MapType section = sections.getMap(i); -+ -+ WalkerUtils.convertList(MCTypeRegistry.BIOME, section.getMap("biomes"), "palette", fromVersion, toVersion); -+ WalkerUtils.convertList(MCTypeRegistry.BLOCK_STATE, section.getMap("block_states"), "palette", fromVersion, toVersion); -+ } -+ } -+ -+ WalkerUtils.convertValues(MCTypeRegistry.STRUCTURE_FEATURE, data.getMap("structures"), "starts", fromVersion, toVersion); -+ -+ return null; -+ }); -+ } -+ -+ private V2842() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2843.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2843.java -new file mode 100644 -index 0000000000000000000000000000000000000000..28a7596b62f8a918f342e2d06eda050a7f2bad0b ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2843.java -@@ -0,0 +1,111 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.Types; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V2843 { -+ -+ private static final int VERSION = MCVersions.V21W42A + 3; -+ -+ public static void register() { -+ ConverterAbstractStringValueTypeRename.register(VERSION, MCTypeRegistry.BIOME, -+ new HashMap<>( -+ Map.of("minecraft:deep_warm_ocean", "minecraft:warm_ocean") -+ )::get -+ ); -+ -+ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ private static void moveOutOfBoundTicks(final ListType ticks, final MapType chunkRoot, final int chunkX, final int chunkZ, final String intoKey) { -+ if (ticks == null) { -+ return; -+ } -+ -+ ListType outOfBounds = null; -+ for (int i = 0, len = ticks.size(); i < len; ++i) { -+ final MapType tick = ticks.getMap(i); -+ final int x = tick.getInt("x"); -+ final int z = tick.getInt("z"); -+ // anything > 1 is lost, anything less than 1 stays. -+ if (Math.max(Math.abs(chunkX - (x >> 4)), Math.abs(chunkZ - (z >> 4))) == 1) { -+ // DFU doesn't remove, so neither do we. -+ if (outOfBounds == null) { -+ outOfBounds = Types.NBT.createEmptyList(); -+ } -+ outOfBounds.addMap(tick.copy()); -+ } -+ } -+ -+ if (outOfBounds != null) { -+ MapType upgradeData = chunkRoot.getMap("UpgradeData"); -+ if (upgradeData == null) { -+ chunkRoot.setMap("UpgradeData", upgradeData = Types.NBT.createEmptyMap()); -+ } -+ upgradeData.setList(intoKey, outOfBounds); -+ } -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ // After renames, so use new names -+ final int x = data.getInt("xPos"); -+ final int z = data.getInt("zPos"); -+ -+ moveOutOfBoundTicks(data.getList("block_ticks", ObjectType.MAP), data, x, z, "neighbor_block_ticks"); -+ moveOutOfBoundTicks(data.getList("fluid_ticks", ObjectType.MAP), data, x, z, "neighbor_fluid_ticks"); -+ -+ return null; -+ } -+ }); -+ -+ // DFU is missing schema for UpgradeData block names -+ MCTypeRegistry.CHUNK.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convertList(MCTypeRegistry.ENTITY, data, "entities", fromVersion, toVersion); -+ WalkerUtils.convertList(MCTypeRegistry.TILE_ENTITY, data, "block_entities", fromVersion, toVersion); -+ -+ final ListType blockTicks = data.getList("block_ticks", ObjectType.MAP); -+ if (blockTicks != null) { -+ for (int i = 0, len = blockTicks.size(); i < len; ++i) { -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, blockTicks.getMap(i), "i", fromVersion, toVersion); -+ } -+ } -+ -+ final MapType upgradeData = data.getMap("UpgradeData"); -+ if (upgradeData != null) { -+ // Even though UpgradeData will retrieve the block from the World when the type no longer exists, -+ // the type from the world may not match what was actually queued. So, even though it may look like we -+ // can skip the walker here, we actually don't if we want to be thorough. -+ final ListType neighbourBlockTicks = upgradeData.getList("neighbor_block_ticks", ObjectType.MAP); -+ if (neighbourBlockTicks != null) { -+ for (int i = 0, len = neighbourBlockTicks.size(); i < len; ++i) { -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, neighbourBlockTicks.getMap(i), "i", fromVersion, toVersion); -+ } -+ } -+ } -+ -+ final ListType sections = data.getList("sections", ObjectType.MAP); -+ if (sections != null) { -+ for (int i = 0, len = sections.size(); i < len; ++i) { -+ final MapType section = sections.getMap(i); -+ -+ WalkerUtils.convertList(MCTypeRegistry.BIOME, section.getMap("biomes"), "palette", fromVersion, toVersion); -+ WalkerUtils.convertList(MCTypeRegistry.BLOCK_STATE, section.getMap("block_states"), "palette", fromVersion, toVersion); -+ } -+ } -+ -+ WalkerUtils.convertValues(MCTypeRegistry.STRUCTURE_FEATURE, data.getMap("structures"), "starts", fromVersion, toVersion); -+ -+ return null; -+ }); -+ } -+ -+ private V2843() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2846.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2846.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e32224267d53d82ba15942141a5cb7a19eb380f2 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2846.java -@@ -0,0 +1,23 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.advancements.ConverterAbstractAdvancementsRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+ -+public final class V2846 { -+ -+ private static final int VERSION = MCVersions.V21W44A + 1; -+ -+ public static void register() { -+ ConverterAbstractAdvancementsRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:husbandry/play_jukebox_in_meadows", "minecraft:adventure/play_jukebox_in_meadows", -+ "minecraft:adventure/caves_and_cliff", "minecraft:adventure/fall_from_world_height", -+ "minecraft:adventure/ride_strider_in_overworld_lava", "minecraft:nether/ride_strider_in_overworld_lava" -+ ) -+ )::get); -+ } -+ -+ private V2846() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2852.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2852.java -new file mode 100644 -index 0000000000000000000000000000000000000000..224ee1d9a3ba68e5a617c2c0846be47feef0bed1 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2852.java -@@ -0,0 +1,31 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V2852 { -+ -+ private static final int VERSION = MCVersions.V1_18_PRE5 + 1; -+ -+ public static void register() { -+ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType dimensions = data.getMap("dimensions"); -+ -+ for (final String dimensionKey : dimensions.keys()) { -+ final MapType dimension = dimensions.getMap(dimensionKey); -+ if (!dimension.hasKey("type")) { -+ throw new IllegalStateException("Unable load old custom worlds."); -+ } -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V2852() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2967.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2967.java -new file mode 100644 -index 0000000000000000000000000000000000000000..eddfdecffcaf5e4cd7e5c3a79864816ffbaacae1 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2967.java -@@ -0,0 +1,58 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V2967 { -+ -+ private static final int VERSION = MCVersions.V22W05A; -+ -+ public static void register() { -+ MCTypeRegistry.WORLD_GEN_SETTINGS.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType dimensions = data.getMap("dimensions"); -+ -+ if (dimensions == null) { -+ return null; -+ } -+ -+ for (final String dimension : dimensions.keys()) { -+ final MapType dimensionData = dimensions.getMap(dimension); -+ if (dimensionData == null) { -+ continue; -+ } -+ -+ final MapType generator = dimensionData.getMap("generator"); -+ if (generator == null) { -+ continue; -+ } -+ -+ final MapType settings = generator.getMap("settings"); -+ if (settings == null) { -+ continue; -+ } -+ -+ final MapType structures = settings.getMap("structures"); -+ if (structures == null) { -+ continue; -+ } -+ -+ for (final String structureKey : structures.keys()) { -+ structures.getMap(structureKey).setString("type", "minecraft:random_spread"); -+ } -+ -+ final MapType stronghold = structures.getMap("stronghold"); -+ stronghold.setString("type", "minecraft:concentric_rings"); -+ structures.setMap("minecraft:stronghold", stronghold.copy()); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V2967() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2970.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2970.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ce083fca547170bb1e1014e868beaf535e940fc3 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V2970.java -@@ -0,0 +1,209 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import com.google.common.collect.ImmutableMap; -+import com.mojang.logging.LogUtils; -+import it.unimi.dsi.fastutil.objects.Object2IntMap; -+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; -+import org.slf4j.Logger; -+import java.util.HashMap; -+import java.util.Iterator; -+import java.util.List; -+import java.util.Locale; -+import java.util.Map; -+ -+public final class V2970 { -+ -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ -+ private static final int VERSION = MCVersions.V22W07A + 1; -+ private static final Map CONVERSION_MAP = new HashMap<>( -+ ImmutableMap.builder() -+ .put("mineshaft", BiomeRemap.create(Map.of(List.of("minecraft:badlands", "minecraft:eroded_badlands", "minecraft:wooded_badlands"), "minecraft:mineshaft_mesa"), "minecraft:mineshaft")) -+ .put("shipwreck", BiomeRemap.create(Map.of(List.of("minecraft:beach", "minecraft:snowy_beach"), "minecraft:shipwreck_beached"), "minecraft:shipwreck")) -+ .put("ocean_ruin", BiomeRemap.create(Map.of(List.of("minecraft:warm_ocean", "minecraft:lukewarm_ocean", "minecraft:deep_lukewarm_ocean"), "minecraft:ocean_ruin_warm"), "minecraft:ocean_ruin_cold")) -+ .put("village", BiomeRemap.create(Map.of(List.of("minecraft:desert"), "minecraft:village_desert", List.of("minecraft:savanna"), "minecraft:village_savanna", List.of("minecraft:snowy_plains"), "minecraft:village_snowy", List.of("minecraft:taiga"), "minecraft:village_taiga"), "minecraft:village_plains")) -+ .put("ruined_portal", BiomeRemap.create(Map.of(List.of("minecraft:desert"), "minecraft:ruined_portal_desert", List.of("minecraft:badlands", "minecraft:eroded_badlands", "minecraft:wooded_badlands", "minecraft:windswept_hills", "minecraft:windswept_forest", "minecraft:windswept_gravelly_hills", "minecraft:savanna_plateau", "minecraft:windswept_savanna", "minecraft:stony_shore", "minecraft:meadow", "minecraft:frozen_peaks", "minecraft:jagged_peaks", "minecraft:stony_peaks", "minecraft:snowy_slopes"), "minecraft:ruined_portal_mountain", List.of("minecraft:bamboo_jungle", "minecraft:jungle", "minecraft:sparse_jungle"), "minecraft:ruined_portal_jungle", List.of("minecraft:deep_frozen_ocean", "minecraft:deep_cold_ocean", "minecraft:deep_ocean", "minecraft:deep_lukewarm_ocean", "minecraft:frozen_ocean", "minecraft:ocean", "minecraft:cold_ocean", "minecraft:lukewarm_ocean", "minecraft:warm_ocean"), "minecraft:ruined_portal_ocean"), "minecraft:ruined_portal")) // Fix MC-248814, ruined_portal_standard->ruined_portal -+ .put("pillager_outpost", BiomeRemap.create("minecraft:pillager_outpost")) -+ .put("mansion", BiomeRemap.create("minecraft:mansion")) -+ .put("jungle_pyramid", BiomeRemap.create("minecraft:jungle_pyramid")) -+ .put("desert_pyramid", BiomeRemap.create("minecraft:desert_pyramid")) -+ .put("igloo", BiomeRemap.create("minecraft:igloo")) -+ .put("swamp_hut", BiomeRemap.create("minecraft:swamp_hut")) -+ .put("stronghold", BiomeRemap.create("minecraft:stronghold")) -+ .put("monument", BiomeRemap.create("minecraft:monument")) -+ .put("fortress", BiomeRemap.create("minecraft:fortress")) -+ .put("endcity", BiomeRemap.create("minecraft:end_city")) -+ .put("buried_treasure", BiomeRemap.create("minecraft:buried_treasure")) -+ .put("nether_fossil", BiomeRemap.create("minecraft:nether_fossil")) -+ .put("bastion_remnant", BiomeRemap.create("minecraft:bastion_remnant")) -+ .build() -+ ); -+ -+ public static void register() { -+ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ private static Object2IntOpenHashMap countBiomes(final MapType chunk) { -+ final ListType sections = chunk.getList("sections", ObjectType.MAP); -+ if (sections == null) { -+ return null; -+ } -+ -+ final Object2IntOpenHashMap ret = new Object2IntOpenHashMap<>(); -+ -+ for (int i = 0, len = sections.size(); i < len; ++i) { -+ final MapType section = sections.getMap(i); -+ -+ final MapType biomes = section.getMap("biomes"); -+ -+ if (biomes == null) { -+ continue; -+ } -+ -+ final ListType palette = biomes.getList("palette", ObjectType.STRING); -+ -+ if (palette == null) { -+ continue; -+ } -+ -+ for (int k = 0, len2 = palette.size(); k < len2; ++k) { -+ ret.addTo(palette.getString(k), 1); -+ } -+ } -+ -+ return ret; -+ } -+ -+ private static String getStructureConverted(String id, final Object2IntOpenHashMap biomeCount) { -+ id = id.toLowerCase(Locale.ROOT); -+ final BiomeRemap remap = CONVERSION_MAP.get(id); -+ if (remap == null) { -+ return null; -+ } -+ if (remap.biomeToNewStructure == null || biomeCount == null) { -+ return remap.dfl; -+ } -+ -+ final Object2IntOpenHashMap remapCount = new Object2IntOpenHashMap<>(); -+ -+ for (final Iterator> iterator = biomeCount.object2IntEntrySet().fastIterator(); iterator.hasNext();) { -+ final Object2IntMap.Entry entry = iterator.next(); -+ final String remappedStructure = remap.biomeToNewStructure.get(entry.getKey()); -+ if (remappedStructure != null) { -+ remapCount.addTo(remappedStructure, entry.getIntValue()); -+ } -+ } -+ -+ String converted = remap.dfl; -+ int maxCount = 0; -+ -+ for (final Iterator> iterator = remapCount.object2IntEntrySet().fastIterator(); iterator.hasNext();) { -+ final Object2IntMap.Entry entry = iterator.next(); -+ final int count = entry.getIntValue(); -+ if (count > maxCount) { -+ maxCount = count; -+ converted = entry.getKey(); -+ } -+ } -+ -+ return converted; -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType structures = data.getMap("structures"); -+ if (structures == null || structures.isEmpty()) { -+ return null; -+ } -+ -+ final Object2IntOpenHashMap biomeCounts = countBiomes(data); -+ -+ final MapType starts = structures.getMap("starts"); -+ final MapType references = structures.getMap("References"); -+ -+ if (starts != null) { -+ final MapType newStarts = data.getTypeUtil().createEmptyMap(); -+ structures.setMap("starts", newStarts); -+ -+ for (final String key : starts.keys()) { -+ final MapType value = starts.getMap(key); -+ if ("INVALID".equals(value.getString("id", "INVALID"))) { -+ continue; -+ } -+ -+ final String remapped = getStructureConverted(key, biomeCounts); -+ -+ if (remapped == null) { -+ LOGGER.warn("Encountered unknown structure in dataconverter: " + key); -+ continue; -+ } -+ -+ value.setString("id", remapped); -+ newStarts.setMap(remapped, value); -+ } -+ } -+ -+ // This TRULY is a guess, no idea what biomes the referent has. -+ if (references != null) { -+ final MapType newReferences = data.getTypeUtil().createEmptyMap(); -+ structures.setMap("References", newReferences); -+ for (final String key : references.keys()) { -+ final long[] value = references.getLongs(key); -+ if (value.length == 0) { -+ continue; -+ } -+ -+ final String newKey = getStructureConverted(key, biomeCounts); -+ if (newKey == null) { -+ LOGGER.warn("Encountered unknown structure reference in dataconverter: " + key); -+ continue; -+ } -+ -+ newReferences.setLongs(newKey, value); -+ } -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V2970() {} -+ -+ private static final class BiomeRemap { -+ -+ public final Map biomeToNewStructure; -+ public final String dfl; -+ -+ private BiomeRemap(final Map biomeToNewStructure, final String dfl) { -+ this.biomeToNewStructure = biomeToNewStructure; -+ this.dfl = dfl; -+ } -+ -+ public static BiomeRemap create(final String newId) { -+ return new BiomeRemap(null, newId); -+ } -+ -+ public static BiomeRemap create(final Map, String> biomeMap, final String newId) { -+ final Map biomeToNewStructure = new HashMap<>(); -+ -+ for (final Map.Entry, String> entry : biomeMap.entrySet()) { -+ final List biomes = entry.getKey(); -+ final String newBiomeStructure = entry.getValue(); -+ -+ for (int i = 0, len = biomes.size(); i < len; ++i) { -+ final String biome = biomes.get(i); -+ if (biomeToNewStructure.putIfAbsent(biome, newBiomeStructure) != null) { -+ throw new IllegalStateException("Duplicate biome remap: " + biome + " -> " + newBiomeStructure + ", but already mapped to " + biomeToNewStructure.get(biome)); -+ } -+ } -+ } -+ -+ return new BiomeRemap(biomeToNewStructure, newId); -+ } -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3077.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3077.java -new file mode 100644 -index 0000000000000000000000000000000000000000..06fe7dd2580cd8eaad9e0c7de8d0e27287d1b0a9 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3077.java -@@ -0,0 +1,40 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+ -+public final class V3077 { -+ -+ private static final int VERSION = MCVersions.V1_18_2 + 102; -+ -+ public static void register() { -+ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final boolean isLightOn = data.getBoolean("isLightOn"); -+ if (isLightOn) { -+ return null; -+ } -+ -+ final ListType sections = data.getList("sections", ObjectType.MAP); -+ if (sections == null) { -+ return null; -+ } -+ -+ for (int i = 0, len = sections.size(); i < len; ++i) { -+ final MapType section = sections.getMap(i); -+ section.remove("BlockLight"); -+ section.remove("SkyLight"); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V3077() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3078.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3078.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a0e89f59c75f6d34480f4b8c4f8fa013dddc5d52 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3078.java -@@ -0,0 +1,23 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.game_event.GameEventListenerWalker; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+ -+public final class V3078 { -+ -+ private static final int VERSION = MCVersions.V1_18_2 + 103; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ registerMob("minecraft:frog"); -+ registerMob("minecraft:tadpole"); -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:sculk_shrieker", new GameEventListenerWalker()); -+ } -+ -+ private V3078() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3081.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3081.java -new file mode 100644 -index 0000000000000000000000000000000000000000..fb108cb7f50755b52f537a888ca155aa9db39b3a ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3081.java -@@ -0,0 +1,21 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.game_event.GameEventListenerWalker; -+ -+public final class V3081 { -+ -+ private static final int VERSION = MCVersions.V22W11A + 1; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ registerMob("minecraft:warden"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:warden", new GameEventListenerWalker()); -+ } -+ -+ private V3081() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3082.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3082.java -new file mode 100644 -index 0000000000000000000000000000000000000000..79768b25a32a5333f8cb6ec6e8c422478a6891df ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3082.java -@@ -0,0 +1,16 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+ -+public final class V3082 { -+ -+ private static final int VERSION = MCVersions.V22W11A + 2; -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:chest_boat", new DataWalkerItemLists("Items")); -+ } -+ -+ private V3082() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3083.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3083.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e6e70ff4a28446fd8c4c663e0e44791ec4e5ac0a ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3083.java -@@ -0,0 +1,23 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.game_event.GameEventListenerWalker; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+ -+public final class V3083 { -+ -+ private static final int VERSION = MCVersions.V22W12A + 1; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ registerMob("minecraft:allay"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:allay", new DataWalkerItemLists("Inventory")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:allay", new GameEventListenerWalker()); -+ } -+ -+ private V3083() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3084.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3084.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6a096226995e89285054b4ab35ed3e14ae4da694 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3084.java -@@ -0,0 +1,42 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.util.NamespaceUtil; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V3084 { -+ -+ private static final int VERSION = MCVersions.V22W12A + 2; -+ -+ private static final Map GAME_EVENT_RENAMES = new HashMap<>( -+ ImmutableMap.builder() -+ .put("minecraft:block_press", "minecraft:block_activate") -+ .put("minecraft:block_switch", "minecraft:block_activate") -+ .put("minecraft:block_unpress", "minecraft:block_deactivate") -+ .put("minecraft:block_unswitch", "minecraft:block_deactivate") -+ .put("minecraft:drinking_finish", "minecraft:drink") -+ .put("minecraft:elytra_free_fall", "minecraft:elytra_glide") -+ .put("minecraft:entity_damaged", "minecraft:entity_damage") -+ .put("minecraft:entity_dying", "minecraft:entity_die") -+ .put("minecraft:entity_killed", "minecraft:entity_die") -+ .put("minecraft:mob_interact", "minecraft:entity_interact") -+ .put("minecraft:ravager_roar", "minecraft:entity_roar") -+ .put("minecraft:ring_bell", "minecraft:block_change") -+ .put("minecraft:shulker_close", "minecraft:container_close") -+ .put("minecraft:shulker_open", "minecraft:container_open") -+ .put("minecraft:wolf_shaking", "minecraft:entity_shake") -+ .build() -+ ); -+ -+ public static void register() { -+ ConverterAbstractStringValueTypeRename.register(VERSION, MCTypeRegistry.GAME_EVENT_NAME, (final String name) -> { -+ return GAME_EVENT_RENAMES.get(NamespaceUtil.correctNamespace(name)); -+ }); -+ } -+ -+ private V3084() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3086.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3086.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f06412a417ba12111c9e8f30b747ed3ad6dcbcb6 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3086.java -@@ -0,0 +1,54 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.advancements.ConverterCriteriaRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterEntityToVariant; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import com.google.common.collect.ImmutableMap; -+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V3086 { -+ -+ private static final int VERSION = MCVersions.V22W13A + 1; -+ -+ private static final Int2ObjectOpenHashMap CAT_ID_CONVERSION = new Int2ObjectOpenHashMap<>(); -+ static { -+ CAT_ID_CONVERSION.defaultReturnValue("minecraft:tabby"); -+ CAT_ID_CONVERSION.put(0, "minecraft:tabby"); -+ CAT_ID_CONVERSION.put(1, "minecraft:black"); -+ CAT_ID_CONVERSION.put(2, "minecraft:red"); -+ CAT_ID_CONVERSION.put(3, "minecraft:siamese"); -+ CAT_ID_CONVERSION.put(4, "minecraft:british"); -+ CAT_ID_CONVERSION.put(5, "minecraft:calico"); -+ CAT_ID_CONVERSION.put(6, "minecraft:persian"); -+ CAT_ID_CONVERSION.put(7, "minecraft:ragdoll"); -+ CAT_ID_CONVERSION.put(8, "minecraft:white"); -+ CAT_ID_CONVERSION.put(9, "minecraft:jellie"); -+ CAT_ID_CONVERSION.put(10, "minecraft:all_black"); -+ } -+ -+ private static final Map CAT_ADVANCEMENTS_CONVERSION = new HashMap<>( -+ ImmutableMap.builder() -+ .put("textures/entity/cat/tabby.png", "minecraft:tabby") -+ .put("textures/entity/cat/black.png", "minecraft:black") -+ .put("textures/entity/cat/red.png", "minecraft:red") -+ .put("textures/entity/cat/siamese.png", "minecraft:siamese") -+ .put("textures/entity/cat/british_shorthair.png", "minecraft:british") -+ .put("textures/entity/cat/calico.png", "minecraft:calico") -+ .put("textures/entity/cat/persian.png", "minecraft:persian") -+ .put("textures/entity/cat/ragdoll.png", "minecraft:ragdoll") -+ .put("textures/entity/cat/white.png", "minecraft:white") -+ .put("textures/entity/cat/jellie.png", "minecraft:jellie") -+ .put("textures/entity/cat/all_black.png", "minecraft:all_black") -+ .build() -+ ); -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:cat", new ConverterEntityToVariant(VERSION, "CatType", CAT_ID_CONVERSION::get)); -+ MCTypeRegistry.ADVANCEMENTS.addStructureConverter(new ConverterCriteriaRename(VERSION, "minecraft:husbandry/complete_catalogue", CAT_ADVANCEMENTS_CONVERSION::get)); -+ } -+ -+ private V3086() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3087.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3087.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b296229502491b54f6352ee1f9db0023296b36ec ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3087.java -@@ -0,0 +1,24 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterEntityToVariant; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -+ -+public final class V3087 { -+ -+ private static final int VERSION = MCVersions.V22W13A + 2; -+ -+ private static final Int2ObjectOpenHashMap FROG_ID_CONVERSION = new Int2ObjectOpenHashMap<>(); -+ static { -+ FROG_ID_CONVERSION.put(0, "minecraft:temperate"); -+ FROG_ID_CONVERSION.put(1, "minecraft:warm"); -+ FROG_ID_CONVERSION.put(2, "minecraft:cold"); -+ } -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:frog", new ConverterEntityToVariant(VERSION, "Variant", FROG_ID_CONVERSION::get)); -+ } -+ -+ private V3087() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3088.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3088.java -new file mode 100644 -index 0000000000000000000000000000000000000000..2752dfd1a7ff896e2ed736846980da5adde6e657 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3088.java -@@ -0,0 +1,25 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.chunk.ConverterAddBlendingData; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+ -+public final class V3088 { -+ -+ // this class originally targeted 3079 but was changed to target a later version without changing the converter, zero clue why -+ // this class then targeted 3088 but was changed to target 3441 -+ // to maintain integrity of the data version, I chose to extract the converter to a separate class and use it in both versions -+ // the reason it is important to never change old converters once released is that it creates _two_ versions under the same id. -+ // Consider the case where a user force upgrades their world, but does not load the chunk. Then, consider the case where -+ // the user does not force upgrade their world. Then, Mojang comes along and makes a decision like this and now both -+ // players load the chunk - they went through a different conversion process, which ultimately creates two versions. -+ // Unfortunately this fix doesn't exactly resolve it, as anyone running Mojang's converters will now be different -+ // from DataConverter's. It's broadly a dumb situation all around that could be avoided if Mojang wasn't being careless here. -+ private static final int VERSION = MCVersions.V22W14A; -+ -+ public static void register() { -+ MCTypeRegistry.CHUNK.addStructureConverter(new ConverterAddBlendingData(VERSION)); -+ } -+ -+ private V3088() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3090.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3090.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b1cfe038364e12d542d93c3108887173b2e05262 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3090.java -@@ -0,0 +1,25 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V3090 { -+ -+ private static final int VERSION = MCVersions.V22W15A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:painting", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ RenameHelper.renameSingle(data, "Motive", "variant"); -+ RenameHelper.renameSingle(data, "Facing", "facing"); -+ return null; -+ } -+ }); -+ } -+ -+ private V3090() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3093.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3093.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d0677d68b393da9b151c7b2add2fbbd8608e315f ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3093.java -@@ -0,0 +1,24 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V3093 { -+ -+ private static final int VERSION = MCVersions.V22W17A; -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:goat", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ data.setBoolean("HasLeftHorn", true); -+ data.setBoolean("HasRightHorn", true); -+ return null; -+ } -+ }); -+ } -+ -+ private V3093() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3094.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3094.java -new file mode 100644 -index 0000000000000000000000000000000000000000..9b9ef34db7dbae8574c4bb3d474592e7991441d5 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3094.java -@@ -0,0 +1,44 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V3094 { -+ -+ private static final int VERSION = MCVersions.V22W17A + 1; -+ -+ private static final String[] SOUND_VARIANT_TO_INSTRUMENT = new String[] { -+ "minecraft:ponder_goat_horn", -+ "minecraft:sing_goat_horn", -+ "minecraft:seek_goat_horn", -+ "minecraft:feel_goat_horn", -+ "minecraft:admire_goat_horn", -+ "minecraft:call_goat_horn", -+ "minecraft:yearn_goat_horn", -+ "minecraft:dream_goat_horn" -+ }; -+ -+ public static void register() { -+ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:goat_horn", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType tag = data.getMap("tag"); -+ -+ if (tag == null) { -+ return null; -+ } -+ -+ final int soundVariant = tag.getInt("SoundVariant"); -+ tag.remove("SoundVariant"); -+ -+ tag.setString("instrument", SOUND_VARIANT_TO_INSTRUMENT[soundVariant < 0 || soundVariant >= SOUND_VARIANT_TO_INSTRUMENT.length ? 0 : soundVariant]); -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V3094() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3097.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3097.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c70d6dc72d1d913904b71640fe3476c644449ee2 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3097.java -@@ -0,0 +1,63 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.advancements.ConverterCriteriaRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterEntityVariantRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.poi.ConverterPoiDelete; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import java.util.HashMap; -+import java.util.HashSet; -+import java.util.Map; -+import java.util.Set; -+ -+public final class V3097 { -+ -+ private static final int VERSION = MCVersions.V22W19A + 1; -+ -+ public static void register() { -+ final DataConverter, MapType> removeFilteredBookText = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType tag = data.getMap("tag"); -+ if (tag == null) { -+ return null; -+ } -+ -+ tag.remove("filtered_title"); -+ tag.remove("filtered_pages"); -+ -+ return null; -+ } -+ }; -+ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:writable_book", removeFilteredBookText); -+ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:written_book", removeFilteredBookText); -+ -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:sign", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ data.remove("FilteredText1"); -+ data.remove("FilteredText2"); -+ data.remove("FilteredText3"); -+ data.remove("FilteredText4"); -+ -+ return null; -+ } -+ }); -+ -+ final Map britishRenamer = new HashMap<>(Map.of( -+ "minecraft:british", "minecraft:british_shorthair" -+ )); -+ final Set poiRemove = new HashSet<>(Set.of( -+ "minecraft:unemployed", -+ "minecraft:nitwit" -+ )); -+ -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:cat", new ConverterEntityVariantRename(VERSION, britishRenamer::get)); -+ MCTypeRegistry.ADVANCEMENTS.addStructureConverter(new ConverterCriteriaRename(VERSION, "minecraft:husbandry/complete_catalogue", britishRenamer::get)); -+ MCTypeRegistry.POI_CHUNK.addStructureConverter(new ConverterPoiDelete(VERSION, poiRemove::contains)); -+ } -+ -+ private V3097() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3108.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3108.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0a1ef2e55a1f9cd6381a2c6fdc04f81ee190ba81 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3108.java -@@ -0,0 +1,29 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V3108 { -+ -+ private static final int VERSION = MCVersions.V1_19_1_PRE1 + 1; -+ -+ public static void register() { -+ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType context = data.getMap("__context"); -+ if ("minecraft:overworld".equals(context == null ? null : context.getString("dimension"))) { -+ return null; -+ } -+ -+ data.remove("blending_data"); -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V3108() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3201.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3201.java -new file mode 100644 -index 0000000000000000000000000000000000000000..04b84c8466d4fa8f2ad21aae2de44273c05495b6 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3201.java -@@ -0,0 +1,35 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V3201 { -+ -+ private static final int VERSION = MCVersions.V1_19_2 + 81; -+ -+ public static void register() { -+ MCTypeRegistry.OPTIONS.addStructureConverter(new DataConverter<>(VERSION) { -+ private static void fixList(final MapType data, final String target) { -+ if (data == null) { -+ return; -+ } -+ final String curr = data.getString(target); -+ if (curr == null) { -+ return; -+ } -+ data.setString(target, curr.replace("\"programer_art\"", "\"programmer_art\"")); -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ fixList(data, "resourcePacks"); -+ fixList(data, "incompatibleResourcePacks"); -+ return null; -+ } -+ }); -+ } -+ -+ private V3201() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3203.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3203.java -new file mode 100644 -index 0000000000000000000000000000000000000000..db9eb946638447445649f4576b3698c0774e44bb ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3203.java -@@ -0,0 +1,20 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+ -+public final class V3203 { -+ -+ private static final int VERSION = MCVersions.V1_19_2 + 83; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ registerMob("minecraft:camel"); -+ } -+ -+ private V3203() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3204.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3204.java -new file mode 100644 -index 0000000000000000000000000000000000000000..87053c0c1de258770e7630830307ab484915aad8 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3204.java -@@ -0,0 +1,16 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+ -+public final class V3204 { -+ -+ private static final int VERSION = MCVersions.V1_19_2 + 84; -+ -+ public static void register() { -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:chiseled_bookshelf", new DataWalkerItemLists("Items")); -+ } -+ -+ private V3204() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3209.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3209.java -new file mode 100644 -index 0000000000000000000000000000000000000000..85270a36c5f75b1c6be49e461b302c3339c95750 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3209.java -@@ -0,0 +1,18 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemstack.ConverterFlattenSpawnEgg; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+ -+public final class V3209 { -+ -+ private static final int VERSION = MCVersions.V22W45A + 1; -+ -+ public static void register() { -+ // Note: This converter reads entity id from its sub data, but we need no breakpoint because entity ids are not -+ // remapped this version -+ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:pig_spawn_egg", new ConverterFlattenSpawnEgg(VERSION, 0)); -+ } -+ -+ private V3209() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3214.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3214.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0096664e25dca8d690c6154324f97efdb1ada723 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3214.java -@@ -0,0 +1,30 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V3214 { -+ -+ private static final int VERSION = MCVersions.V1_19_3_PRE3 + 1; -+ -+ public static void register() { -+ MCTypeRegistry.OPTIONS.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final String value = data.getString("ao"); -+ -+ if ("0".equals(value)) { -+ data.setString("ao", "false"); -+ } else if ("1".equals(value) || "2".equals(value)) { -+ data.setString("ao", "true"); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V3214() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3319.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3319.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0a4a1f690f568b8977e9b2caaf7fba15cb3307a5 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3319.java -@@ -0,0 +1,23 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V3319 { -+ -+ private static final int VERSION = MCVersions.V1_19_3 + 101; -+ -+ public static void register() { -+ MCTypeRegistry.OPTIONS.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ data.setBoolean("onboardAccessibility", false); -+ return null; -+ } -+ }); -+ } -+ -+ private V3319() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3322.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3322.java -new file mode 100644 -index 0000000000000000000000000000000000000000..53827b9b8999e7b284f3df0f41c98dbbc7d69c1c ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3322.java -@@ -0,0 +1,84 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+ -+import java.util.HashSet; -+import java.util.Set; -+ -+public final class V3322 { -+ -+ private static final int VERSION = MCVersions.V23W04A + 1; -+ -+ private static final Set EFFECT_ITEM_TYPES = new HashSet<>( -+ Set.of( -+ "minecraft:potion", -+ "minecraft:splash_potion", -+ "minecraft:lingering_potion", -+ "minecraft:tipped_arrow" -+ ) -+ ); -+ -+ private static void updateEffectList(final MapType root, final String path) { -+ if (root == null) { -+ return; -+ } -+ -+ final ListType effects = root.getList(path, ObjectType.MAP); -+ -+ if (effects == null) { -+ return; -+ } -+ -+ for (int i = 0, len = effects.size(); i < len; ++i) { -+ final MapType data = effects.getMap(i); -+ final MapType factorData = data.getMap("FactorCalculationData"); -+ if (factorData == null) { -+ continue; -+ } -+ -+ final int timestamp = factorData.getInt("effect_changed_timestamp", -1); -+ factorData.remove("effect_changed_timestamp"); -+ -+ final int duration = data.getInt("Duration", -1); -+ -+ final int ticksActive = timestamp - duration; -+ factorData.setInt("ticks_active", ticksActive); -+ } -+ } -+ -+ public static void register() { -+ final DataConverter, MapType> entityEffectFix = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ updateEffectList(data, "Effects"); -+ updateEffectList(data, "ActiveEffects"); -+ updateEffectList(data, "CustomPotionEffects"); -+ return null; -+ } -+ }; -+ -+ MCTypeRegistry.PLAYER.addStructureConverter(entityEffectFix); -+ MCTypeRegistry.ENTITY.addStructureConverter(entityEffectFix); -+ -+ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final String id = data.getString("id"); -+ if (!EFFECT_ITEM_TYPES.contains(id)) { -+ return null; -+ } -+ -+ updateEffectList(data.getMap("tag"), "CustomPotionEffects"); -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V3322() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3325.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3325.java -new file mode 100644 -index 0000000000000000000000000000000000000000..2120a5928446f2597fb261d5d6e91c3c9700cb22 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3325.java -@@ -0,0 +1,19 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; -+ -+public final class V3325 { -+ -+ private static final int VERSION = MCVersions.V23W05A + 2; -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:item_display", new DataWalkerItems("item")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:block_display", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "block_state")); -+ // text_display is a simple entity -+ } -+ -+ private V3325() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3326.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3326.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6a8d3f6fd18d941e5b0b18fc5208b7fe1f9fd724 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3326.java -@@ -0,0 +1,20 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+ -+public final class V3326 { -+ -+ private static final int VERSION = MCVersions.V23W06A; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ registerMob("minecraft:sniffer"); -+ } -+ -+ private V3326() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3327.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3327.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7051d4f01b6f43f3d435d21d65b83ba702ffda41 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3327.java -@@ -0,0 +1,19 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerListPaths; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; -+ -+public final class V3327 { -+ -+ private static final int VERSION = MCVersions.V23W06A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:decorated_pot", new DataWalkerListPaths<>(MCTypeRegistry.ITEM_NAME, "shards")); -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:decorated_pot", new DataWalkerItems("item")); -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:suspicious_sand", new DataWalkerItems("item")); -+ } -+ -+ private V3327() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3328.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3328.java -new file mode 100644 -index 0000000000000000000000000000000000000000..75a3cbc8e6749abd4bceff710d2f7c3ca6df9d70 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3328.java -@@ -0,0 +1,15 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+ -+public final class V3328 { -+ -+ private static final int VERSION = MCVersions.V23W06A + 2; -+ -+ public static void register() { -+ // registers simple entity "minecraft:interaction" -+ } -+ -+ private V3328() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3438.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3438.java -new file mode 100644 -index 0000000000000000000000000000000000000000..30c23572a5989f0bd6bff6e424ee59c84bc8d8f1 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3438.java -@@ -0,0 +1,47 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.tileentity.ConverterAbstractTileEntityRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V3438 { -+ -+ public static final int VERSION = MCVersions.V1_19_4 + 101; -+ -+ public static void register() { -+ // brushable block rename -+ MCTypeRegistry.TILE_ENTITY.copyWalkers(VERSION, "minecraft:suspicious_sand", "minecraft:brushable_block"); -+ -+ ConverterAbstractTileEntityRename.register(VERSION, new HashMap<>(Map.of( -+ "minecraft:suspicious_sand", "minecraft:brushable_block" -+ ))::get); -+ -+ -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:brushable_block", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ RenameHelper.renameSingle(data, "loot_table", "LootTable"); -+ RenameHelper.renameSingle(data, "loot_table_seed", "LootTableSeed"); -+ return null; -+ } -+ }); -+ -+ ConverterAbstractItemRename.register(VERSION, new HashMap<>( -+ Map.of( -+ "minecraft:pottery_shard_archer", "minecraft:archer_pottery_shard", -+ "minecraft:pottery_shard_prize", "minecraft:prize_pottery_shard", -+ "minecraft:pottery_shard_arms_up", "minecraft:arms_up_pottery_shard", -+ "minecraft:pottery_shard_skull", "minecraft:skull_pottery_shard" -+ ) -+ )::get); -+ } -+ -+ private V3438() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3439.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3439.java -new file mode 100644 -index 0000000000000000000000000000000000000000..5c09f745e5200393cf4ecdcb5b42466c2e2d94d9 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3439.java -@@ -0,0 +1,96 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.util.ComponentUtils; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V3439 { -+ -+ private static final int VERSION = MCVersions.V1_19_4 + 102; -+ -+ public static void register() { -+ final DataConverter, MapType> signTileUpdater = new DataConverter<>(VERSION) { -+ private static final String DEFAULT_COLOR = "black"; -+ -+ private static ListType migrateToList(final MapType root, final String prefix) { -+ if (root == null) { -+ return null; -+ } -+ -+ final ListType ret = root.getTypeUtil().createEmptyList(); -+ -+ ret.addString(root.getString(prefix.concat("1"), ComponentUtils.EMPTY)); -+ ret.addString(root.getString(prefix.concat("2"), ComponentUtils.EMPTY)); -+ ret.addString(root.getString(prefix.concat("3"), ComponentUtils.EMPTY)); -+ ret.addString(root.getString(prefix.concat("4"), ComponentUtils.EMPTY)); -+ -+ return ret; -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ // front text -+ final MapType frontText = data.getTypeUtil().createEmptyMap(); -+ data.setMap("front_text", frontText); -+ -+ final ListType frontMessages = migrateToList(data, "Text"); -+ frontText.setList("messages", frontMessages); -+ -+ ListType frontFilteredMessages = null; -+ -+ for (int i = 0; i < 4; ++i) { -+ final String filtered = data.getString("FilteredText" + i); -+ if (filtered == null) { -+ if (frontFilteredMessages != null) { -+ frontFilteredMessages.addString(frontMessages.getString(i)); -+ } -+ continue; -+ } -+ -+ if (frontFilteredMessages == null) { -+ frontFilteredMessages = data.getTypeUtil().createEmptyList(); -+ for (int k = 0; k < i; ++k) { -+ frontFilteredMessages.addString(frontMessages.getString(k)); -+ } -+ } -+ -+ frontFilteredMessages.addString(filtered); -+ } -+ -+ if (frontFilteredMessages != null) { -+ frontText.setList("filtered_messages", frontFilteredMessages); -+ } -+ -+ frontText.setString("color", data.getString("Color", DEFAULT_COLOR)); -+ frontText.setBoolean("has_glowing_text", data.getBoolean("GlowingText", false)); -+ frontText.setBoolean("_filtered_correct", true); -+ -+ // back text -+ final MapType backText = data.getTypeUtil().createEmptyMap(); -+ data.setMap("back_text", backText); -+ -+ final ListType blankMessages = data.getTypeUtil().createEmptyList(); -+ backText.setList("messages", blankMessages); -+ -+ for (int i = 0; i < 4; ++i) { -+ blankMessages.addString(ComponentUtils.EMPTY); -+ } -+ -+ backText.setString("color", DEFAULT_COLOR); -+ backText.setBoolean("has_glowing_text", false); -+ -+ // misc -+ data.setBoolean("is_waxed", false); -+ return null; -+ } -+ }; -+ -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:sign", signTileUpdater); -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:hanging_sign", signTileUpdater); -+ } -+ -+ private V3439() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3440.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3440.java -new file mode 100644 -index 0000000000000000000000000000000000000000..914df7885582a1fde398e755dbfaf00e19ce16b3 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3440.java -@@ -0,0 +1,29 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.ConverterAbstractStringValueTypeRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.leveldat.ConverterRemoveFeatureFlag; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.util.NamespaceUtil; -+ -+import java.util.Arrays; -+import java.util.HashSet; -+ -+public final class V3440 { -+ -+ private static final int VERSION = MCVersions.V1_19_4 + 103; -+ -+ public static void register() { -+ // Note: MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST is namespaced string -+ ConverterAbstractStringValueTypeRename.register(VERSION, MCTypeRegistry.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST, (final String in) -> { -+ return "minecraft:overworld_update_1_20".equals(NamespaceUtil.correctNamespace(in)) ? "minecraft:overworld" : null; -+ }); -+ MCTypeRegistry.LEVEL.addStructureConverter(new ConverterRemoveFeatureFlag(VERSION, new HashSet<>( -+ Arrays.asList( -+ "minecraft:update_1_20" -+ ) -+ ))); -+ } -+ -+ private V3440() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3441.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3441.java -new file mode 100644 -index 0000000000000000000000000000000000000000..2cf41561b4a229ba4d60540f85ba0a8946bb9753 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3441.java -@@ -0,0 +1,17 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.chunk.ConverterAddBlendingData; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+ -+public final class V3441 { -+ -+ private static final int VERSION = MCVersions.V1_19_4 + 104; -+ -+ public static void register() { -+ // See V3088 for why this converter is duplicated here and in V3088 -+ MCTypeRegistry.CHUNK.addStructureConverter(new ConverterAddBlendingData(VERSION)); -+ } -+ -+ private V3441() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3447.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3447.java -new file mode 100644 -index 0000000000000000000000000000000000000000..5db4a5222bf80f01c128d97ec849b89003837beb ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3447.java -@@ -0,0 +1,49 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V3447 { -+ -+ private static final int VERSION = MCVersions.V23W14A + 2; -+ -+ public static void register() { -+ final String[] targets = new String[] { -+ "minecraft:angler_pottery_shard", -+ "minecraft:archer_pottery_shard", -+ "minecraft:arms_up_pottery_shard", -+ "minecraft:blade_pottery_shard", -+ "minecraft:brewer_pottery_shard", -+ "minecraft:burn_pottery_shard", -+ "minecraft:danger_pottery_shard", -+ "minecraft:explorer_pottery_shard", -+ "minecraft:friend_pottery_shard", -+ "minecraft:heart_pottery_shard", -+ "minecraft:heartbreak_pottery_shard", -+ "minecraft:howl_pottery_shard", -+ "minecraft:miner_pottery_shard", -+ "minecraft:mourner_pottery_shard", -+ "minecraft:plenty_pottery_shard", -+ "minecraft:prize_pottery_shard", -+ "minecraft:sheaf_pottery_shard", -+ "minecraft:shelter_pottery_shard", -+ "minecraft:skull_pottery_shard", -+ "minecraft:snort_pottery_shard" -+ }; -+ // shard->sherd -+ final Map rename = new HashMap<>(targets.length); -+ -+ for (final String target : targets) { -+ final String replace = target.replace("_pottery_shard", "_pottery_sherd"); -+ if (rename.put(target, replace) != null) { -+ throw new IllegalArgumentException("Duplicate target " + target); -+ } -+ } -+ -+ ConverterAbstractItemRename.register(VERSION, rename::get); -+ } -+ -+ private V3447() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3448.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3448.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6f447d59677be4630b53e948419a0ae2a3414315 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3448.java -@@ -0,0 +1,28 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerListPaths; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V3448 { -+ -+ private static final int VERSION = MCVersions.V23W14A + 3; -+ -+ public static void register() { -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:decorated_pot", new DataWalkerListPaths<>(MCTypeRegistry.ITEM_NAME, "sherds")); -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:decorated_pot", new DataWalkerItems("item")); -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:decorated_pot", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ RenameHelper.renameSingle(data, "shards", "sherds"); -+ return null; -+ } -+ }); -+ } -+ -+ private V3448() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3450.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3450.java -new file mode 100644 -index 0000000000000000000000000000000000000000..9e7f34a40280a7704c4a4d1c03a7dda8e030aff5 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3450.java -@@ -0,0 +1,23 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.chunk.ConverterRenameStatus; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V3450 { -+ -+ private static final int VERSION = MCVersions.V23W16A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.CHUNK.addStructureConverter(new ConverterRenameStatus(VERSION, new HashMap<>( -+ Map.of( -+ "minecraft:liquid_carvers", "minecraft:carvers", -+ "minecraft:heightmaps", "minecraft:spawn" -+ ) -+ )::get)); -+ } -+ -+ private V3450() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3451.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3451.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ea97d596bfb4b9c6b9b7d0534604e042a775ea78 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3451.java -@@ -0,0 +1,38 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+ -+public final class V3451 { -+ -+ private static final int VERSION = MCVersions.V23W16A + 2; -+ -+ public static void register() { -+ MCTypeRegistry.CHUNK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ data.remove("isLightOn"); -+ -+ final ListType sections = data.getList("sections", ObjectType.MAP); -+ if (sections == null) { -+ return null; -+ } -+ -+ for (int i = 0, len = sections.size(); i < len; ++i) { -+ final MapType section = sections.getMap(i); -+ -+ section.remove("BlockLight"); -+ section.remove("SkyLight"); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V3451() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3459.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3459.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e8a1fcd9e67b151a360e11089289154a14dde27c ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3459.java -@@ -0,0 +1,38 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V3459 { -+ -+ private static final int VERSION = MCVersions.V1_20_PRE5 + 1; -+ -+ public static void register() { -+ MCTypeRegistry.LEVEL.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (data.hasKey("DragonFight")) { -+ return null; -+ } -+ -+ final MapType dimensionData = data.getMap("DimensionData"); -+ if (dimensionData == null) { -+ return null; -+ } -+ -+ final MapType endData = dimensionData.getMap("1"); -+ if (endData != null) { -+ final MapType dragonFight = endData.getMap("DragonFight", endData.getTypeUtil().createEmptyMap()).copy(); -+ V3807.flattenBlockPos(dragonFight, "ExitPortalLocation"); -+ data.setMap("DragonFight", dragonFight); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V3459() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3564.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3564.java -new file mode 100644 -index 0000000000000000000000000000000000000000..5c64ec5b9bdcc279bc1b86e6bb0b877003b213cb ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3564.java -@@ -0,0 +1,93 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.util.ComponentUtils; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+ -+public final class V3564 { -+ -+ private static final int VERSION = MCVersions.V1_20_1 + 99; -+ -+ public static void register() { -+ final DataConverter, MapType> converter = new DataConverter<>(VERSION) { -+ -+ private static final String[] LEGACY_FIELDS = new String[] { -+ "Text1", -+ "Text2", -+ "Text3", -+ "Text4", -+ -+ "FilteredText1", -+ "FilteredText2", -+ "FilteredText3", -+ "FilteredText4", -+ -+ "Color", -+ -+ "GlowingText" -+ }; -+ -+ -+ private static void updateText(final MapType text) { -+ if (text == null) { -+ return; -+ } -+ -+ if (text.getBoolean("_filtered_correct", false)) { -+ text.remove("_filtered_correct"); -+ return; -+ } -+ -+ final ListType filteredMessages = text.getList("filtered_messages", ObjectType.STRING); -+ -+ if (filteredMessages == null || filteredMessages.size() == 0) { -+ return; -+ } -+ -+ // should treat null here as empty list -+ final ListType messages = text.getList("messages", ObjectType.STRING); -+ -+ final ListType newFilteredList = filteredMessages.getTypeUtil().createEmptyList(); -+ boolean newFilteredIsEmpty = true; -+ -+ for (int i = 0, len = filteredMessages.size(); i < len; ++i) { -+ final String filtered = filteredMessages.getString(i); -+ final String message = messages != null && i < messages.size() ? messages.getString(i) : ComponentUtils.EMPTY; -+ -+ final String newFiltered = ComponentUtils.EMPTY.equals(filtered) ? message : filtered; -+ -+ newFilteredList.addString(newFiltered); -+ -+ newFilteredIsEmpty = newFilteredIsEmpty && ComponentUtils.EMPTY.equals(newFiltered); -+ } -+ -+ if (newFilteredIsEmpty) { -+ text.remove("filtered_messages"); -+ } else { -+ text.setList("filtered_messages", newFilteredList); -+ } -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ updateText(data.getMap("front_text")); -+ updateText(data.getMap("back_text")); -+ -+ for (final String toRemove : LEGACY_FIELDS) { -+ data.remove(toRemove); -+ } -+ -+ return null; -+ } -+ }; -+ -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:sign", converter); -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:hanging_sign", converter); -+ } -+ -+ private V3564() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3565.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3565.java -new file mode 100644 -index 0000000000000000000000000000000000000000..685021e87236c5b7ce5ee0b5422d7bf856ea6652 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3565.java -@@ -0,0 +1,32 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V3565 { -+ -+ private static final int VERSION = MCVersions.V1_20_1 + 100; -+ -+ public static void register() { -+ MCTypeRegistry.SAVED_DATA_RANDOM_SEQUENCES.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType root, final long sourceVersion, final long toVersion) { -+ final MapType oldData = root.getMap("data"); -+ if (oldData == null) { -+ return null; -+ } -+ -+ final MapType newData = root.getTypeUtil().createEmptyMap(); -+ root.setMap("data", newData); -+ -+ newData.setMap("sequences", oldData); -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V3565() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3566.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3566.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a5b91dc5eb4e9206cbb4489ce14195d7517ef4e0 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3566.java -@@ -0,0 +1,58 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V3566 { -+ -+ private static final int VERSION = MCVersions.V1_20_1 + 101; -+ -+ public static void register() { -+ MCTypeRegistry.SAVED_DATA_SCOREBOARD.addStructureConverter(new DataConverter<>(VERSION) { -+ -+ private static final Map SLOT_RENAMES = new HashMap<>( -+ ImmutableMap.builder() -+ .put("slot_0", "list") -+ .put("slot_1", "sidebar") -+ .put("slot_2", "below_name") -+ .put("slot_3", "sidebar.team.black") -+ .put("slot_4", "sidebar.team.dark_blue") -+ .put("slot_5", "sidebar.team.dark_green") -+ .put("slot_6", "sidebar.team.dark_aqua") -+ .put("slot_7", "sidebar.team.dark_red") -+ .put("slot_8", "sidebar.team.dark_purple") -+ .put("slot_9", "sidebar.team.gold") -+ .put("slot_10", "sidebar.team.gray") -+ .put("slot_11", "sidebar.team.dark_gray") -+ .put("slot_12", "sidebar.team.blue") -+ .put("slot_13", "sidebar.team.green") -+ .put("slot_14", "sidebar.team.aqua") -+ .put("slot_15", "sidebar.team.red") -+ .put("slot_16", "sidebar.team.light_purple") -+ .put("slot_17", "sidebar.team.yellow") -+ .put("slot_18", "sidebar.team.white") -+ .build() -+ ); -+ -+ @Override -+ public MapType convert(final MapType root, final long sourceVersion, final long toVersion) { -+ final MapType data = root.getMap("data"); -+ if (data == null) { -+ return null; -+ } -+ -+ RenameHelper.renameKeys(data.getMap("DisplaySlots"), SLOT_RENAMES::get); -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V3566() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3568.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3568.java -new file mode 100644 -index 0000000000000000000000000000000000000000..39b4bd2d0bb36ef242467e89010ef5fc1490de9b ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3568.java -@@ -0,0 +1,245 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import java.util.HashMap; -+import java.util.HashSet; -+import java.util.Map; -+import java.util.Set; -+ -+public final class V3568 { -+ -+ private static final int VERSION = MCVersions.V23W31A + 1; -+ -+ private static final String[] EFFECT_ID_MAP = new String[34]; -+ static { -+ EFFECT_ID_MAP[1] = "minecraft:speed"; -+ EFFECT_ID_MAP[2] = "minecraft:slowness"; -+ EFFECT_ID_MAP[3] = "minecraft:haste"; -+ EFFECT_ID_MAP[4] = "minecraft:mining_fatigue"; -+ EFFECT_ID_MAP[5] = "minecraft:strength"; -+ EFFECT_ID_MAP[6] = "minecraft:instant_health"; -+ EFFECT_ID_MAP[7] = "minecraft:instant_damage"; -+ EFFECT_ID_MAP[8] = "minecraft:jump_boost"; -+ EFFECT_ID_MAP[9] = "minecraft:nausea"; -+ EFFECT_ID_MAP[10] = "minecraft:regeneration"; -+ EFFECT_ID_MAP[11] = "minecraft:resistance"; -+ EFFECT_ID_MAP[12] = "minecraft:fire_resistance"; -+ EFFECT_ID_MAP[13] = "minecraft:water_breathing"; -+ EFFECT_ID_MAP[14] = "minecraft:invisibility"; -+ EFFECT_ID_MAP[15] = "minecraft:blindness"; -+ EFFECT_ID_MAP[16] = "minecraft:night_vision"; -+ EFFECT_ID_MAP[17] = "minecraft:hunger"; -+ EFFECT_ID_MAP[18] = "minecraft:weakness"; -+ EFFECT_ID_MAP[19] = "minecraft:poison"; -+ EFFECT_ID_MAP[20] = "minecraft:wither"; -+ EFFECT_ID_MAP[21] = "minecraft:health_boost"; -+ EFFECT_ID_MAP[22] = "minecraft:absorption"; -+ EFFECT_ID_MAP[23] = "minecraft:saturation"; -+ EFFECT_ID_MAP[24] = "minecraft:glowing"; -+ EFFECT_ID_MAP[25] = "minecraft:levitation"; -+ EFFECT_ID_MAP[26] = "minecraft:luck"; -+ EFFECT_ID_MAP[27] = "minecraft:unluck"; -+ EFFECT_ID_MAP[28] = "minecraft:slow_falling"; -+ EFFECT_ID_MAP[29] = "minecraft:conduit_power"; -+ EFFECT_ID_MAP[30] = "minecraft:dolphins_grace"; -+ EFFECT_ID_MAP[31] = "minecraft:bad_omen"; -+ EFFECT_ID_MAP[32] = "minecraft:hero_of_the_village"; -+ EFFECT_ID_MAP[33] = "minecraft:darkness"; -+ } -+ private static final Set EFFECT_ITEMS = -+ new HashSet<>( -+ Set.of( -+ "minecraft:potion", -+ "minecraft:splash_potion", -+ "minecraft:lingering_potion", -+ "minecraft:tipped_arrow" -+ ) -+ ); -+ -+ private static String readLegacyEffect(final MapType data, final String path) { -+ final Number id = data.getNumber(path); -+ if (id == null) { -+ return null; -+ } -+ -+ final int castedId = id.intValue(); -+ return castedId >= 0 && castedId < EFFECT_ID_MAP.length ? EFFECT_ID_MAP[castedId] : null; -+ } -+ -+ private static void convertLegacyEffect(final MapType data, final String legacyPath, final String newPath) { -+ final Number id = data.getNumber(legacyPath); -+ data.remove(legacyPath); -+ -+ if (id == null) { -+ return; -+ } -+ -+ final int castedId = id.intValue(); -+ final String newId = castedId >= 0 && castedId < EFFECT_ID_MAP.length ? EFFECT_ID_MAP[castedId] : null; -+ -+ if (newId == null) { -+ return; -+ } -+ -+ data.setString(newPath, newId); -+ } -+ -+ private static final Map MOB_EFFECT_RENAMES = new HashMap<>(); -+ static { -+ MOB_EFFECT_RENAMES.put("Ambient", "ambient"); -+ MOB_EFFECT_RENAMES.put("Amplifier", "amplifier"); -+ MOB_EFFECT_RENAMES.put("Duration", "duration"); -+ MOB_EFFECT_RENAMES.put("ShowParticles", "show_particles"); -+ MOB_EFFECT_RENAMES.put("ShowIcon", "show_icon"); -+ MOB_EFFECT_RENAMES.put("FactorCalculationData", "factor_calculation_data"); -+ MOB_EFFECT_RENAMES.put("HiddenEffect", "hidden_effect"); -+ } -+ -+ private static void convertMobEffect(final MapType mobEffect) { -+ if (mobEffect == null) { -+ return; -+ } -+ -+ convertLegacyEffect(mobEffect, "Id", "id"); -+ -+ for (final Map.Entry rename : MOB_EFFECT_RENAMES.entrySet()) { -+ RenameHelper.renameSingle(mobEffect, rename.getKey(), rename.getValue()); -+ } -+ -+ convertMobEffect(mobEffect.getMap("hidden_effect")); -+ } -+ -+ private static void convertMobEffectList(final MapType data, final String oldPath, final String newPath) { -+ final ListType effects = data.getList(oldPath, ObjectType.MAP); -+ if (effects == null) { -+ return; -+ } -+ -+ for (int i = 0, len = effects.size(); i < len; ++i) { -+ convertMobEffect(effects.getMap(i)); -+ } -+ -+ data.remove(oldPath); -+ data.setList(newPath, effects); -+ } -+ -+ private static void removeAndSet(final MapType data, final String toRemovePath, -+ final String toSetPath, final Object toSet) { -+ data.remove(toRemovePath); -+ if (toSet != null) { -+ data.setGeneric(toSetPath, toSet); -+ } -+ } -+ -+ private static void updateSuspiciousStew(final MapType from, final MapType into) { -+ removeAndSet(into, "EffectId", "id", readLegacyEffect(from, "EffectId")); -+ removeAndSet(into, "EffectDuration", "duration", from.getGeneric("EffectDuration")); -+ } -+ -+ public static void register() { -+ final DataConverter, MapType> beaconConverter = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ convertLegacyEffect(data, "Primary", "primary_effect"); -+ convertLegacyEffect(data, "Secondary", "secondary_effect"); -+ -+ return null; -+ } -+ }; -+ -+ final DataConverter, MapType> mooshroomConverter = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType newEffect = data.getTypeUtil().createEmptyMap(); -+ updateSuspiciousStew(data, newEffect); -+ -+ data.remove("EffectId"); -+ data.remove("EffectDuration"); -+ -+ if (!newEffect.isEmpty()) { -+ final ListType stewEffects = data.getTypeUtil().createEmptyList(); -+ data.setList("stew_effects", stewEffects); -+ -+ stewEffects.addMap(newEffect); -+ } -+ -+ return null; -+ } -+ }; -+ final DataConverter, MapType> arrowConverter = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ convertMobEffectList(data, "CustomPotionEffects", "custom_potion_effects"); -+ return null; -+ } -+ }; -+ final DataConverter, MapType> areaEffectCloudConverter = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ convertMobEffectList(data, "Effects", "effects"); -+ return null; -+ } -+ }; -+ final DataConverter, MapType> livingEntityConverter = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ convertMobEffectList(data, "ActiveEffects", "active_effects"); -+ return null; -+ } -+ }; -+ -+ final DataConverter, MapType> itemConverter = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType root, final long sourceVersion, final long toVersion) { -+ final String id = root.getString("id"); -+ -+ final MapType tag = root.getMap("tag"); -+ -+ if (tag == null) { -+ return null; -+ } -+ -+ if ("minecraft:suspicious_stew".equals(id)) { -+ RenameHelper.renameSingle(tag, "Effects", "effects"); -+ -+ final ListType effects = tag.getList("effects", ObjectType.MAP); -+ -+ if (effects != null) { -+ for (int i = 0, len = effects.size(); i < len; ++i) { -+ final MapType effect = effects.getMap(i); -+ updateSuspiciousStew(effect, effect); -+ } -+ } -+ -+ return null; -+ } -+ -+ if (EFFECT_ITEMS.contains(id)) { -+ convertMobEffectList(tag, "CustomPotionEffects", "custom_potion_effects"); -+ return null; -+ } -+ -+ return null; -+ } -+ }; -+ -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:beacon", beaconConverter); -+ -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:mooshroom", mooshroomConverter); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:arrow", arrowConverter); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:area_effect_cloud", areaEffectCloudConverter); -+ MCTypeRegistry.ENTITY.addStructureConverter(livingEntityConverter); -+ -+ MCTypeRegistry.PLAYER.addStructureConverter(livingEntityConverter); -+ -+ MCTypeRegistry.ITEM_STACK.addStructureConverter(itemConverter); -+ } -+ -+ private V3568() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3682.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3682.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f9e3eb71b268f7bc1940f0199fddfa1ac27401b8 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3682.java -@@ -0,0 +1,16 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+ -+public final class V3682 { -+ -+ private static final int VERSION = MCVersions.V23W41A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:crafter", new DataWalkerItemLists("Items")); -+ } -+ -+ private V3682() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3683.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3683.java -new file mode 100644 -index 0000000000000000000000000000000000000000..855a95b4ef686fa9d2cefdef5664290528e4dc60 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3683.java -@@ -0,0 +1,33 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V3683 { -+ -+ private static final int VERSION = MCVersions.V23W41A + 2; -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:tnt", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ RenameHelper.renameSingle(data, "Fuse", "fuse"); -+ -+ final MapType defaultState = data.getTypeUtil().createEmptyMap(); -+ data.setMap("block_state", defaultState); -+ -+ defaultState.setString("Name", "minecraft:tnt"); -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:tnt", new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "block_state")); -+ } -+ -+ private V3683() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3685.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3685.java -new file mode 100644 -index 0000000000000000000000000000000000000000..603467a7a2b6da93181a0a32eedb30d1614b0069 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3685.java -@@ -0,0 +1,64 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.TypeUtil; -+ -+public final class V3685 { -+ -+ private static final int VERSION = MCVersions.V23W42A + 1; -+ -+ private static String getType(final MapType arrow) { -+ return "minecraft:empty".equals(arrow.getString("Potion", "minecraft:empty")) ? "minecraft:arrow" : "minecraft:tipped_arrow"; -+ } -+ -+ private static MapType createItem(final TypeUtil util, final String id, final int count) { -+ final MapType ret = util.createEmptyMap(); -+ -+ ret.setString("id", id); -+ ret.setInt("Count", count); -+ -+ return ret; -+ } -+ -+ private static void registerArrowEntity(final String id) { -+ MCTypeRegistry.ENTITY.addWalker(VERSION, id, new DataWalkerTypePaths<>(MCTypeRegistry.BLOCK_STATE, "inBlockState")); -+ // new: item -+ MCTypeRegistry.ENTITY.addWalker(VERSION, id, new DataWalkerItems("item")); -+ } -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:trident", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ RenameHelper.renameSingle(data, "Trident", "item"); -+ return null; -+ } -+ }); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:arrow", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ data.setMap("item", createItem(data.getTypeUtil(), getType(data), 1)); -+ return null; -+ } -+ }); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:spectral_arrow", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ data.setMap("item", createItem(data.getTypeUtil(), "minecraft:spectral_arrow", 1)); -+ return null; -+ } -+ }); -+ -+ registerArrowEntity("minecraft:trident"); -+ registerArrowEntity("minecraft:spectral_arrow"); -+ registerArrowEntity("minecraft:arrow"); -+ } -+ -+ private V3685() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3689.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3689.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ccda8d0f7c0a284fd4f91622dc33b832b4a95c45 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3689.java -@@ -0,0 +1,37 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+ -+public final class V3689 { -+ -+ private static final int VERSION = MCVersions.V23W44A + 1; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ registerMob("minecraft:breeze"); -+ // minecraft:wind_charge is a simple entity -+ // minecraft:breeze_wind_charge is a simple entity -+ -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:trial_spawner", (final MapType data, final long fromVersion, final long toVersion) -> { -+ final ListType spawnPotentials = data.getList("spawn_potentials", ObjectType.MAP); -+ if (spawnPotentials != null) { -+ for (int i = 0, len = spawnPotentials.size(); i < len; ++i) { -+ WalkerUtils.convert(MCTypeRegistry.ENTITY, spawnPotentials.getMap(i).getMap("data"), "entity", fromVersion, toVersion); -+ } -+ } -+ -+ WalkerUtils.convert(MCTypeRegistry.ENTITY, data.getMap("spawn_data"), "entity", fromVersion, toVersion); -+ return null; -+ }); -+ } -+ -+ private V3689() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3692.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3692.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1fc62f9cadb990790420376d5c80b14775b71a48 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3692.java -@@ -0,0 +1,25 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.blockname.ConverterAbstractBlockRename; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V3692 { -+ -+ private static final int VERSION = MCVersions.V23W46A + 1; -+ -+ private static final Map GRASS_RENAME = new HashMap<>( -+ Map.of( -+ "minecraft:grass", "minecraft:short_grass" -+ ) -+ ); -+ -+ public static void register() { -+ ConverterAbstractBlockRename.register(VERSION, GRASS_RENAME::get); -+ ConverterAbstractItemRename.register(VERSION, GRASS_RENAME::get); -+ } -+ -+ private V3692() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3799.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3799.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0c34d445825e8b49249af852b1f2f09c8b5a2574 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3799.java -@@ -0,0 +1,14 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+ -+public final class V3799 { -+ -+ private static final int VERSION = MCVersions.V1_20_4 + 99; -+ -+ public static void register() { -+ V100.registerEquipment(VERSION, "minecraft:armadillo"); -+ } -+ -+ private V3799() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3800.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3800.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a40397feb5962bd5f4a44cc85bb359f5f99ff03a ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3800.java -@@ -0,0 +1,23 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V3800 { -+ -+ private static final int VERSION = MCVersions.V1_20_4 + 100; -+ -+ public static void register() { -+ final Map renames = new HashMap<>( -+ Map.of( -+ "minecraft:scute", "minecraft:turtle_scute" -+ ) -+ ); -+ -+ ConverterAbstractItemRename.register(VERSION, renames::get); -+ } -+ -+ private V3800() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3803.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3803.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7ff5e2f1a386d75b6d0d6fc3160f2241bf74b262 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3803.java -@@ -0,0 +1,24 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemstack.ConverterEnchantmentsRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V3803 { -+ -+ private static final int VERSION = MCVersions.V23W51B + 1; -+ -+ public static void register() { -+ final Map renames = new HashMap<>( -+ Map.of( -+ "minecraft:sweeping", "minecraft:sweeping_edge" -+ ) -+ ); -+ -+ MCTypeRegistry.ITEM_STACK.addStructureConverter(new ConverterEnchantmentsRename(VERSION, renames::get)); -+ } -+ -+ private V3803() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3807.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3807.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a76916cdb7cf91b8ba5461524472b3e455f02885 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3807.java -@@ -0,0 +1,72 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+ -+public final class V3807 { -+ -+ private static final int VERSION = MCVersions.V24W04A + 1; -+ -+ public static void flattenBlockPos(final MapType data, final String path) { -+ if (data == null) { -+ return; -+ } -+ -+ final MapType pos = data.getMap(path); -+ if (pos == null) { -+ return; -+ } -+ -+ final Number x = pos.getNumber("X"); -+ final Number y = pos.getNumber("Y"); -+ final Number z = pos.getNumber("Z"); -+ -+ if (x == null || y == null || z == null) { -+ return; -+ } -+ -+ data.setInts(path, new int[] { x.intValue(), y.intValue(), z.intValue() }); -+ } -+ -+ public static void register() { -+ // Step 0 -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:vault", (final MapType root, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convert(MCTypeRegistry.ITEM_STACK, root.getMap("config"), "key_item", fromVersion, toVersion); -+ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, root.getMap("server_data"), "items_to_eject", fromVersion, toVersion); -+ WalkerUtils.convert(MCTypeRegistry.ITEM_STACK, root.getMap("shared_data"), "display_item", fromVersion, toVersion); -+ -+ return null; -+ }); -+ -+ // Step 1 -+ MCTypeRegistry.SAVED_DATA_MAP_DATA.addStructureConverter(new DataConverter<>(VERSION, 1) { -+ @Override -+ public MapType convert(final MapType root, final long sourceVersion, final long toVersion) { -+ final MapType data = root.getMap("data"); -+ -+ if (data == null) { -+ return null; -+ } -+ -+ final ListType banners = data.getList("banners", ObjectType.MAP); -+ -+ if (banners == null) { -+ return null; -+ } -+ -+ for (int i = 0, len = banners.size(); i < len; ++i) { -+ V3807.flattenBlockPos(banners.getMap(i), "Pos"); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V3807() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3808.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3808.java -new file mode 100644 -index 0000000000000000000000000000000000000000..cce86ebda50fc258eb28ad2f45e908b29934abec ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3808.java -@@ -0,0 +1,76 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+ -+public final class V3808 { -+ -+ private static final int VERSION = MCVersions.V24W04A + 2; -+ -+ public static void register() { -+ class BodyArmorConverter extends DataConverter, MapType> { -+ private final String path; -+ private final boolean clearArmor; -+ -+ public BodyArmorConverter(final int toVersion, final String path, final boolean clearArmor) { -+ this(toVersion, 0, path, clearArmor); -+ } -+ -+ public BodyArmorConverter(final int toVersion, final int versionStep, final String path, final boolean clearArmor) { -+ super(toVersion, versionStep); -+ -+ this.path = path; -+ this.clearArmor = clearArmor; -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType prev = data.getMap(this.path); -+ -+ if (prev == null) { -+ return null; -+ } -+ -+ data.remove(this.path); -+ -+ data.setMap("body_armor_item", prev); -+ data.setFloat("body_armor_drop_chance", 2.0F); -+ -+ if (this.clearArmor) { -+ final ListType armor = data.getList("ArmorItems", ObjectType.MAP); -+ if (armor.size() > 2) { -+ armor.setMap(2, data.getTypeUtil().createEmptyMap()); -+ } -+ -+ final ListType chances = data.getList("ArmorDropChances", ObjectType.FLOAT); -+ if (chances.size() > 2) { -+ chances.setFloat(2, 0.085F); -+ } -+ } -+ -+ return null; -+ } -+ } -+ -+ // Step 0 -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:horse", new BodyArmorConverter(VERSION, "ArmorItem", true)); -+ -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:horse", new DataWalkerItems("SaddleItem")); -+ V100.registerEquipment(VERSION, "minecraft:horse"); -+ -+ // Step 1 -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:llama", new BodyArmorConverter(VERSION, 1, "DecorItem", false)); -+ -+ MCTypeRegistry.ENTITY.addWalker(VERSION, 1, "minecraft:llama", new DataWalkerItemLists("Items")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, 1, "minecraft:llama", new DataWalkerItems("SaddleItem")); -+ V100.registerEquipment(VERSION, 1, "minecraft:llama"); -+ } -+ -+ private V3808() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3809.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3809.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7b782a6bb8d41f61201323d541f4acfac5af49dc ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3809.java -@@ -0,0 +1,40 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+ -+public final class V3809 { -+ -+ private static final int VERSION = MCVersions.V24W05A; -+ -+ public static void register() { -+ final DataConverter, MapType> slotConverter = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final ListType items = data.getList("Items", ObjectType.MAP); -+ if (items == null) { -+ return null; -+ } -+ -+ for (int i = 0, len = items.size(); i < len; ++i) { -+ final MapType item = items.getMap(i); -+ -+ final int slot = item.getInt("Slot", 2); -+ item.setByte("Slot", (byte)(slot - 2)); -+ } -+ -+ return null; -+ } -+ }; -+ -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:llama", slotConverter); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:mule", slotConverter); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:donkey", slotConverter); -+ } -+ -+ private V3809() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3812.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3812.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f0c0748b003648e5fe06d0b6dd1b21948ac0e54a ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3812.java -@@ -0,0 +1,48 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.util.NamespaceUtil; -+ -+public final class V3812 { -+ -+ private static final int VERSION = MCVersions.V24W05B + 1; -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:wolf", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ boolean doubleHealth = false; -+ -+ final ListType attributes = data.getList("Attributes", ObjectType.MAP); -+ if (attributes != null) { -+ for (int i = 0, len = attributes.size(); i < len; ++i) { -+ final MapType attribute = attributes.getMap(i); -+ -+ if (!"minecraft:generic.max_health".equals(NamespaceUtil.correctNamespace(attribute.getString("Name")))) { -+ continue; -+ } -+ -+ final double base = attribute.getDouble("Base", 0.0D); -+ if (base == 20.0D) { -+ attribute.setDouble("Base", 40.0D); -+ doubleHealth = true; -+ } -+ } -+ } -+ -+ if (doubleHealth) { -+ data.setFloat("Health", data.getFloat("Health", 0.0F) * 2.0F); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V3812() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3813.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3813.java -new file mode 100644 -index 0000000000000000000000000000000000000000..920d7734d883d74e8334102b22cabce24a79db7e ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3813.java -@@ -0,0 +1,131 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+ -+public final class V3813 { -+ -+ private static final int VERSION = MCVersions.V24W05B + 2; -+ -+ private static final String[] PATROLLING_MOBS = new String[] { -+ "minecraft:witch", -+ "minecraft:ravager", -+ "minecraft:pillager", -+ "minecraft:illusioner", -+ "minecraft:evoker", -+ "minecraft:vindicator" -+ }; -+ -+ public static void register() { -+ class RootPositionConverter extends DataConverter, MapType> { -+ private final RenamePair[] convert; -+ -+ public RootPositionConverter(final int toVersion, final RenamePair[] convert) { -+ this(toVersion, 0, convert); -+ } -+ -+ public RootPositionConverter(final int toVersion, final int versionStep, final RenamePair[] convert) { -+ super(toVersion, versionStep); -+ this.convert = convert; -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ for (final RenamePair rename : this.convert) { -+ V3807.flattenBlockPos(data, rename.from); -+ RenameHelper.renameSingle(data, rename.from, rename.to); -+ } -+ return null; -+ } -+ } -+ -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:bee", new RootPositionConverter(VERSION, new RenamePair[] { -+ new RenamePair("HivePos", "hive_pos"), -+ new RenamePair("FlowerPos", "flower_pos") -+ })); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:end_crystal", new RootPositionConverter(VERSION, new RenamePair[] { -+ new RenamePair("BeamTarget", "beam_target"), -+ })); -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:wandering_trader", new RootPositionConverter(VERSION, new RenamePair[] { -+ new RenamePair("WanderTarget", "wander_target"), -+ })); -+ -+ final RootPositionConverter patrolConverter = new RootPositionConverter(VERSION, new RenamePair[] { -+ new RenamePair("PatrolTarget", "patrol_target"), -+ }); -+ for (final String id : PATROLLING_MOBS) { -+ MCTypeRegistry.ENTITY.addConverterForId(id, patrolConverter); -+ } -+ -+ MCTypeRegistry.ENTITY.addStructureConverter(new RootPositionConverter(VERSION, new RenamePair[] { -+ new RenamePair("Leash", "leash"), -+ })); -+ -+ -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:beehive", new RootPositionConverter(VERSION, new RenamePair[] { -+ new RenamePair("FlowerPos", "flower_pos"), -+ })); -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:end_gateway", new RootPositionConverter(VERSION, new RenamePair[] { -+ new RenamePair("ExitPortal", "exit_portal"), -+ })); -+ -+ MCTypeRegistry.SAVED_DATA_MAP_DATA.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType root, final long sourceVersion, final long toVersion) { -+ final MapType data = root.getMap("data"); -+ -+ if (data == null) { -+ return null; -+ } -+ -+ final ListType frames = data.getList("frames", ObjectType.MAP); -+ if (frames != null) { -+ for (int i = 0, len = frames.size(); i < len; ++i) { -+ final MapType frame = frames.getMap(i); -+ -+ V3807.flattenBlockPos(frame, "Pos"); -+ -+ RenameHelper.renameSingle(frame, "Pos", "pos"); -+ RenameHelper.renameSingle(frame, "Rotation", "rotation"); -+ RenameHelper.renameSingle(frame, "EntityId", "entity_id"); -+ } -+ } -+ -+ final ListType banners = data.getList("banners", ObjectType.MAP); -+ for (int i = 0, len = banners.size(); i < len; ++i) { -+ final MapType banner = banners.getMap(i); -+ -+ RenameHelper.renameSingle(banner, "Pos", "pos"); -+ RenameHelper.renameSingle(banner, "Color", "color"); -+ RenameHelper.renameSingle(banner, "Name", "name"); -+ } -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:compass", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType tag = data.getMap("tag"); -+ -+ if (tag == null) { -+ return null; -+ } -+ -+ V3807.flattenBlockPos(tag, "LodestonePos"); -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V3813() {} -+ -+ private static record RenamePair(String from, String to) {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3814.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3814.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d3b156129452202aea1f3b9f32142c91b64dc777 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3814.java -@@ -0,0 +1,21 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.attributes.ConverterAbstractAttributesRename; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V3814 { -+ -+ private static final int VERSION = MCVersions.V24W05B + 3; -+ -+ public static void register() { -+ final Map renames = new HashMap<>( -+ Map.of("minecraft:horse.jump_strength", "minecraft:generic.jump_strength") -+ ); -+ -+ ConverterAbstractAttributesRename.register(VERSION, renames::get); -+ } -+ -+ private V3814() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3816.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3816.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f50b81d931a1908d405bb72e0679983a742d5223 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3816.java -@@ -0,0 +1,14 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+ -+public final class V3816 { -+ -+ private static final int VERSION = MCVersions.V24W06A + 1; -+ -+ public static void register() { -+ V100.registerEquipment(VERSION, "minecraft:bogged"); -+ } -+ -+ private V3816() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3818.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3818.java -new file mode 100644 -index 0000000000000000000000000000000000000000..15257d47a2c449290e428402a9fa1e955f07e341 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3818.java -@@ -0,0 +1,339 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.converters.datatypes.DataWalker; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.custom.V3818_Commands; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemstack.ConverterItemStackToDataComponents; -+import ca.spottedleaf.dataconverter.minecraft.converters.particle.ConverterParticleToNBT; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.Types; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V3818 { -+ -+ private static final int VERSION = MCVersions.V24W07A + 1; -+ -+ private static final String[] BANNER_COLOURS = new String[] { -+ "white", -+ "orange", -+ "magenta", -+ "light_blue", -+ "yellow", -+ "lime", -+ "pink", -+ "gray", -+ "light_gray", -+ "cyan", -+ "purple", -+ "blue", -+ "brown", -+ "green", -+ "red", -+ "black", -+ }; -+ -+ public static String getBannerColour(final int id) { -+ return id >= 0 && id < BANNER_COLOURS.length ? BANNER_COLOURS[id] : BANNER_COLOURS[0]; -+ } -+ -+ public static void register() { -+ // Step 0 -+ // Note: No breakpoint needed, nothing nests hotbar -+ MCTypeRegistry.HOTBAR.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ for (final String key : data.keys()) { -+ final ListType itemList = data.getList(key, ObjectType.MAP); -+ if (itemList != null) { -+ for (int i = 0, len = itemList.size(); i < len; ++i) { -+ final MapType item = itemList.getMap(i); -+ -+ final String id = item.getString("id"); -+ final int count = item.getInt("Count"); -+ -+ if ("minecraft:air".equals(id) || count <= 0) { -+ itemList.setMap(i, item.getTypeUtil().createEmptyMap()); -+ } -+ } -+ } -+ } -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:beehive", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ RenameHelper.renameSingle(data, "Bees", "bees"); -+ -+ final ListType bees = data.getList("bees", ObjectType.MAP); -+ if (bees != null) { -+ for (int i = 0, len = bees.size(); i < len; ++i) { -+ final MapType bee = bees.getMap(i); -+ -+ RenameHelper.renameSingle(bee, "EntityData", "entity_data"); -+ RenameHelper.renameSingle(bee, "TicksInHive", "ticks_in_hive"); -+ RenameHelper.renameSingle(bee, "MinOccupationTicks", "min_ticks_in_hive"); -+ } -+ } -+ -+ return null; -+ } -+ }); -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:beehive", (final MapType root, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convertListPath(MCTypeRegistry.ENTITY, root, "bees", "entity_data", fromVersion, toVersion); -+ -+ return null; -+ }); -+ -+ // Step 1 -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:banner", new DataConverter<>(VERSION, 1) { -+ private static final Map PATTERN_UPDATE = new HashMap<>(); -+ static { -+ PATTERN_UPDATE.put("b", "minecraft:base"); -+ PATTERN_UPDATE.put("bl", "minecraft:square_bottom_left"); -+ PATTERN_UPDATE.put("br", "minecraft:square_bottom_right"); -+ PATTERN_UPDATE.put("tl", "minecraft:square_top_left"); -+ PATTERN_UPDATE.put("tr", "minecraft:square_top_right"); -+ PATTERN_UPDATE.put("bs", "minecraft:stripe_bottom"); -+ PATTERN_UPDATE.put("ts", "minecraft:stripe_top"); -+ PATTERN_UPDATE.put("ls", "minecraft:stripe_left"); -+ PATTERN_UPDATE.put("rs", "minecraft:stripe_right"); -+ PATTERN_UPDATE.put("cs", "minecraft:stripe_center"); -+ PATTERN_UPDATE.put("ms", "minecraft:stripe_middle"); -+ PATTERN_UPDATE.put("drs", "minecraft:stripe_downright"); -+ PATTERN_UPDATE.put("dls", "minecraft:stripe_downleft"); -+ PATTERN_UPDATE.put("ss", "minecraft:small_stripes"); -+ PATTERN_UPDATE.put("cr", "minecraft:cross"); -+ PATTERN_UPDATE.put("sc", "minecraft:straight_cross"); -+ PATTERN_UPDATE.put("bt", "minecraft:triangle_bottom"); -+ PATTERN_UPDATE.put("tt", "minecraft:triangle_top"); -+ PATTERN_UPDATE.put("bts", "minecraft:triangles_bottom"); -+ PATTERN_UPDATE.put("tts", "minecraft:triangles_top"); -+ PATTERN_UPDATE.put("ld", "minecraft:diagonal_left"); -+ PATTERN_UPDATE.put("rd", "minecraft:diagonal_up_right"); -+ PATTERN_UPDATE.put("lud", "minecraft:diagonal_up_left"); -+ PATTERN_UPDATE.put("rud", "minecraft:diagonal_right"); -+ PATTERN_UPDATE.put("mc", "minecraft:circle"); -+ PATTERN_UPDATE.put("mr", "minecraft:rhombus"); -+ PATTERN_UPDATE.put("vh", "minecraft:half_vertical"); -+ PATTERN_UPDATE.put("hh", "minecraft:half_horizontal"); -+ PATTERN_UPDATE.put("vhr", "minecraft:half_vertical_right"); -+ PATTERN_UPDATE.put("hhb", "minecraft:half_horizontal_bottom"); -+ PATTERN_UPDATE.put("bo", "minecraft:border"); -+ PATTERN_UPDATE.put("cbo", "minecraft:curly_border"); -+ PATTERN_UPDATE.put("gra", "minecraft:gradient"); -+ PATTERN_UPDATE.put("gru", "minecraft:gradient_up"); -+ PATTERN_UPDATE.put("bri", "minecraft:bricks"); -+ PATTERN_UPDATE.put("glb", "minecraft:globe"); -+ PATTERN_UPDATE.put("cre", "minecraft:creeper"); -+ PATTERN_UPDATE.put("sku", "minecraft:skull"); -+ PATTERN_UPDATE.put("flo", "minecraft:flower"); -+ PATTERN_UPDATE.put("moj", "minecraft:mojang"); -+ PATTERN_UPDATE.put("pig", "minecraft:piglin"); -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final ListType patterns = data.getList("Patterns", ObjectType.MAP); -+ if (patterns != null) { -+ for (int i = 0, len = patterns.size(); i < len; ++i) { -+ final MapType pattern = patterns.getMap(i); -+ -+ final String patternName = pattern.getString("Pattern"); -+ if (patternName != null) { -+ final String renamed = PATTERN_UPDATE.get(patternName); -+ if (renamed != null) { -+ pattern.setString("Pattern", renamed); -+ } -+ } -+ RenameHelper.renameSingle(pattern, "Pattern", "pattern"); -+ -+ final String newColour = getBannerColour(pattern.getInt("Color")); -+ pattern.setString("Color", newColour); -+ RenameHelper.renameSingle(pattern, "Color", "color"); -+ } -+ } -+ RenameHelper.renameSingle(data, "Patterns", "patterns"); -+ -+ return null; -+ } -+ }); -+ -+ // Step 2 -+ // Note: there is nothing after the previous breakpoint (1.19.4) that reads nested entity item -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:arrow", new DataConverter<>(VERSION, 2) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final Object potion = data.getGeneric("Potion"); -+ final Object customPotionEffects = data.getGeneric("custom_potion_effects"); -+ final Object color = data.getGeneric("Color"); -+ -+ if (potion == null && customPotionEffects == null && color == null) { -+ return null; -+ } -+ -+ data.remove("Potion"); -+ data.remove("custom_potion_effects"); -+ data.remove("Color"); -+ -+ final MapType item = data.getMap("item"); -+ if (item == null) { -+ return null; -+ } -+ -+ final MapType tag = item.getOrCreateMap("tag"); -+ -+ if (potion != null) { -+ tag.setGeneric("Potion", potion); -+ } -+ if (customPotionEffects != null) { -+ tag.setGeneric("custom_potion_effects", customPotionEffects); -+ } -+ if (color != null) { -+ tag.setGeneric("CustomPotionColor", color); -+ } -+ -+ return null; -+ } -+ }); -+ -+ // Step 3 -+ MCTypeRegistry.DATA_COMPONENTS.addStructureWalker(VERSION, new DataWalker<>() { -+ private static void walkBlockPredicates(final MapType root, final long fromVersion, final long toVersion) { -+ if (root.hasKey("blocks", ObjectType.STRING)) { -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, root, "blocks", fromVersion, toVersion); -+ } else if (root.hasKey("blocks", ObjectType.LIST)) { -+ WalkerUtils.convertList(MCTypeRegistry.BLOCK_NAME, root, "blocks", fromVersion, toVersion); -+ } -+ } -+ -+ @Override -+ public MapType walk(final MapType root, final long fromVersion, final long toVersion) { -+ WalkerUtils.convertListPath(MCTypeRegistry.ENTITY, root, "minecraft:bees", "entity_data", fromVersion, toVersion); -+ -+ WalkerUtils.convert(MCTypeRegistry.TILE_ENTITY, root, "minecraft:block_entity_data", fromVersion, toVersion); -+ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, root, "minecraft:bundle_contents", fromVersion, toVersion); -+ -+ final MapType canBreak = root.getMap("minecraft:can_break"); -+ if (canBreak != null) { -+ final ListType predicates = canBreak.getList("predicates", ObjectType.MAP); -+ if (predicates != null) { -+ for (int i = 0, len = predicates.size(); i < len; ++i) { -+ walkBlockPredicates(predicates.getMap(i), fromVersion, toVersion); -+ } -+ } -+ // Not handled by DFU: simple encoding does not require "predicates" -+ walkBlockPredicates(canBreak, fromVersion, toVersion); -+ } -+ -+ final MapType canPlaceOn = root.getMap("minecraft:can_break"); -+ if (canPlaceOn != null) { -+ final ListType predicates = canPlaceOn.getList("predicates", ObjectType.MAP); -+ if (predicates != null) { -+ for (int i = 0, len = predicates.size(); i < len; ++i) { -+ walkBlockPredicates(predicates.getMap(i), fromVersion, toVersion); -+ } -+ } -+ // Not handled by DFU: simple encoding does not require "predicates" -+ walkBlockPredicates(canPlaceOn, fromVersion, toVersion); -+ } -+ -+ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, root, "minecraft:charged_projectiles", fromVersion, toVersion); -+ WalkerUtils.convertListPath(MCTypeRegistry.ITEM_STACK, root, "minecraft:container", "item", fromVersion, toVersion); -+ WalkerUtils.convert(MCTypeRegistry.ENTITY, root, "minecraft:entity_data", fromVersion, toVersion); -+ WalkerUtils.convertList(MCTypeRegistry.ITEM_NAME, root, "minecraft:pot_decorations", fromVersion, toVersion); -+ -+ return null; -+ } -+ }); -+ -+ // Step 4 -+ MCTypeRegistry.PARTICLE.addStructureConverter(new DataConverter<>(VERSION, 4) { -+ @Override -+ public MapType convert(final Object input, final long sourceVersion, final long toVersion) { -+ if (!(input instanceof String flat)) { -+ return null; -+ } -+ -+ return ConverterParticleToNBT.convert(flat, Types.NBT); -+ } -+ }); -+ -+ MCTypeRegistry.PARTICLE.addStructureWalker(VERSION, 4, (final Object input, final long fromVersion, final long toVersion) -> { -+ if (!(input instanceof MapType)) { -+ return null; -+ } -+ -+ final MapType root = (MapType)input; -+ -+ WalkerUtils.convert(MCTypeRegistry.ITEM_STACK, root, "item", fromVersion, toVersion); -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_STATE, root, "block_state", fromVersion, toVersion); -+ -+ return null; -+ }); -+ -+ // Step 5 -+ // Note: needs breakpoint, reads nested tile entity data -+ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(VERSION, 5) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ return ConverterItemStackToDataComponents.convertItem(data); -+ } -+ }); -+ -+ MCTypeRegistry.ITEM_STACK.addStructureWalker(VERSION, 5, (final MapType root, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convert(MCTypeRegistry.ITEM_NAME, root, "id", fromVersion, toVersion); -+ WalkerUtils.convert(MCTypeRegistry.DATA_COMPONENTS, root, "components", fromVersion, toVersion); -+ -+ return null; -+ }); -+ -+ // Custom converter for converting commands inside signs, books, command blocks -+ V3818_Commands.register_5(); -+ -+ // Step 6 -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:area_effect_cloud", new DataConverter<>(VERSION, 6) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final Object color = data.getGeneric("Color"); -+ final Object effects = data.getGeneric("effects"); -+ final Object potion = data.getGeneric("Potion"); -+ -+ if (color == null && effects == null && potion == null) { -+ return null; -+ } -+ data.remove("Color"); -+ data.remove("effects"); -+ data.remove("Potion"); -+ -+ final MapType potionContents = data.getTypeUtil().createEmptyMap(); -+ data.setMap("potion_contents", potionContents); -+ -+ if (color != null) { -+ potionContents.setGeneric("custom_color", color); -+ } -+ -+ if (effects != null) { -+ potionContents.setGeneric("custom_effects", effects); -+ } -+ -+ if (potion != null) { -+ potionContents.setGeneric("potion", potion); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V3818() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3820.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3820.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0635b81924d4431fdfbcdd3814eaf01f85e53787 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3820.java -@@ -0,0 +1,78 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemstack.ConverterItemStackToDataComponents; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V3820 { -+ -+ private static final int VERSION = MCVersions.V24W09A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:skull", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final Object skullOwner = data.getGeneric("SkullOwner"); -+ final Object extraType = data.getGeneric("ExtraType"); -+ -+ if (skullOwner == null && extraType == null) { -+ return null; -+ } -+ -+ data.remove("SkullOwner"); -+ data.remove("ExtraType"); -+ -+ data.setMap( -+ "profile", -+ ConverterItemStackToDataComponents.convertProfile( -+ skullOwner == null ? extraType : skullOwner, data.getTypeUtil() -+ ) -+ ); -+ -+ return null; -+ } -+ }); -+ // I don't see why this converter is necessary, V3818 should have converted correctly... -+ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType components = data.getMap("components"); -+ -+ if (components == null) { -+ return null; -+ } -+ -+ final MapType oldTarget = components.getMap("minecraft:lodestone_target"); -+ if (oldTarget == null) { -+ return null; -+ } -+ -+ components.remove("minecraft:lodestone_target"); -+ components.setMap("minecraft:lodestone_tracker", oldTarget); -+ -+ final Object pos = oldTarget.getMap("pos"); -+ final Object dim = oldTarget.getMap("dimension"); -+ -+ if (pos == null && dim == null) { -+ return null; -+ } -+ -+ oldTarget.remove("pos"); -+ oldTarget.remove("dimension"); -+ -+ final MapType target = oldTarget.getTypeUtil().createEmptyMap(); -+ oldTarget.setMap("target", target); -+ -+ target.setGeneric("pos", pos); -+ target.setGeneric("dimension", dim); -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V3820() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3825.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3825.java -new file mode 100644 -index 0000000000000000000000000000000000000000..cded86949c5af12aa96a8883b56b36b13d5dcd57 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3825.java -@@ -0,0 +1,155 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.util.ComponentUtils; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import java.util.Arrays; -+import java.util.HashSet; -+import java.util.Set; -+ -+public final class V3825 { -+ -+ private static final int VERSION = MCVersions.V24W12A + 1; -+ -+ private static final Set BANNER_NAMES = new HashSet<>( -+ Arrays.asList( -+ "block.minecraft.ominous_banner" -+ ) -+ ); -+ private static final Set MAP_NAMES = new HashSet<>( -+ Arrays.asList( -+ "filled_map.buried_treasure", -+ "filled_map.explorer_jungle", -+ "filled_map.explorer_swamp", -+ "filled_map.mansion", -+ "filled_map.monument", -+ "filled_map.trial_chambers", -+ "filled_map.village_desert", -+ "filled_map.village_plains", -+ "filled_map.village_savanna", -+ "filled_map.village_snowy", -+ "filled_map.village_taiga" -+ ) -+ ); -+ -+ public static void register() { -+ MCTypeRegistry.ITEM_STACK.addStructureConverter(new DataConverter<>(VERSION) { -+ private static void convertName(final MapType components, final Set standardNames) { -+ final String customName = components.getString("minecraft:custom_name"); -+ if (customName == null) { -+ return; -+ } -+ -+ final String translation = ComponentUtils.retrieveTranslationString(customName); -+ if (translation == null) { -+ return; -+ } -+ -+ if (standardNames.contains(translation)) { -+ components.remove("minecraft:custom_name"); -+ components.setString("minecraft:item_name", customName); -+ } -+ } -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType components = data.getMap("components"); -+ if (components == null) { -+ return null; -+ } -+ -+ final String id = data.getString("id"); -+ if (id == null) { -+ return null; -+ } -+ -+ switch (id) { -+ case "minecraft:white_banner": { -+ convertName(components, BANNER_NAMES); -+ break; -+ } -+ case "minecraft:filled_map": { -+ convertName(components, MAP_NAMES); -+ break; -+ } -+ } -+ -+ return null; -+ } -+ }); -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:banner", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final String customName = data.getString("CustomName"); -+ if (customName == null || !"block.minecraft.ominous_banner".equals(ComponentUtils.retrieveTranslationString(customName))) { -+ return null; -+ } -+ -+ data.remove("CustomName"); -+ -+ final MapType components = data.getOrCreateMap("components"); -+ -+ components.setString("minecraft:item_name", customName); -+ components.setMap("minecraft:hide_additional_tooltip", components.getTypeUtil().createEmptyMap()); -+ -+ return null; -+ } -+ }); -+ // DFU does not change the schema, even though it moves spawn_potentials -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:trial_spawner", (final MapType data, final long fromVersion, final long toVersion) -> { -+ final MapType normalConfig = data.getMap("normal_config"); -+ if (normalConfig != null) { -+ WalkerUtils.convertListPath(MCTypeRegistry.ENTITY, normalConfig, "spawn_potentials", "data", "entity", fromVersion, toVersion); -+ } -+ final MapType ominousConfig = data.getMap("ominous_config"); -+ if (ominousConfig != null) { -+ WalkerUtils.convertListPath(MCTypeRegistry.ENTITY, normalConfig, "spawn_potentials", "data", "entity", fromVersion, toVersion); -+ } -+ -+ WalkerUtils.convert(MCTypeRegistry.ENTITY, data.getMap("spawn_data"), "entity", fromVersion, toVersion); -+ return null; -+ }); -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:trial_spawner", new DataConverter<>(VERSION) { -+ private static final String[] NORMAL_CONFIG_KEYS = new String[] { -+ "spawn_range", -+ "total_mobs", -+ "simultaneous_mobs", -+ "total_mobs_added_per_player", -+ "simultaneous_mobs_added_per_player", -+ "ticks_between_spawn", -+ "spawn_potentials", -+ "loot_tables_to_eject", -+ "items_to_drop_when_ominous" -+ }; -+ -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType normalConfig = data.getTypeUtil().createEmptyMap(); -+ -+ for (final String normalKey : NORMAL_CONFIG_KEYS) { -+ final Object normalValue = data.getGeneric(normalKey); -+ if (normalValue != null) { -+ data.remove(normalKey); -+ normalConfig.setGeneric(normalKey, normalValue); -+ } -+ } -+ -+ if (!normalConfig.isEmpty()) { -+ data.setMap("normal_config", normalConfig); -+ } -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:ominous_item_spawner", new DataWalkerItems("item")); -+ } -+ -+ private V3825() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3828.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3828.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f752bb2fca2e4cd438c0540460912d4bc2c6f25e ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3828.java -@@ -0,0 +1,37 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.util.NamespaceUtil; -+ -+public final class V3828 { -+ -+ private static final int VERSION = MCVersions.V24W14A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.VILLAGER_TRADE.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType buyB = data.getMap("buyB"); -+ -+ if (buyB == null) { -+ return null; -+ } -+ -+ final String id = NamespaceUtil.correctNamespace(buyB.getString("id", "minecraft:air")); -+ final int count = buyB.getInt("count", 0); -+ -+ // Fix DFU: use count <= 0 instead of count == 0 -+ if ("minecraft:air".equals(id) || count <= 0) { -+ data.remove("buyB"); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V3828() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3833.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3833.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4581b9ce0734e552fd8810239e7b86f8b43513c5 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V3833.java -@@ -0,0 +1,37 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.util.NamespaceUtil; -+ -+public final class V3833 { -+ -+ private static final int VERSION = MCVersions.V1_20_5_PRE4 + 1; -+ -+ public static void register() { -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:brushable_block", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType item = data.getMap("item"); -+ if (item == null) { -+ return null; -+ } -+ -+ -+ final String id = NamespaceUtil.correctNamespace(item.getString("id", "minecraft:air")); -+ final int count = item.getInt("count", 0); -+ -+ // Fix DFU: use count <= 0 instead of count == 0 -+ if ("minecraft:air".equals(id) || count <= 0) { -+ data.remove("item"); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V3833() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V501.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V501.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a7a4d6446b7765ac485af82df660aafab05955bf ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V501.java -@@ -0,0 +1,18 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+ -+public final class V501 { -+ -+ private static final int VERSION = MCVersions.V16W20A; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ registerMob("PolarBear"); -+ } -+ -+ private V501() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V502.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V502.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7f88b435378305a3a66e1e54b85afd9b019513ee ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V502.java -@@ -0,0 +1,45 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import java.util.concurrent.ThreadLocalRandom; -+ -+public final class V502 { -+ -+ private static final int VERSION = MCVersions.V16W20A + 1; -+ -+ public static void register() { -+ ConverterAbstractItemRename.register(VERSION, (final String name) -> { -+ return "minecraft:cooked_fished".equals(name) ? "minecraft:cooked_fish" : null; -+ }); -+ MCTypeRegistry.ENTITY.addConverterForId("Zombie", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (!data.getBoolean("IsVillager")) { -+ return null; -+ } -+ -+ data.remove("IsVillager"); -+ -+ if (data.hasKey("ZombieType")) { -+ return null; -+ } -+ -+ int type = data.getInt("VillagerProfession", -1); -+ // Vanilla doesn't remove the profession tag, so we don't! -+ if (type < 0 || type >= 6) { -+ type = ThreadLocalRandom.current().nextInt(6); -+ } -+ -+ data.setInt("ZombieType", type); -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V502() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V505.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V505.java -new file mode 100644 -index 0000000000000000000000000000000000000000..3faf2c3265600141003355771f38a7879e0f769a ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V505.java -@@ -0,0 +1,23 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V505 { -+ -+ private static final int VERSION = MCVersions.V16W21B + 1; -+ -+ public static void register() { -+ MCTypeRegistry.OPTIONS.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ data.setString("useVbo", "true"); -+ return null; -+ } -+ }); -+ } -+ -+ private V505() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V700.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V700.java -new file mode 100644 -index 0000000000000000000000000000000000000000..3b65108c6a1ac469bb8f81a933b6475f3ea9f63f ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V700.java -@@ -0,0 +1,32 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V700 { -+ -+ private static final int VERSION = MCVersions.V1_10_2 + 188; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("Guardian", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (data.getBoolean("Elder")) { -+ data.setString("id", "ElderGuardian"); -+ } -+ data.remove("Elder"); -+ return null; -+ } -+ }); -+ -+ registerMob("ElderGuardian"); -+ } -+ -+ private V700() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V701.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V701.java -new file mode 100644 -index 0000000000000000000000000000000000000000..55f00e218f04e1e095ccc7d62282d87d7eb8f8c7 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V701.java -@@ -0,0 +1,41 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V701 { -+ -+ private static final int VERSION = MCVersions.V1_10_2 + 189; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("Skeleton", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final int type = data.getInt("SkeletonType"); -+ data.remove("SkeletonType"); -+ -+ switch (type) { -+ case 1: -+ data.setString("id", "WitherSkeleton"); -+ break; -+ case 2: -+ data.setString("id", "Stray"); -+ break; -+ } -+ -+ return null; -+ } -+ }); -+ -+ registerMob("WitherSkeleton"); -+ registerMob("Stray"); -+ } -+ -+ private V701() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V702.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V702.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c0d74b4822be60c637f26b2ef1e172fdf9e89d01 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V702.java -@@ -0,0 +1,56 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V702 { -+ -+ private static final int VERSION = MCVersions.V1_10_2 + 190; -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("Zombie", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final int zombieType = data.getInt("ZombieType"); -+ data.remove("ZombieType"); -+ -+ switch (zombieType) { -+ case 0: -+ default: -+ break; -+ -+ case 1: -+ case 2: -+ case 3: -+ case 4: -+ case 5: -+ data.setString("id", "ZombieVillager"); -+ data.setInt("Profession", zombieType - 1); -+ break; -+ -+ case 6: -+ data.setString("id", "Husk"); -+ break; -+ } -+ -+ return null; -+ } -+ }); -+ -+ registerMob("ZombieVillager"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "ZombieVillager", (final MapType root, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convertList(MCTypeRegistry.VILLAGER_TRADE, root.getMap("Offers"), "Recipes", fromVersion, toVersion); -+ return null; -+ }); -+ registerMob( "Husk"); -+ } -+ -+ private V702() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V703.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V703.java -new file mode 100644 -index 0000000000000000000000000000000000000000..cc593df4a09d6cb93196d8cfb34ebac43e61ebbe ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V703.java -@@ -0,0 +1,67 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V703 { -+ -+ private static final int VERSION = MCVersions.V1_10_2 + 191; -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("EntityHorse", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final int type = data.getInt("Type"); -+ data.remove("Type"); -+ -+ switch (type) { -+ case 0: -+ default: -+ data.setString("id", "Horse"); -+ break; -+ -+ case 1: -+ data.setString("id", "Donkey"); -+ break; -+ -+ case 2: -+ data.setString("id", "Mule"); -+ break; -+ -+ case 3: -+ data.setString("id", "ZombieHorse"); -+ break; -+ -+ case 4: -+ data.setString("id", "SkeletonHorse"); -+ break; -+ } -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "Horse", new DataWalkerItems("ArmorItem", "SaddleItem")); -+ V100.registerEquipment(VERSION, "Horse"); -+ -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "Donkey", new DataWalkerItems("SaddleItem")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "Donkey", new DataWalkerItemLists("Items")); -+ V100.registerEquipment(VERSION, "Donkey"); -+ -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "Mule", new DataWalkerItems("SaddleItem")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "Mule", new DataWalkerItemLists("Items")); -+ V100.registerEquipment(VERSION, "Mule"); -+ -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "ZombieHorse", new DataWalkerItems("SaddleItem")); -+ V100.registerEquipment(VERSION, "ZombieHorse"); -+ -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "SkeletonHorse", new DataWalkerItems("SaddleItem")); -+ V100.registerEquipment(VERSION, "SkeletonHorse"); -+ } -+ -+ private V703() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V704.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V704.java -new file mode 100644 -index 0000000000000000000000000000000000000000..10ee10109405bd569beb23a235a7411db45d6ec0 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V704.java -@@ -0,0 +1,432 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.hooks.DataHookEnforceNamespacedID; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+import ca.spottedleaf.dataconverter.minecraft.walkers.item_name.DataWalkerItemNames; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.util.Long2ObjectArraySortedMap; -+import com.mojang.logging.LogUtils; -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.registries.BuiltInRegistries; -+import net.minecraft.world.item.BlockItem; -+import net.minecraft.world.item.Item; -+import net.minecraft.world.level.block.Block; -+import net.minecraft.world.level.block.EntityBlock; -+import net.minecraft.world.level.block.entity.BlockEntity; -+import net.minecraft.world.level.block.entity.BlockEntityType; -+import org.slf4j.Logger; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V704 { -+ -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ -+ private static final int VERSION = MCVersions.V1_10_2 + 192; -+ -+ public static final Map ITEM_ID_TO_TILE_ENTITY_ID = new HashMap<>() { -+ @Override -+ public String put(final String key, final String value) { -+ if (this.containsKey(key)) { -+ LOGGER.error("Duplicate item id to tile key: " + key); -+ throw new RuntimeException(); // only devs should see the consequence of this... at least start up the damn thing... -+ } -+ return super.put(key, value); -+ } -+ }; -+ static { -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:furnace", "minecraft:furnace"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:lit_furnace", "minecraft:furnace"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:chest", "minecraft:chest"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:trapped_chest", "minecraft:chest"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:ender_chest", "minecraft:ender_chest"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:jukebox", "minecraft:jukebox"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:dispenser", "minecraft:dispenser"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:dropper", "minecraft:dropper"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:mob_spawner", "minecraft:mob_spawner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:spawner", "minecraft:mob_spawner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:noteblock", "minecraft:noteblock"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:brewing_stand", "minecraft:brewing_stand"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:enhanting_table", "minecraft:enchanting_table"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:command_block", "minecraft:command_block"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:beacon", "minecraft:beacon"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:skull", "minecraft:skull"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:daylight_detector", "minecraft:daylight_detector"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:hopper", "minecraft:hopper"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:banner", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:flower_pot", "minecraft:flower_pot"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:repeating_command_block", "minecraft:command_block"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:chain_command_block", "minecraft:command_block"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:shulker_box", "minecraft:shulker_box"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:white_shulker_box", "minecraft:shulker_box"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:orange_shulker_box", "minecraft:shulker_box"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:magenta_shulker_box", "minecraft:shulker_box"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:light_blue_shulker_box", "minecraft:shulker_box"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:yellow_shulker_box", "minecraft:shulker_box"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:lime_shulker_box", "minecraft:shulker_box"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:pink_shulker_box", "minecraft:shulker_box"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:gray_shulker_box", "minecraft:shulker_box"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:silver_shulker_box", "minecraft:shulker_box"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:cyan_shulker_box", "minecraft:shulker_box"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:purple_shulker_box", "minecraft:shulker_box"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:blue_shulker_box", "minecraft:shulker_box"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:brown_shulker_box", "minecraft:shulker_box"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:green_shulker_box", "minecraft:shulker_box"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:red_shulker_box", "minecraft:shulker_box"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:black_shulker_box", "minecraft:shulker_box"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:bed", "minecraft:bed"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:light_gray_shulker_box", "minecraft:shulker_box"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:white_banner", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:orange_banner", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:magenta_banner", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:light_blue_banner", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:yellow_banner", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:lime_banner", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:pink_banner", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:gray_banner", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:silver_banner", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:cyan_banner", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:purple_banner", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:blue_banner", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:brown_banner", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:green_banner", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:red_banner", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:black_banner", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:standing_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:wall_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:piston_head", "minecraft:piston"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:daylight_detector_inverted", "minecraft:daylight_detector"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:unpowered_comparator", "minecraft:comparator"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:powered_comparator", "minecraft:comparator"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:wall_banner", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:standing_banner", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:structure_block", "minecraft:structure_block"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:end_portal", "minecraft:end_portal"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:end_gateway", "minecraft:end_gateway"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:shield", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:white_bed", "minecraft:bed"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:orange_bed", "minecraft:bed"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:magenta_bed", "minecraft:bed"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:light_blue_bed", "minecraft:bed"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:yellow_bed", "minecraft:bed"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:lime_bed", "minecraft:bed"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:pink_bed", "minecraft:bed"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:gray_bed", "minecraft:bed"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:silver_bed", "minecraft:bed"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:cyan_bed", "minecraft:bed"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:purple_bed", "minecraft:bed"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:blue_bed", "minecraft:bed"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:brown_bed", "minecraft:bed"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:green_bed", "minecraft:bed"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:red_bed", "minecraft:bed"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:black_bed", "minecraft:bed"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:oak_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:spruce_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:birch_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:jungle_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:acacia_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:dark_oak_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:crimson_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:warped_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:skeleton_skull", "minecraft:skull"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:wither_skeleton_skull", "minecraft:skull"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:zombie_head", "minecraft:skull"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:player_head", "minecraft:skull"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:creeper_head", "minecraft:skull"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:dragon_head", "minecraft:skull"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:barrel", "minecraft:barrel"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:conduit", "minecraft:conduit"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:smoker", "minecraft:smoker"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:blast_furnace", "minecraft:blast_furnace"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:lectern", "minecraft:lectern"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:bell", "minecraft:bell"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:jigsaw", "minecraft:jigsaw"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:campfire", "minecraft:campfire"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:bee_nest", "minecraft:beehive"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:beehive", "minecraft:beehive"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:sculk_sensor", "minecraft:sculk_sensor"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:decorated_pot", "minecraft:decorated_pot"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:crafter", "minecraft:crafter"); -+ -+ // These are missing from Vanilla up to 1.20.5 -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:enchanting_table", "minecraft:enchanting_table"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:comparator", "minecraft:comparator"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:light_gray_bed", "minecraft:bed"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:light_gray_banner", "minecraft:banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:soul_campfire", "minecraft:campfire"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:sculk_catalyst", "minecraft:sculk_catalyst"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:mangrove_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:sculk_shrieker", "minecraft:sculk_shrieker"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:chiseled_bookshelf", "minecraft:chiseled_bookshelf"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:bamboo_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:oak_hanging_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:spruce_hanging_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:birch_hanging_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:jungle_hanging_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:acacia_hanging_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:dark_oak_hanging_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:mangrove_hanging_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:bamboo_hanging_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:crimson_hanging_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:warped_hanging_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:piglin_head", "minecraft:skull"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:suspicious_sand", "minecraft:brushable_block"); // note: this was renamed in the past, see special case in the itemstack walker -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:cherry_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:cherry_hanging_sign", "minecraft:sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:suspicious_gravel", "minecraft:brushable_block"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:calibrated_sculk_sensor", "minecraft:calibrated_sculk_sensor"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:trial_spawner", "minecraft:trial_spawner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:vault", "minecraft:vault"); -+ } -+ -+ // This class is responsible for also integrity checking the item id to tile id map here, we just use the item registry to figure it out -+ // No longer need to do this, as items now use components in 1.20.5 -+ /*static { -+ for (final Item item : BuiltInRegistries.ITEM) { -+ if (!(item instanceof BlockItem)) { -+ continue; -+ } -+ -+ if (!(((BlockItem)item).getBlock() instanceof EntityBlock entityBlock)) { -+ continue; -+ } -+ -+ String possibleId; -+ try { -+ final BlockEntity entity = entityBlock.newBlockEntity(new BlockPos(0, 0, 0), ((Block)entityBlock).defaultBlockState()); -+ if (entity != null) { -+ possibleId = BlockEntityType.getKey(entity.getType()).toString(); -+ } else { -+ possibleId = null; -+ } -+ } catch (final Throwable th) { -+ possibleId = null; -+ } -+ -+ final String itemName = BuiltInRegistries.ITEM.getKey(item).toString(); -+ final String mappedTo = ITEM_ID_TO_TILE_ENTITY_ID.get(itemName); -+ if (mappedTo == null) { -+ LOGGER.error("Item id " + itemName + " does not contain tile mapping! (V704)"); -+ } else if (possibleId != null && !mappedTo.equals(possibleId)) { -+ final boolean chestCase = mappedTo.equals("minecraft:chest") && possibleId.equals("minecraft:trapped_chest"); -+ final boolean signCase = mappedTo.equals("minecraft:sign") && possibleId.equals("minecraft:hanging_sign"); -+ // save data is identical for the chest and sign case, so we don't care -+ // it's also important to note that there is no versioning for this map, so it is possible -+ // that mapping them correctly could cause issues converting old data -+ if (!chestCase && !signCase) { -+ LOGGER.error("Item id " + itemName + " is mapped to the wrong tile entity! Mapped to: " + mappedTo + ", expected: " + possibleId); -+ } -+ } -+ } -+ }*/ -+ -+ private static Long2ObjectArraySortedMap makeSingle(final long k1, final String v1) { -+ final Long2ObjectArraySortedMap ret = new Long2ObjectArraySortedMap<>(); -+ -+ ret.put(k1, v1); -+ -+ return ret; -+ } -+ -+ private static Long2ObjectArraySortedMap makeDouble(final long k1, final String v1, -+ final long k2, final String v2) { -+ final Long2ObjectArraySortedMap ret = new Long2ObjectArraySortedMap<>(); -+ -+ ret.put(k1, v1); -+ ret.put(k2, v2); -+ -+ return ret; -+ } -+ -+ private static final Map> ITEM_ID_TO_ENTITY_ID = new HashMap<>(); -+ static { -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:armor_stand", makeDouble(V99.VERSION, "ArmorStand", V705.VERSION, "minecraft:armor_stand")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:painting", makeDouble(V99.VERSION, "Painting", V705.VERSION, "minecraft:painting")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:boat", makeDouble(V99.VERSION, "Boat", V705.VERSION, "minecraft:boat")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:oak_boat", makeSingle(V705.VERSION, "minecraft:boat")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:oak_chest_boat", makeSingle(V705.VERSION, "minecraft:chest_boat")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:spruce_boat",makeSingle(V705.VERSION, "minecraft:boat")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:spruce_chest_boat", makeSingle(V705.VERSION, "minecraft:chest_boat")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:birch_boat", makeSingle(V705.VERSION, "minecraft:boat")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:birch_chest_boat", makeSingle(V705.VERSION, "minecraft:chest_boat")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:jungle_boat", makeSingle(V705.VERSION, "minecraft:boat")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:jungle_chest_boat", makeSingle(V705.VERSION, "minecraft:chest_boat")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:acacia_boat", makeSingle(V705.VERSION, "minecraft:boat")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:acacia_chest_boat", makeSingle(V705.VERSION, "minecraft:chest_boat")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:cherry_boat", makeSingle(V705.VERSION, "minecraft:boat")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:cherry_chest_boat", makeSingle(V705.VERSION, "minecraft:chest_boat")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:dark_oak_boat", makeSingle(V705.VERSION, "minecraft:boat")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:dark_oak_chest_boat", makeSingle(V705.VERSION, "minecraft:chest_boat")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:mangrove_boat", makeSingle(V705.VERSION, "minecraft:boat")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:mangrove_chest_boat", makeSingle(V705.VERSION, "minecraft:chest_boat")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:bamboo_raft", makeSingle(V705.VERSION, "minecraft:boat")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:bamboo_chest_raft", makeSingle(V705.VERSION, "minecraft:chest_boat")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:minecart", makeDouble(V99.VERSION, "MinecartRideable", V705.VERSION, "minecraft:minecart")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:chest_minecart", makeDouble(V99.VERSION, "MinecartChest", V705.VERSION, "minecraft:chest_minecart")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:furnace_minecart", makeDouble(V99.VERSION, "MinecartFurnace", V705.VERSION, "minecraft:furnace_minecart")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:tnt_minecart", makeDouble(V99.VERSION, "MinecartTNT", V705.VERSION, "minecraft:tnt_minecart")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:hopper_minecart", makeDouble(V99.VERSION, "MinecartHopper", V705.VERSION, "minecraft:hopper_minecart")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:item_frame", makeDouble(V99.VERSION, "ItemFrame", V705.VERSION, "minecraft:item_frame")); -+ ITEM_ID_TO_ENTITY_ID.put("minecraft:glow_item_frame", makeSingle(V705.VERSION, "minecraft:glow_item_frame")); -+ } -+ -+ private static final Map TILE_ID_UPDATE = new HashMap<>(); -+ static { -+ TILE_ID_UPDATE.put("Airportal", "minecraft:end_portal"); -+ TILE_ID_UPDATE.put("Banner", "minecraft:banner"); -+ TILE_ID_UPDATE.put("Beacon", "minecraft:beacon"); -+ TILE_ID_UPDATE.put("Cauldron", "minecraft:brewing_stand"); -+ TILE_ID_UPDATE.put("Chest", "minecraft:chest"); -+ TILE_ID_UPDATE.put("Comparator", "minecraft:comparator"); -+ TILE_ID_UPDATE.put("Control", "minecraft:command_block"); -+ TILE_ID_UPDATE.put("DLDetector", "minecraft:daylight_detector"); -+ TILE_ID_UPDATE.put("Dropper", "minecraft:dropper"); -+ TILE_ID_UPDATE.put("EnchantTable", "minecraft:enchanting_table"); -+ TILE_ID_UPDATE.put("EndGateway", "minecraft:end_gateway"); -+ TILE_ID_UPDATE.put("EnderChest", "minecraft:ender_chest"); -+ TILE_ID_UPDATE.put("FlowerPot", "minecraft:flower_pot"); -+ TILE_ID_UPDATE.put("Furnace", "minecraft:furnace"); -+ TILE_ID_UPDATE.put("Hopper", "minecraft:hopper"); -+ TILE_ID_UPDATE.put("MobSpawner", "minecraft:mob_spawner"); -+ TILE_ID_UPDATE.put("Music", "minecraft:noteblock"); -+ TILE_ID_UPDATE.put("Piston", "minecraft:piston"); -+ TILE_ID_UPDATE.put("RecordPlayer", "minecraft:jukebox"); -+ TILE_ID_UPDATE.put("Sign", "minecraft:sign"); -+ TILE_ID_UPDATE.put("Skull", "minecraft:skull"); -+ TILE_ID_UPDATE.put("Structure", "minecraft:structure_block"); -+ TILE_ID_UPDATE.put("Trap", "minecraft:dispenser"); -+ } -+ -+ private static void registerInventory(final String id) { -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, id, new DataWalkerItemLists("Items")); -+ } -+ -+ public static void register() { -+ MCTypeRegistry.TILE_ENTITY.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final String id = data.getString("id"); -+ if (id == null) { -+ return null; -+ } -+ -+ data.setString("id", TILE_ID_UPDATE.getOrDefault(id, id)); -+ return null; -+ } -+ }); -+ -+ -+ MCTypeRegistry.TILE_ENTITY.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convert(MCTypeRegistry.DATA_COMPONENTS, data, "components", fromVersion, toVersion); -+ return null; -+ }); -+ registerInventory( "minecraft:furnace"); -+ registerInventory( "minecraft:chest"); -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:jukebox", new DataWalkerItems("RecordItem")); -+ registerInventory("minecraft:dispenser"); -+ registerInventory("minecraft:dropper"); -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:mob_spawner", (final MapType data, final long fromVersion, final long toVersion) -> { -+ MCTypeRegistry.UNTAGGED_SPAWNER.convert(data, fromVersion, toVersion); -+ return null; -+ }); -+ registerInventory("minecraft:brewing_stand"); -+ registerInventory("minecraft:hopper"); -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:flower_pot", new DataWalkerItemNames("Item")); -+ MCTypeRegistry.TILE_ENTITY.addWalker( -+ VERSION, "minecraft:command_block", -+ new DataWalkerTypePaths<>(MCTypeRegistry.DATACONVERTER_CUSTOM_TYPE_COMMAND, "Command") -+ ); -+ -+ MCTypeRegistry.ITEM_STACK.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convert(MCTypeRegistry.ITEM_NAME, data, "id", fromVersion, toVersion); -+ -+ final MapType tag = data.getMap("tag"); -+ if (tag == null) { -+ return null; -+ } -+ -+ // only things here are in tag, if changed update if above -+ -+ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, tag, "Items", fromVersion, toVersion); -+ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, tag, "ChargedProjectiles", fromVersion, toVersion); -+ -+ MapType entityTag = tag.getMap("EntityTag"); -+ if (entityTag != null) { -+ final String itemId = data.getString("id"); -+ final String entityId; -+ if (itemId != null && itemId.contains("_spawn_egg")) { -+ // V1451 changes spawn eggs to have the sub entity id be a part of the item id, but of course Mojang never -+ // bothered to write in logic to set the sub entity id, so we have to. -+ // format is ALWAYS :_spawn_egg post flattening -+ entityId = itemId.substring(0, itemId.indexOf("_spawn_egg")); -+ } else { -+ final Long2ObjectArraySortedMap mappingByVersion = ITEM_ID_TO_ENTITY_ID.get(itemId); -+ final String mapped = mappingByVersion == null ? null : mappingByVersion.getFloor(fromVersion); -+ entityId = mapped == null ? entityTag.getString("id") : mapped; -+ } -+ -+ if (entityId == null) { -+ if (!"minecraft:air".equals(itemId)) { -+ LOGGER.warn("Unable to resolve Entity for ItemStack (V704): " + itemId); -+ } -+ } else { -+ if (!entityTag.hasKey("id", ObjectType.STRING)) { -+ entityTag.setString("id", entityId); -+ } -+ } -+ -+ final MapType replace = MCTypeRegistry.ENTITY.convert(entityTag, fromVersion, toVersion); -+ -+ if (replace != null) { -+ entityTag = replace; -+ tag.setMap("EntityTag", entityTag); -+ } -+ } -+ -+ MapType blockEntityTag = tag.getMap("BlockEntityTag"); -+ if (blockEntityTag != null) { -+ final String itemId = data.getString("id"); -+ final String entityId; -+ if ("minecraft:suspicious_sand".equals(itemId) && fromVersion < V3438.VERSION) { -+ // renamed after this version, and since the id is a mapping to just string we need to special case this -+ entityId = "minecraft:suspicious_sand"; -+ } else { -+ entityId = ITEM_ID_TO_TILE_ENTITY_ID.get(itemId); -+ } -+ -+ if (entityId == null) { -+ if (!"minecraft:air".equals(itemId)) { -+ LOGGER.warn("Unable to resolve BlockEntity for ItemStack (V704): " + itemId); -+ } -+ } else { -+ if (!blockEntityTag.hasKey("id", ObjectType.STRING)) { -+ blockEntityTag.setString("id", entityId); -+ } -+ } -+ final MapType replace = MCTypeRegistry.TILE_ENTITY.convert(blockEntityTag, fromVersion, toVersion); -+ if (replace != null) { -+ blockEntityTag = replace; -+ tag.setMap("BlockEntityTag", blockEntityTag); -+ } -+ } -+ -+ WalkerUtils.convertList(MCTypeRegistry.BLOCK_NAME, tag, "CanDestroy", fromVersion, toVersion); -+ WalkerUtils.convertList(MCTypeRegistry.BLOCK_NAME, tag, "CanPlaceOn", fromVersion, toVersion); -+ -+ return null; -+ }); -+ -+ // Enforce namespace for ids -+ MCTypeRegistry.TILE_ENTITY.addStructureHook(VERSION, new DataHookEnforceNamespacedID()); -+ } -+ -+ private V704() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V705.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V705.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e0efac6a303d4c9623e03acdf07f89c2cacc9f04 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V705.java -@@ -0,0 +1,221 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.entity.ConverterAbstractEntityRename; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.hooks.DataHookEnforceNamespacedID; -+import ca.spottedleaf.dataconverter.minecraft.hooks.DataHookValueTypeEnforceNamespaced; -+import ca.spottedleaf.dataconverter.minecraft.walkers.block_name.DataWalkerBlockNames; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; -+import ca.spottedleaf.dataconverter.minecraft.walkers.tile_entity.DataWalkerTileEntities; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.MapType; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V705 { -+ -+ public static final int VERSION = MCVersions.V1_10_2 + 193; -+ -+ private static final Map ENTITY_ID_UPDATE = new HashMap<>(); -+ static { -+ ENTITY_ID_UPDATE.put("AreaEffectCloud", "minecraft:area_effect_cloud"); -+ ENTITY_ID_UPDATE.put("ArmorStand", "minecraft:armor_stand"); -+ ENTITY_ID_UPDATE.put("Arrow", "minecraft:arrow"); -+ ENTITY_ID_UPDATE.put("Bat", "minecraft:bat"); -+ ENTITY_ID_UPDATE.put("Blaze", "minecraft:blaze"); -+ ENTITY_ID_UPDATE.put("Boat", "minecraft:boat"); -+ ENTITY_ID_UPDATE.put("CaveSpider", "minecraft:cave_spider"); -+ ENTITY_ID_UPDATE.put("Chicken", "minecraft:chicken"); -+ ENTITY_ID_UPDATE.put("Cow", "minecraft:cow"); -+ ENTITY_ID_UPDATE.put("Creeper", "minecraft:creeper"); -+ ENTITY_ID_UPDATE.put("Donkey", "minecraft:donkey"); -+ ENTITY_ID_UPDATE.put("DragonFireball", "minecraft:dragon_fireball"); -+ ENTITY_ID_UPDATE.put("ElderGuardian", "minecraft:elder_guardian"); -+ ENTITY_ID_UPDATE.put("EnderCrystal", "minecraft:ender_crystal"); -+ ENTITY_ID_UPDATE.put("EnderDragon", "minecraft:ender_dragon"); -+ ENTITY_ID_UPDATE.put("Enderman", "minecraft:enderman"); -+ ENTITY_ID_UPDATE.put("Endermite", "minecraft:endermite"); -+ ENTITY_ID_UPDATE.put("EyeOfEnderSignal", "minecraft:eye_of_ender_signal"); -+ ENTITY_ID_UPDATE.put("FallingSand", "minecraft:falling_block"); -+ ENTITY_ID_UPDATE.put("Fireball", "minecraft:fireball"); -+ ENTITY_ID_UPDATE.put("FireworksRocketEntity", "minecraft:fireworks_rocket"); -+ ENTITY_ID_UPDATE.put("Ghast", "minecraft:ghast"); -+ ENTITY_ID_UPDATE.put("Giant", "minecraft:giant"); -+ ENTITY_ID_UPDATE.put("Guardian", "minecraft:guardian"); -+ ENTITY_ID_UPDATE.put("Horse", "minecraft:horse"); -+ ENTITY_ID_UPDATE.put("Husk", "minecraft:husk"); -+ ENTITY_ID_UPDATE.put("Item", "minecraft:item"); -+ ENTITY_ID_UPDATE.put("ItemFrame", "minecraft:item_frame"); -+ ENTITY_ID_UPDATE.put("LavaSlime", "minecraft:magma_cube"); -+ ENTITY_ID_UPDATE.put("LeashKnot", "minecraft:leash_knot"); -+ ENTITY_ID_UPDATE.put("MinecartChest", "minecraft:chest_minecart"); -+ ENTITY_ID_UPDATE.put("MinecartCommandBlock", "minecraft:commandblock_minecart"); -+ ENTITY_ID_UPDATE.put("MinecartFurnace", "minecraft:furnace_minecart"); -+ ENTITY_ID_UPDATE.put("MinecartHopper", "minecraft:hopper_minecart"); -+ ENTITY_ID_UPDATE.put("MinecartRideable", "minecraft:minecart"); -+ ENTITY_ID_UPDATE.put("MinecartSpawner", "minecraft:spawner_minecart"); -+ ENTITY_ID_UPDATE.put("MinecartTNT", "minecraft:tnt_minecart"); -+ ENTITY_ID_UPDATE.put("Mule", "minecraft:mule"); -+ ENTITY_ID_UPDATE.put("MushroomCow", "minecraft:mooshroom"); -+ ENTITY_ID_UPDATE.put("Ozelot", "minecraft:ocelot"); -+ ENTITY_ID_UPDATE.put("Painting", "minecraft:painting"); -+ ENTITY_ID_UPDATE.put("Pig", "minecraft:pig"); -+ ENTITY_ID_UPDATE.put("PigZombie", "minecraft:zombie_pigman"); -+ ENTITY_ID_UPDATE.put("PolarBear", "minecraft:polar_bear"); -+ ENTITY_ID_UPDATE.put("PrimedTnt", "minecraft:tnt"); -+ ENTITY_ID_UPDATE.put("Rabbit", "minecraft:rabbit"); -+ ENTITY_ID_UPDATE.put("Sheep", "minecraft:sheep"); -+ ENTITY_ID_UPDATE.put("Shulker", "minecraft:shulker"); -+ ENTITY_ID_UPDATE.put("ShulkerBullet", "minecraft:shulker_bullet"); -+ ENTITY_ID_UPDATE.put("Silverfish", "minecraft:silverfish"); -+ ENTITY_ID_UPDATE.put("Skeleton", "minecraft:skeleton"); -+ ENTITY_ID_UPDATE.put("SkeletonHorse", "minecraft:skeleton_horse"); -+ ENTITY_ID_UPDATE.put("Slime", "minecraft:slime"); -+ ENTITY_ID_UPDATE.put("SmallFireball", "minecraft:small_fireball"); -+ ENTITY_ID_UPDATE.put("SnowMan", "minecraft:snowman"); -+ ENTITY_ID_UPDATE.put("Snowball", "minecraft:snowball"); -+ ENTITY_ID_UPDATE.put("SpectralArrow", "minecraft:spectral_arrow"); -+ ENTITY_ID_UPDATE.put("Spider", "minecraft:spider"); -+ ENTITY_ID_UPDATE.put("Squid", "minecraft:squid"); -+ ENTITY_ID_UPDATE.put("Stray", "minecraft:stray"); -+ ENTITY_ID_UPDATE.put("ThrownEgg", "minecraft:egg"); -+ ENTITY_ID_UPDATE.put("ThrownEnderpearl", "minecraft:ender_pearl"); -+ ENTITY_ID_UPDATE.put("ThrownExpBottle", "minecraft:xp_bottle"); -+ ENTITY_ID_UPDATE.put("ThrownPotion", "minecraft:potion"); -+ ENTITY_ID_UPDATE.put("Villager", "minecraft:villager"); -+ ENTITY_ID_UPDATE.put("VillagerGolem", "minecraft:villager_golem"); -+ ENTITY_ID_UPDATE.put("Witch", "minecraft:witch"); -+ ENTITY_ID_UPDATE.put("WitherBoss", "minecraft:wither"); -+ ENTITY_ID_UPDATE.put("WitherSkeleton", "minecraft:wither_skeleton"); -+ ENTITY_ID_UPDATE.put("WitherSkull", "minecraft:wither_skull"); -+ ENTITY_ID_UPDATE.put("Wolf", "minecraft:wolf"); -+ ENTITY_ID_UPDATE.put("XPOrb", "minecraft:xp_orb"); -+ ENTITY_ID_UPDATE.put("Zombie", "minecraft:zombie"); -+ ENTITY_ID_UPDATE.put("ZombieHorse", "minecraft:zombie_horse"); -+ ENTITY_ID_UPDATE.put("ZombieVillager", "minecraft:zombie_villager"); -+ } -+ -+ private static void registerMob(final String id) { -+ V100.registerEquipment(VERSION, id); -+ } -+ -+ private static void registerThrowableProjectile(final String id) { -+ MCTypeRegistry.ENTITY.addWalker(VERSION, id, new DataWalkerBlockNames("inTile")); -+ } -+ -+ public static void register() { -+ ConverterAbstractEntityRename.register(VERSION, ENTITY_ID_UPDATE::get); -+ -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:area_effect_cloud", new DataWalkerTypePaths<>(MCTypeRegistry.PARTICLE, "Particle")); -+ registerMob("minecraft:armor_stand"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:arrow", new DataWalkerBlockNames("inTile")); -+ registerMob("minecraft:bat"); -+ registerMob("minecraft:blaze"); -+ registerMob("minecraft:cave_spider"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:chest_minecart", new DataWalkerBlockNames("DisplayTile")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:chest_minecart", new DataWalkerItemLists("Items")); -+ registerMob("minecraft:chicken"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:commandblock_minecart", new DataWalkerBlockNames("DisplayTile")); -+ registerMob("minecraft:cow"); -+ registerMob("minecraft:creeper"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:donkey", new DataWalkerItemLists("Items")); -+ V100.registerEquipment(VERSION, "minecraft:donkey"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:donkey", new DataWalkerItems("SaddleItem")); -+ registerThrowableProjectile("minecraft:egg"); -+ registerMob("minecraft:elder_guardian"); -+ registerMob("minecraft:ender_dragon"); -+ V100.registerEquipment(VERSION, "minecraft:enderman"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:enderman", new DataWalkerBlockNames("carried")); -+ registerMob("minecraft:endermite"); -+ registerThrowableProjectile("minecraft:ender_pearl"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:falling_block", new DataWalkerBlockNames("Block")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:falling_block", new DataWalkerTileEntities("TileEntityData")); -+ registerThrowableProjectile("minecraft:fireball"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:fireworks_rocket", new DataWalkerItems("FireworksItem")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:furnace_minecart", new DataWalkerBlockNames("DisplayTile")); -+ registerMob("minecraft:ghast"); -+ registerMob("minecraft:giant"); -+ registerMob("minecraft:guardian"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:hopper_minecart", new DataWalkerBlockNames("DisplayTile")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:hopper_minecart", new DataWalkerItemLists("Items")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:horse", new DataWalkerItems("ArmorItem", "SaddleItem")); -+ V100.registerEquipment(VERSION, "minecraft:horse"); -+ registerMob("minecraft:husk"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:item", new DataWalkerItems("Item")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:item_frame", new DataWalkerItems("Item")); -+ registerMob("minecraft:magma_cube"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:minecart", new DataWalkerBlockNames("DisplayTile")); -+ registerMob("minecraft:mooshroom"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:mule", new DataWalkerItemLists("Items")); -+ V100.registerEquipment(VERSION, "minecraft:mule"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:mule", new DataWalkerItems("SaddleItem")); -+ registerMob("minecraft:ocelot"); -+ registerMob("minecraft:pig"); -+ registerMob("minecraft:polar_bear"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:potion", new DataWalkerItems("Potion")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:potion", new DataWalkerBlockNames("inTile")); -+ registerMob("minecraft:rabbit"); -+ registerMob("minecraft:sheep"); -+ registerMob("minecraft:shulker"); -+ registerMob("minecraft:silverfish"); -+ registerMob("minecraft:skeleton"); -+ V100.registerEquipment(VERSION, "minecraft:skeleton_horse"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:skeleton_horse", new DataWalkerItems("SaddleItem")); -+ registerMob("minecraft:slime"); -+ registerThrowableProjectile("minecraft:small_fireball"); -+ registerThrowableProjectile("minecraft:snowball"); -+ registerMob("minecraft:snowman"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:spawner_minecart", new DataWalkerBlockNames("DisplayTile")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:spawner_minecart", (final MapType data, final long fromVersion, final long toVersion) -> { -+ MCTypeRegistry.UNTAGGED_SPAWNER.convert(data, fromVersion, toVersion); -+ return null; -+ }); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:spectral_arrow", new DataWalkerBlockNames("inTile")); -+ registerMob("minecraft:spider"); -+ registerMob("minecraft:squid"); -+ registerMob("minecraft:stray"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:tnt_minecart", new DataWalkerBlockNames("DisplayTile")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:villager", (final MapType data, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, data, "Inventory", fromVersion, toVersion); -+ -+ WalkerUtils.convertList(MCTypeRegistry.VILLAGER_TRADE, data.getMap("Offers"), "Recipes", fromVersion, toVersion); -+ -+ return null; -+ }); -+ V100.registerEquipment(VERSION, "minecraft:villager"); -+ registerMob("minecraft:villager_golem"); -+ registerMob("minecraft:witch"); -+ registerMob("minecraft:wither"); -+ registerMob("minecraft:wither_skeleton"); -+ registerThrowableProjectile("minecraft:wither_skull"); -+ registerMob("minecraft:wolf"); -+ registerThrowableProjectile("minecraft:xp_bottle"); -+ registerMob("minecraft:zombie"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:zombie_horse", new DataWalkerItems("SaddleItem")); -+ V100.registerEquipment(VERSION, "minecraft:zombie_horse"); -+ registerMob("minecraft:zombie_pigman"); -+ registerMob("minecraft:zombie_villager"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:zombie_villager", (final MapType data, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convertList(MCTypeRegistry.VILLAGER_TRADE, data.getMap("Offers"), "Recipes", fromVersion, toVersion); -+ -+ return null; -+ }); -+ registerMob("minecraft:evocation_illager"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:llama", new DataWalkerItemLists("Items")); -+ V100.registerEquipment(VERSION, "minecraft:llama"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "minecraft:llama", new DataWalkerItems("SaddleItem", "DecorItem")); -+ registerMob("minecraft:vex"); -+ registerMob("minecraft:vindication_illager"); -+ // Don't need to re-register itemstack walker, the V704 will correctly choose the right id for armorstand based on -+ // the source version -+ -+ // Enforce namespace for ids -+ MCTypeRegistry.ENTITY.addStructureHook(VERSION, new DataHookEnforceNamespacedID()); -+ MCTypeRegistry.ENTITY_NAME.addStructureHook(VERSION, new DataHookValueTypeEnforceNamespaced()); -+ } -+ -+ private V705() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V804.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V804.java -new file mode 100644 -index 0000000000000000000000000000000000000000..81a2006d5e2059df0979c6380a16255767bcd89a ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V804.java -@@ -0,0 +1,59 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V804 { -+ -+ private static final int VERSION = MCVersions.V16W35A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:banner", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType tag = data.getMap("tag"); -+ if (tag == null) { -+ return null; -+ } -+ -+ final MapType blockEntity = tag.getMap("BlockEntityTag"); -+ if (blockEntity == null) { -+ return null; -+ } -+ -+ if (!blockEntity.hasKey("Base", ObjectType.NUMBER)) { -+ return null; -+ } -+ -+ data.setShort("Damage", (short)(blockEntity.getShort("Base") & 15)); -+ -+ final MapType display = tag.getMap("display"); -+ if (display != null) { -+ final ListType lore = display.getList("Lore", ObjectType.STRING); -+ if (lore != null) { -+ if (lore.size() == 1 && "(+NBT)".equals(lore.getString(0))) { -+ return null; -+ } -+ } -+ } -+ -+ blockEntity.remove("Base"); -+ if (blockEntity.isEmpty()) { -+ tag.remove("BlockEntityTag"); -+ } -+ -+ if (tag.isEmpty()) { -+ data.remove("tag"); -+ } -+ -+ return null; -+ } -+ }); -+ } -+ -+ private V804() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V806.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V806.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f4ebe856d03d9837214e9a1c93f1b1e79aa7bb08 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V806.java -@@ -0,0 +1,39 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.Types; -+ -+public final class V806 { -+ -+ private static final int VERSION = MCVersions.V16W36A + 1; -+ -+ public static void register() { -+ final DataConverter, MapType> potionWaterUpdater = new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ MapType tag = data.getMap("tag"); -+ if (tag == null) { -+ tag = Types.NBT.createEmptyMap(); -+ data.setMap("tag", tag); -+ } -+ -+ if (!tag.hasKey("Potion", ObjectType.STRING)) { -+ tag.setString("Potion", "minecraft:water"); -+ } -+ -+ return null; -+ } -+ }; -+ -+ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:potion", potionWaterUpdater); -+ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:splash_potion", potionWaterUpdater); -+ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:lingering_potion", potionWaterUpdater); -+ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:tipped_arrow", potionWaterUpdater); -+ } -+ -+ private V806() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V808.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V808.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c6b6038255e16bd15873bb7fe596b721fcec365e ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V808.java -@@ -0,0 +1,29 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V808 { -+ -+ private static final int VERSION = MCVersions.V16W38A + 1; -+ -+ public static void register() { -+ MCTypeRegistry.ENTITY.addConverterForId("minecraft:shulker", new DataConverter<>(VERSION, 1) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ if (!data.hasKey("Color", ObjectType.NUMBER)) { -+ data.setByte("Color", (byte)10); -+ } -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "minecraft:shulker_box", new DataWalkerItemLists("Items")); -+ } -+ -+ private V808() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V813.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V813.java -new file mode 100644 -index 0000000000000000000000000000000000000000..68810919e168f36de160033aa659060487d94bd8 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V813.java -@@ -0,0 +1,64 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class V813 { -+ -+ private static final int VERSION = MCVersions.V16W40A; -+ -+ private static final String[] SHULKER_ID_BY_COLOUR = new String[] { -+ "minecraft:white_shulker_box", -+ "minecraft:orange_shulker_box", -+ "minecraft:magenta_shulker_box", -+ "minecraft:light_blue_shulker_box", -+ "minecraft:yellow_shulker_box", -+ "minecraft:lime_shulker_box", -+ "minecraft:pink_shulker_box", -+ "minecraft:gray_shulker_box", -+ "minecraft:silver_shulker_box", -+ "minecraft:cyan_shulker_box", -+ "minecraft:purple_shulker_box", -+ "minecraft:blue_shulker_box", -+ "minecraft:brown_shulker_box", -+ "minecraft:green_shulker_box", -+ "minecraft:red_shulker_box", -+ "minecraft:black_shulker_box" -+ }; -+ -+ public static void register() { -+ MCTypeRegistry.ITEM_STACK.addConverterForId("minecraft:shulker_box", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final MapType tag = data.getMap("tag"); -+ if (tag == null) { -+ return null; -+ } -+ -+ final MapType blockEntity = tag.getMap("BlockEntityTag"); -+ if (blockEntity == null) { -+ return null; -+ } -+ -+ final int color = blockEntity.getInt("Color"); -+ blockEntity.remove("Color"); -+ -+ data.setString("id", SHULKER_ID_BY_COLOUR[color % SHULKER_ID_BY_COLOUR.length]); -+ -+ return null; -+ } -+ }); -+ -+ MCTypeRegistry.TILE_ENTITY.addConverterForId("minecraft:shulker_box", new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ data.remove("Color"); -+ return null; -+ } -+ }); -+ } -+ -+ private V813() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V816.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V816.java -new file mode 100644 -index 0000000000000000000000000000000000000000..dc9fba23654262b1489e4f8056a7f4b222ab1179 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V816.java -@@ -0,0 +1,27 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.converters.DataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.types.MapType; -+import java.util.Locale; -+ -+public final class V816 { -+ -+ private static final int VERSION = MCVersions.V16W43A; -+ -+ public static void register() { -+ MCTypeRegistry.OPTIONS.addStructureConverter(new DataConverter<>(VERSION) { -+ @Override -+ public MapType convert(final MapType data, final long sourceVersion, final long toVersion) { -+ final String lang = data.getString("lang"); -+ if (lang != null) { -+ data.setString("lang", lang.toLowerCase(Locale.ROOT)); -+ } -+ return null; -+ } -+ }); -+ } -+ -+ private V816() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V820.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V820.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0d59cb380e625bb2658216d4a6cb8faebdd147c5 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V820.java -@@ -0,0 +1,21 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.itemname.ConverterAbstractItemRename; -+import com.google.common.collect.ImmutableMap; -+import java.util.HashMap; -+ -+public final class V820 { -+ -+ private static final int VERSION = MCVersions.V1_11 + 1; -+ -+ public static void register() { -+ ConverterAbstractItemRename.register(VERSION, new HashMap<>( -+ ImmutableMap.of( -+ "minecraft:totem", "minecraft:totem_of_undying" -+ ) -+ )::get); -+ } -+ -+ private V820() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V99.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V99.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f0e26849e28ce7ce362927ec81b281e51bd1e591 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/versions/V99.java -@@ -0,0 +1,363 @@ -+package ca.spottedleaf.dataconverter.minecraft.versions; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.HelperItemNameV102; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.hooks.DataHookEnforceNamespacedID; -+import ca.spottedleaf.dataconverter.minecraft.hooks.DataHookValueTypeEnforceNamespaced; -+import ca.spottedleaf.dataconverter.minecraft.walkers.block_name.DataWalkerBlockNames; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItemLists; -+import ca.spottedleaf.dataconverter.minecraft.walkers.item_name.DataWalkerItemNames; -+import ca.spottedleaf.dataconverter.minecraft.walkers.itemstack.DataWalkerItems; -+import ca.spottedleaf.dataconverter.minecraft.walkers.tile_entity.DataWalkerTileEntities; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import com.mojang.logging.LogUtils; -+import org.slf4j.Logger; -+import java.util.HashMap; -+import java.util.Map; -+ -+public final class V99 { -+ -+ // Structure for all data before data upgrading was added to minecraft (pre 15w32a) -+ -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ -+ public static final int VERSION = MCVersions.V15W32A - 1; -+ -+ private static final Map ITEM_ID_TO_TILE_ENTITY_ID = new HashMap<>(); -+ static { -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:furnace", "Furnace"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:lit_furnace", "Furnace"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:chest", "Chest"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:trapped_chest", "Chest"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:ender_chest", "EnderChest"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:jukebox", "RecordPlayer"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:dispenser", "Trap"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:dropper", "Dropper"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:sign", "Sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:mob_spawner", "MobSpawner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:noteblock", "Music"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:brewing_stand", "Cauldron"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:enhanting_table", "EnchantTable"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:command_block", "CommandBlock"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:beacon", "Beacon"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:skull", "Skull"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:daylight_detector", "DLDetector"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:hopper", "Hopper"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:banner", "Banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:flower_pot", "FlowerPot"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:repeating_command_block", "CommandBlock"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:chain_command_block", "CommandBlock"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:standing_sign", "Sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:wall_sign", "Sign"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:piston_head", "Piston"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:daylight_detector_inverted", "DLDetector"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:unpowered_comparator", "Comparator"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:powered_comparator", "Comparator"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:wall_banner", "Banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:standing_banner", "Banner"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:structure_block", "Structure"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:end_portal", "Airportal"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:end_gateway", "EndGateway"); -+ ITEM_ID_TO_TILE_ENTITY_ID.put("minecraft:shield", "Banner"); -+ } -+ -+ private static void registerMob(final String id) { -+ MCTypeRegistry.ENTITY.addWalker(VERSION, id, new DataWalkerItemLists("Equipment")); -+ } -+ -+ private static void registerProjectile(final String id) { -+ MCTypeRegistry.ENTITY.addWalker(VERSION, id, new DataWalkerBlockNames("inTile")); -+ } -+ -+ private static void registerInventory(final String id) { -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, id, new DataWalkerItemLists("Items")); -+ } -+ -+ public static void register() { -+ // entities -+ MCTypeRegistry.ENTITY.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convert(MCTypeRegistry.ENTITY, data, "Riding", fromVersion, toVersion); -+ -+ return null; -+ }); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "Item", new DataWalkerItems("Item")); -+ registerProjectile("ThrownEgg"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "Arrow", new DataWalkerBlockNames("inTile")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "TippedArrow", new DataWalkerBlockNames("inTile")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "SpectralArrow", new DataWalkerBlockNames("inTile")); -+ registerProjectile("Snowball"); -+ registerProjectile("Fireball"); -+ registerProjectile("SmallFireball"); -+ registerProjectile("ThrownEnderpearl"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "ThrownPotion", new DataWalkerBlockNames("inTile")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "ThrownPotion", new DataWalkerItems("Potion")); -+ registerProjectile("ThrownExpBottle"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "ItemFrame", new DataWalkerItems("Item")); -+ registerProjectile("WitherSkull"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "FallingSand", new DataWalkerBlockNames("Block")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "FallingSand", new DataWalkerTileEntities("TileEntityData")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "FireworksRocketEntity", new DataWalkerItems("FireworksItem")); -+ // Note: Minecart is the generic entity. It can be subtyped via an int to become one of the specific minecarts -+ // (i.e rideable, chest, furnace, tnt, etc) -+ // Because of this, we add all walkers to the generic type, even though they might not be needed. -+ // Vanilla does not make the generic minecart convert spawners, but we do. -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "Minecart", new DataWalkerBlockNames("DisplayTile")); // for all minecart types -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "Minecart", new DataWalkerItemLists("Items")); // for chest types -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "Minecart", (final MapType data, final long fromVersion, final long toVersion) -> { -+ MCTypeRegistry.UNTAGGED_SPAWNER.convert(data, fromVersion, toVersion); -+ return null; -+ }); // for spawner type -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "MinecartRideable", new DataWalkerBlockNames("DisplayTile")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "MinecartChest", new DataWalkerBlockNames("DisplayTile")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "MinecartChest", new DataWalkerItemLists("Items")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "MinecartFurnace", new DataWalkerBlockNames("DisplayTile")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "MinecartTNT", new DataWalkerBlockNames("DisplayTile")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "MinecartSpawner", new DataWalkerBlockNames("DisplayTile")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "MinecartSpawner", (final MapType data, final long fromVersion, final long toVersion) -> { -+ MCTypeRegistry.UNTAGGED_SPAWNER.convert(data, fromVersion, toVersion); -+ return null; -+ }); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "MinecartHopper", new DataWalkerBlockNames("DisplayTile")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "MinecartHopper", new DataWalkerItemLists("Items")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "MinecartCommandBlock", new DataWalkerBlockNames("DisplayTile")); -+ registerMob("ArmorStand"); -+ registerMob("Creeper"); -+ registerMob("Skeleton"); -+ registerMob("Spider"); -+ registerMob("Giant"); -+ registerMob("Zombie"); -+ registerMob("Slime"); -+ registerMob("Ghast"); -+ registerMob("PigZombie"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "Enderman", new DataWalkerBlockNames("carried")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "Enderman", new DataWalkerItemLists("Equipment")); -+ registerMob("CaveSpider"); -+ registerMob("Silverfish"); -+ registerMob("Blaze"); -+ registerMob("LavaSlime"); -+ registerMob("EnderDragon"); -+ registerMob("WitherBoss"); -+ registerMob("Bat"); -+ registerMob("Witch"); -+ registerMob("Endermite"); -+ registerMob("Guardian"); -+ registerMob("Pig"); -+ registerMob("Sheep"); -+ registerMob("Cow"); -+ registerMob("Chicken"); -+ registerMob("Squid"); -+ registerMob("Wolf"); -+ registerMob("MushroomCow"); -+ registerMob("SnowMan"); -+ registerMob("Ozelot"); -+ registerMob("VillagerGolem"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "EntityHorse", new DataWalkerItemLists("Items", "Equipment")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "EntityHorse", new DataWalkerItems("ArmorItem", "SaddleItem")); -+ registerMob("Rabbit"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "Villager", new DataWalkerItemLists("Inventory", "Equipment")); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "Villager", (final MapType data, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convertList(MCTypeRegistry.VILLAGER_TRADE, data.getMap("Offers"), "Recipes", fromVersion, toVersion); -+ -+ return null; -+ }); -+ registerMob("Shulker"); -+ MCTypeRegistry.ENTITY.addWalker(VERSION, "AreaEffectCloud", new DataWalkerTypePaths<>(MCTypeRegistry.PARTICLE, "Particle")); -+ -+ // tile entities -+ MCTypeRegistry.TILE_ENTITY.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convert(MCTypeRegistry.DATA_COMPONENTS, data, "components", fromVersion, toVersion); -+ -+ return null; -+ }); -+ -+ // Inventory -> new DataWalkerItemLists("Items") -+ registerInventory("Furnace"); -+ registerInventory("Chest"); -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "RecordPlayer", new DataWalkerItems("RecordItem")); -+ registerInventory("Trap"); -+ registerInventory("Dropper"); -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "MobSpawner", (final MapType data, final long fromVersion, final long toVersion) -> { -+ MCTypeRegistry.UNTAGGED_SPAWNER.convert(data, fromVersion, toVersion); -+ return null; -+ }); -+ registerInventory("Cauldron"); -+ registerInventory("Hopper"); -+ // Note: Vanilla does not properly handle this case for FlowerPot, it will not convert int ids! -+ MCTypeRegistry.TILE_ENTITY.addWalker(VERSION, "FlowerPot", new DataWalkerItemNames("Item")); -+ MCTypeRegistry.TILE_ENTITY.addWalker( -+ VERSION, "Control", -+ new DataWalkerTypePaths<>(MCTypeRegistry.DATACONVERTER_CUSTOM_TYPE_COMMAND, "Command") -+ ); -+ -+ // rest -+ -+ MCTypeRegistry.ITEM_STACK.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convert(MCTypeRegistry.ITEM_NAME, data, "id", fromVersion, toVersion); -+ -+ final MapType tag = data.getMap("tag"); -+ if (tag == null) { -+ return null; -+ } -+ -+ // only things here are in tag, if changed update if above -+ -+ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, tag, "Items", fromVersion, toVersion); -+ WalkerUtils.convertList(MCTypeRegistry.ITEM_STACK, tag, "ChargedProjectiles", fromVersion, toVersion); -+ -+ MapType entityTag = tag.getMap("EntityTag"); -+ if (entityTag != null) { -+ String itemId = getStringId(data.getString("id")); -+ final String entityId; -+ if ("minecraft:armor_stand".equals(itemId)) { -+ // The check for version id is removed here. For whatever reason, the legacy -+ // data converters used entity id "minecraft:armor_stand" when version was greater-than 514, -+ // but entity ids were not namespaced until V705! So somebody fucked up the legacy converters. -+ // DFU agrees with my analysis here, it will only set the entityId here to the namespaced variant -+ // with the V705 schema. -+ entityId = "ArmorStand"; -+ } else if ("minecraft:item_frame".equals(itemId)) { -+ // add missing item_frame entity id -+ entityId = "ItemFrame"; -+ } else if ("minecraft:painting".equals(itemId)) { -+ entityId = "Painting"; -+ } else { -+ entityId = entityTag.getString("id"); -+ } -+ -+ final boolean removeId; -+ if (entityId == null) { -+ if (!"minecraft:air".equals(itemId)) { -+ LOGGER.warn("Unable to resolve Entity for ItemStack (V99): " + data.getGeneric("id")); -+ } -+ removeId = false; -+ } else { -+ removeId = !entityTag.hasKey("id", ObjectType.STRING); -+ if (removeId) { -+ entityTag.setString("id", entityId); -+ } -+ } -+ -+ final MapType replace = MCTypeRegistry.ENTITY.convert(entityTag, fromVersion, toVersion); -+ -+ if (replace != null) { -+ entityTag = replace; -+ tag.setMap("EntityTag", entityTag); -+ } -+ if (removeId) { -+ entityTag.remove("id"); -+ } -+ } -+ -+ MapType blockEntityTag = tag.getMap("BlockEntityTag"); -+ if (blockEntityTag != null) { -+ final String itemId = getStringId(data.getString("id")); -+ final String entityId = ITEM_ID_TO_TILE_ENTITY_ID.get(itemId); -+ final boolean removeId; -+ if (entityId == null) { -+ if (!"minecraft:air".equals(itemId)) { -+ LOGGER.warn("Unable to resolve BlockEntity for ItemStack (V99): " + data.getGeneric("id")); -+ } -+ removeId = false; -+ } else { -+ removeId = !blockEntityTag.hasKey("id", ObjectType.STRING); -+ blockEntityTag.setString("id", entityId); -+ } -+ final MapType replace = MCTypeRegistry.TILE_ENTITY.convert(blockEntityTag, fromVersion, toVersion); -+ if (replace != null) { -+ blockEntityTag = replace; -+ tag.setMap("BlockEntityTag", blockEntityTag); -+ } -+ if (removeId) { -+ blockEntityTag.remove("id"); -+ } -+ } -+ -+ WalkerUtils.convertList(MCTypeRegistry.BLOCK_NAME, tag, "CanDestroy", fromVersion, toVersion); -+ WalkerUtils.convertList(MCTypeRegistry.BLOCK_NAME, tag, "CanPlaceOn", fromVersion, toVersion); -+ -+ return null; -+ }); -+ -+ MCTypeRegistry.PLAYER.addStructureWalker(VERSION, new DataWalkerItemLists("Inventory", "EnderItems")); -+ -+ MCTypeRegistry.CHUNK.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ final MapType level = data.getMap("Level"); -+ if (level == null) { -+ return null; -+ } -+ -+ WalkerUtils.convertList(MCTypeRegistry.ENTITY, level, "Entities", fromVersion, toVersion); -+ WalkerUtils.convertList(MCTypeRegistry.TILE_ENTITY, level, "TileEntities", fromVersion, toVersion); -+ -+ final ListType tileTicks = level.getList("TileTicks", ObjectType.MAP); -+ if (tileTicks != null) { -+ for (int i = 0, len = tileTicks.size(); i < len; ++i) { -+ final MapType tileTick = tileTicks.getMap(i); -+ WalkerUtils.convert(MCTypeRegistry.BLOCK_NAME, tileTick, "i", fromVersion, toVersion); -+ } -+ } -+ -+ return null; -+ }); -+ -+ MCTypeRegistry.ENTITY_CHUNK.addStructureWalker(VERSION, (final MapType data, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convertList(MCTypeRegistry.ENTITY, data, "Entities", fromVersion, toVersion); -+ -+ return null; -+ }); -+ -+ MCTypeRegistry.SAVED_DATA_SCOREBOARD.addStructureWalker(VERSION, (final MapType root, final long fromVersion, final long toVersion) -> { -+ final MapType data = root.getMap("data"); -+ if (data == null) { -+ return null; -+ } -+ -+ WalkerUtils.convertList(MCTypeRegistry.OBJECTIVE, data, "Objectives", fromVersion, toVersion); -+ WalkerUtils.convertList(MCTypeRegistry.TEAM, data, "Teams", fromVersion, toVersion); -+ -+ return null; -+ }); -+ MCTypeRegistry.SAVED_DATA_STRUCTURE_FEATURE_INDICES.addStructureWalker(VERSION, (final MapType root, final long fromVersion, final long toVersion) -> { -+ final MapType data = root.getMap("data"); -+ if (data == null) { -+ return null; -+ } -+ -+ WalkerUtils.convertValues(MCTypeRegistry.STRUCTURE_FEATURE, data, "Features", fromVersion, toVersion); -+ -+ return null; -+ }); -+ -+ MCTypeRegistry.VILLAGER_TRADE.addStructureWalker(VERSION, (final MapType root, final long fromVersion, final long toVersion) -> { -+ WalkerUtils.convert(MCTypeRegistry.ITEM_STACK, root, "buy", fromVersion, toVersion); -+ WalkerUtils.convert(MCTypeRegistry.ITEM_STACK, root, "buyB", fromVersion, toVersion); -+ WalkerUtils.convert(MCTypeRegistry.ITEM_STACK, root, "sell", fromVersion, toVersion); -+ -+ return null; -+ }); -+ -+ // Enforce namespacing for ids -+ MCTypeRegistry.BLOCK_NAME.addStructureHook(VERSION, new DataHookValueTypeEnforceNamespaced()); -+ MCTypeRegistry.ITEM_NAME.addStructureHook(VERSION, new DataHookValueTypeEnforceNamespaced()); -+ MCTypeRegistry.ITEM_STACK.addStructureHook(VERSION, new DataHookEnforceNamespacedID()); -+ -+ // Entity is absent; the String form is not yet namespaced, unlike the above. -+ } -+ -+ private static String getStringId(final Object id) { -+ if (id instanceof String) { -+ return (String)id; -+ } else if (id instanceof Number) { -+ return HelperItemNameV102.getNameFromId(((Number)id).intValue()); -+ } else { -+ return null; -+ } -+ } -+ -+ private V99() {} -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/block_name/DataWalkerBlockNames.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/block_name/DataWalkerBlockNames.java -new file mode 100644 -index 0000000000000000000000000000000000000000..930e014858ef635ebe25f7f92dc81ba0eaac50a8 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/block_name/DataWalkerBlockNames.java -@@ -0,0 +1,11 @@ -+package ca.spottedleaf.dataconverter.minecraft.walkers.block_name; -+ -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; -+ -+public final class DataWalkerBlockNames extends DataWalkerTypePaths { -+ -+ public DataWalkerBlockNames(final String... paths) { -+ super(MCTypeRegistry.BLOCK_NAME, paths); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/game_event/GameEventListenerWalker.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/game_event/GameEventListenerWalker.java -new file mode 100644 -index 0000000000000000000000000000000000000000..64fc063748d4839d787a773d2c7258dcffc6bc21 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/game_event/GameEventListenerWalker.java -@@ -0,0 +1,26 @@ -+package ca.spottedleaf.dataconverter.minecraft.walkers.game_event; -+ -+import ca.spottedleaf.dataconverter.converters.datatypes.DataWalker; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.WalkerUtils; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class GameEventListenerWalker implements DataWalker> { -+ -+ @Override -+ public MapType walk(final MapType data, final long fromVersion, final long toVersion) { -+ final MapType listener = data.getMap("listener"); -+ if (listener == null) { -+ return null; -+ } -+ -+ final MapType event = listener.getMap("event"); -+ if (event == null) { -+ return null; -+ } -+ -+ WalkerUtils.convert(MCTypeRegistry.GAME_EVENT_NAME, event, "game_event", fromVersion, toVersion); -+ -+ return null; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerListPaths.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerListPaths.java -new file mode 100644 -index 0000000000000000000000000000000000000000..20c8efdb746c9d3b9d87bf991dc44e11e1ea697c ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerListPaths.java -@@ -0,0 +1,38 @@ -+package ca.spottedleaf.dataconverter.minecraft.walkers.generic; -+ -+import ca.spottedleaf.dataconverter.converters.datatypes.DataType; -+import ca.spottedleaf.dataconverter.converters.datatypes.DataWalker; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public class DataWalkerListPaths implements DataWalker> { -+ -+ protected final DataType type; -+ protected final String[] paths; -+ -+ public DataWalkerListPaths(final DataType type, final String... paths) { -+ this.type = type; -+ this.paths = paths; -+ } -+ -+ @Override -+ public final MapType walk(final MapType data, final long fromVersion, final long toVersion) { -+ final DataType type = this.type; -+ for (final String path : this.paths) { -+ final ListType list = data.getListUnchecked(path); -+ if (list == null) { -+ continue; -+ } -+ -+ for (int i = 0, len = list.size(); i < len; ++i) { -+ final Object current = list.getGeneric(i); -+ final Object converted = type.convert((T)current, fromVersion, toVersion); -+ if (converted != null) { -+ list.setGeneric(i, converted); -+ } -+ } -+ } -+ -+ return null; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerTypePaths.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerTypePaths.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4205546d7b2c4a07d23a017004989875b7beb3c6 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/DataWalkerTypePaths.java -@@ -0,0 +1,34 @@ -+package ca.spottedleaf.dataconverter.minecraft.walkers.generic; -+ -+import ca.spottedleaf.dataconverter.converters.datatypes.DataType; -+import ca.spottedleaf.dataconverter.converters.datatypes.DataWalker; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public class DataWalkerTypePaths implements DataWalker> { -+ -+ protected final DataType type; -+ protected final String[] paths; -+ -+ public DataWalkerTypePaths(final DataType type, final String... paths) { -+ this.type = type; -+ this.paths = paths; -+ } -+ -+ @Override -+ public final MapType walk(final MapType data, final long fromVersion, final long toVersion) { -+ for (final String path : this.paths) { -+ final Object current = data.getGeneric(path); -+ if (current == null) { -+ continue; -+ } -+ -+ final Object converted = this.type.convert((T)current, fromVersion, toVersion); -+ -+ if (converted != null) { -+ data.setGeneric(path, converted); -+ } -+ } -+ -+ return null; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/WalkerUtils.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/WalkerUtils.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4feedd9e48c3a85bd75b9c0a3b09c91fa9532a93 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/generic/WalkerUtils.java -@@ -0,0 +1,183 @@ -+package ca.spottedleaf.dataconverter.minecraft.walkers.generic; -+ -+import ca.spottedleaf.dataconverter.minecraft.converters.helpers.RenameHelper; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCDataType; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCValueType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import java.util.ArrayList; -+ -+public final class WalkerUtils { -+ -+ public static void convert(final MCDataType type, final MapType data, final String path, final long fromVersion, final long toVersion) { -+ if (data == null) { -+ return; -+ } -+ -+ final MapType map = data.getMap(path); -+ if (map != null) { -+ final MapType replace = type.convert(map, fromVersion, toVersion); -+ if (replace != null) { -+ data.setMap(path, replace); -+ } -+ } -+ } -+ -+ public static void convertList(final MCDataType type, final MapType data, final String path, final long fromVersion, final long toVersion) { -+ if (data == null) { -+ return; -+ } -+ -+ final ListType list = data.getList(path, ObjectType.MAP); -+ if (list != null) { -+ for (int i = 0, len = list.size(); i < len; ++i) { -+ final MapType replace = type.convert(list.getMap(i), fromVersion, toVersion); -+ if (replace != null) { -+ list.setMap(i, replace); -+ } -+ } -+ } -+ } -+ -+ public static void convertListPath(final MCDataType type, final MapType data, final String listPath, final String elementPath, -+ final long fromVersion, final long toVersion) { -+ if (data == null) { -+ return; -+ } -+ -+ final ListType list = data.getList(listPath, ObjectType.MAP); -+ if (list != null) { -+ for (int i = 0, len = list.size(); i < len; ++i) { -+ WalkerUtils.convert(type, list.getMap(i), elementPath, fromVersion, toVersion); -+ } -+ } -+ } -+ -+ public static void convertListPath(final MCDataType type, final MapType data, final String listPath, final String elementPath1, -+ final String elementPath2, final long fromVersion, final long toVersion) { -+ if (data == null) { -+ return; -+ } -+ -+ final ListType list = data.getList(listPath, ObjectType.MAP); -+ if (list != null) { -+ for (int i = 0, len = list.size(); i < len; ++i) { -+ WalkerUtils.convert(type, list.getMap(i).getMap(elementPath1), elementPath2, fromVersion, toVersion); -+ } -+ } -+ } -+ -+ public static void convert(final MCValueType type, final MapType data, final String path, final long fromVersion, final long toVersion) { -+ if (data == null) { -+ return; -+ } -+ -+ final Object value = data.getGeneric(path); -+ if (value != null) { -+ final Object converted = type.convert(value, fromVersion, toVersion); -+ if (converted != null) { -+ data.setGeneric(path, converted); -+ } -+ } -+ } -+ -+ public static void convert(final MCValueType type, final ListType data, final long fromVersion, final long toVersion) { -+ if (data == null) { -+ return; -+ } -+ -+ for (int i = 0, len = data.size(); i < len; ++i) { -+ final Object value = data.getGeneric(i); -+ final Object converted = type.convert(value, fromVersion, toVersion); -+ if (converted != null) { -+ data.setGeneric(i, converted); -+ } -+ } -+ } -+ -+ public static void convertList(final MCValueType type, final MapType data, final String path, final long fromVersion, final long toVersion) { -+ if (data == null) { -+ return; -+ } -+ -+ final ListType list = data.getListUnchecked(path); -+ if (list != null) { -+ convert(type, list, fromVersion, toVersion); -+ } -+ } -+ -+ public static void convertListPath(final MCValueType type, final MapType data, final String listPath, final String elementPath, -+ final long fromVersion, final long toVersion) { -+ if (data == null) { -+ return; -+ } -+ -+ final ListType list = data.getList(listPath, ObjectType.MAP); -+ if (list != null) { -+ for (int i = 0, len = list.size(); i < len; ++i) { -+ WalkerUtils.convert(type, list.getMap(i), elementPath, fromVersion, toVersion); -+ } -+ } -+ } -+ -+ public static void convertListPath(final MCValueType type, final MapType data, final String listPath, final String elementPath1, -+ final String elementPath2, final long fromVersion, final long toVersion) { -+ if (data == null) { -+ return; -+ } -+ -+ final ListType list = data.getList(listPath, ObjectType.MAP); -+ if (list != null) { -+ for (int i = 0, len = list.size(); i < len; ++i) { -+ WalkerUtils.convert(type, list.getMap(i).getMap(elementPath1), elementPath2, fromVersion, toVersion); -+ } -+ } -+ } -+ -+ public static void convertKeys(final MCValueType type, final MapType data, final String path, final long fromVersion, final long toVersion) { -+ if (data == null) { -+ return; -+ } -+ -+ final MapType map = data.getMap(path); -+ if (map != null) { -+ convertKeys(type, map, fromVersion, toVersion); -+ } -+ } -+ -+ public static void convertKeys(final MCValueType type, final MapType data, final long fromVersion, final long toVersion) { -+ if (data == null) { -+ return; -+ } -+ -+ RenameHelper.renameKeys(data, (final String input) -> { -+ return (String)type.convert(input, fromVersion, toVersion); -+ }); -+ } -+ -+ public static void convertValues(final MCDataType type, final MapType data, final String path, final long fromVersion, final long toVersion) { -+ if (data == null) { -+ return; -+ } -+ -+ convertValues(type, data.getMap(path), fromVersion, toVersion); -+ } -+ -+ public static void convertValues(final MCDataType type, final MapType data, final long fromVersion, final long toVersion) { -+ if (data == null) { -+ return; -+ } -+ -+ for (final String key : data.keys()) { -+ final MapType value = data.getMap(key); -+ if (value != null) { -+ final MapType replace = type.convert(value, fromVersion, toVersion); -+ if (replace != null) { -+ // no CME, key is in map already -+ data.setMap(key, replace); -+ } -+ } -+ } -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/item_name/DataWalkerItemNames.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/item_name/DataWalkerItemNames.java -new file mode 100644 -index 0000000000000000000000000000000000000000..14e291efd864d97dcf83db01c09b9daaae1949bd ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/item_name/DataWalkerItemNames.java -@@ -0,0 +1,11 @@ -+package ca.spottedleaf.dataconverter.minecraft.walkers.item_name; -+ -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; -+ -+public final class DataWalkerItemNames extends DataWalkerTypePaths { -+ -+ public DataWalkerItemNames(final String... paths) { -+ super(MCTypeRegistry.ITEM_NAME, paths); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItemLists.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItemLists.java -new file mode 100644 -index 0000000000000000000000000000000000000000..5b4402c3cc4e68e9c591e8bbb4a2542d8e2214d4 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItemLists.java -@@ -0,0 +1,12 @@ -+package ca.spottedleaf.dataconverter.minecraft.walkers.itemstack; -+ -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerListPaths; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public class DataWalkerItemLists extends DataWalkerListPaths, MapType> { -+ -+ public DataWalkerItemLists(final String... paths) { -+ super(MCTypeRegistry.ITEM_STACK, paths); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItems.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItems.java -new file mode 100644 -index 0000000000000000000000000000000000000000..04770e8378ac8784895cdfe400a47b0b601c2187 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/itemstack/DataWalkerItems.java -@@ -0,0 +1,12 @@ -+package ca.spottedleaf.dataconverter.minecraft.walkers.itemstack; -+ -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public class DataWalkerItems extends DataWalkerTypePaths, MapType> { -+ -+ public DataWalkerItems(final String... paths) { -+ super(MCTypeRegistry.ITEM_STACK, paths); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/tile_entity/DataWalkerTileEntities.java b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/tile_entity/DataWalkerTileEntities.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d9cc21bf41cb4b377752b684f8e59818cd620103 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/minecraft/walkers/tile_entity/DataWalkerTileEntities.java -@@ -0,0 +1,12 @@ -+package ca.spottedleaf.dataconverter.minecraft.walkers.tile_entity; -+ -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import ca.spottedleaf.dataconverter.minecraft.walkers.generic.DataWalkerTypePaths; -+import ca.spottedleaf.dataconverter.types.MapType; -+ -+public final class DataWalkerTileEntities extends DataWalkerTypePaths, MapType> { -+ -+ public DataWalkerTileEntities(final String... paths) { -+ super(MCTypeRegistry.TILE_ENTITY, paths); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/ListType.java b/src/main/java/ca/spottedleaf/dataconverter/types/ListType.java -new file mode 100644 -index 0000000000000000000000000000000000000000..19f7e95f754e8385bbe60fd2fb7fc95b6a4ebd7c ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/types/ListType.java -@@ -0,0 +1,272 @@ -+package ca.spottedleaf.dataconverter.types; -+ -+public interface ListType { -+ -+ public TypeUtil getTypeUtil(); -+ -+ @Override -+ public int hashCode(); -+ -+ @Override -+ public boolean equals(final Object other); -+ -+ // Provides a deep copy of this list -+ public ListType copy(); -+ -+ // returns NONE if no type has been assigned. if NONE, then this list is also empty. It is not true on the other hand that an empty list has no type. -+ public ObjectType getType(); -+ -+ public int size(); -+ -+ public void remove(final int index); -+ -+ public default Object getGeneric(final int index) { -+ switch (this.getType()) { -+ case NONE: -+ throw new IllegalStateException("List is empty and has no type"); -+ case BYTE: -+ return Byte.valueOf(this.getByte(index)); -+ case SHORT: -+ return Short.valueOf(this.getShort(index)); -+ case INT: -+ return Integer.valueOf(this.getInt(index)); -+ case LONG: -+ return Long.valueOf(this.getLong(index)); -+ case FLOAT: -+ return Float.valueOf(this.getFloat(index)); -+ case DOUBLE: -+ return Double.valueOf(this.getDouble(index)); -+ case NUMBER: -+ return this.getNumber(index); -+ case BYTE_ARRAY: -+ return this.getBytes(index); -+ case SHORT_ARRAY: -+ return this.getShorts(index); -+ case INT_ARRAY: -+ return this.getInts(index); -+ case LONG_ARRAY: -+ return this.getLongs(index); -+ case LIST: -+ return this.getList(index); -+ case MAP: -+ return this.getMap(index); -+ case STRING: -+ return this.getString(index); -+ default: -+ throw new UnsupportedOperationException(this.getType().name()); -+ } -+ } -+ -+ public default void setGeneric(final int index, final Object to) { -+ if (to instanceof Number) { -+ if (to instanceof Byte) { -+ this.setByte(index, ((Byte)to).byteValue()); -+ return; -+ } else if (to instanceof Short) { -+ this.setShort(index, ((Short)to).shortValue()); -+ return; -+ } else if (to instanceof Integer) { -+ this.setInt(index, ((Integer)to).intValue()); -+ return; -+ } else if (to instanceof Long) { -+ this.setLong(index, ((Long)to).longValue()); -+ return; -+ } else if (to instanceof Float) { -+ this.setFloat(index, ((Float)to).floatValue()); -+ return; -+ } else if (to instanceof Double) { -+ this.setDouble(index, ((Double)to).doubleValue()); -+ return; -+ } // else fall through to throw -+ } else if (to instanceof MapType) { -+ this.setMap(index, (MapType)to); -+ return; -+ } else if (to instanceof ListType) { -+ this.setList(index, (ListType)to); -+ return; -+ } else if (to instanceof String) { -+ this.setString(index, (String)to); -+ return; -+ } else if (to.getClass().isArray()) { -+ if (to instanceof byte[]) { -+ this.setBytes(index, (byte[])to); -+ return; -+ } else if (to instanceof short[]) { -+ this.setShorts(index, (short[])to); -+ return; -+ } else if (to instanceof int[]) { -+ this.setInts(index, (int[])to); -+ return; -+ } else if (to instanceof long[]) { -+ this.setLongs(index, (long[])to); -+ return; -+ } // else fall through to throw -+ } -+ -+ throw new IllegalArgumentException("Object " + to + " is not a valid type!"); -+ } -+ -+ // types here are strict. if the type on get does not match the underlying type, will throw. -+ -+ public Number getNumber(final int index); -+ -+ // if the value at index is a Number but not a byte, then returns the number casted to byte. If the value at the index is not a number, then throws -+ public byte getByte(final int index); -+ -+ public void setByte(final int index, final byte to); -+ -+ // if the value at index is a Number but not a short, then returns the number casted to short. If the value at the index is not a number, then throws -+ public short getShort(final int index); -+ -+ public void setShort(final int index, final short to); -+ -+ // if the value at index is a Number but not a int, then returns the number casted to int. If the value at the index is not a number, then throws -+ public int getInt(final int index); -+ -+ public void setInt(final int index, final int to); -+ -+ // if the value at index is a Number but not a long, then returns the number casted to long. If the value at the index is not a number, then throws -+ public long getLong(final int index); -+ -+ public void setLong(final int index, final long to); -+ -+ // if the value at index is a Number but not a float, then returns the number casted to float. If the value at the index is not a number, then throws -+ public float getFloat(final int index); -+ -+ public void setFloat(final int index, final float to); -+ -+ // if the value at index is a Number but not a double, then returns the number casted to double. If the value at the index is not a number, then throws -+ public double getDouble(final int index); -+ -+ public void setDouble(final int index, final double to); -+ -+ public byte[] getBytes(final int index); -+ -+ public void setBytes(final int index, final byte[] to); -+ -+ public short[] getShorts(final int index); -+ -+ public void setShorts(final int index, final short[] to); -+ -+ public int[] getInts(final int index); -+ -+ public void setInts(final int index, final int[] to); -+ -+ public long[] getLongs(final int index); -+ -+ public void setLongs(final int index, final long[] to); -+ -+ public ListType getList(final int index); -+ -+ public void setList(final int index, final ListType list); -+ -+ public MapType getMap(final int index); -+ -+ public void setMap(final int index, final MapType to); -+ -+ public String getString(final int index); -+ -+ public void setString(final int index, final String to); -+ -+ public default void addGeneric(final Object to) { -+ if (to instanceof Number) { -+ if (to instanceof Byte) { -+ this.addByte(((Byte)to).byteValue()); -+ return; -+ } else if (to instanceof Short) { -+ this.addShort(((Short)to).shortValue()); -+ return; -+ } else if (to instanceof Integer) { -+ this.addInt(((Integer)to).intValue()); -+ return; -+ } else if (to instanceof Long) { -+ this.addLong(((Long)to).longValue()); -+ return; -+ } else if (to instanceof Float) { -+ this.addFloat(((Float)to).floatValue()); -+ return; -+ } else if (to instanceof Double) { -+ this.addDouble(((Double)to).doubleValue()); -+ return; -+ } // else fall through to throw -+ } else if (to instanceof MapType) { -+ this.addMap((MapType)to); -+ return; -+ } else if (to instanceof ListType) { -+ this.addList((ListType)to); -+ return; -+ } else if (to instanceof String) { -+ this.addString((String)to); -+ return; -+ } else if (to.getClass().isArray()) { -+ if (to instanceof byte[]) { -+ this.addByteArray((byte[])to); -+ return; -+ } else if (to instanceof short[]) { -+ this.addShortArray((short[])to); -+ return; -+ } else if (to instanceof int[]) { -+ this.addIntArray((int[])to); -+ return; -+ } else if (to instanceof long[]) { -+ this.addLongArray((long[])to); -+ return; -+ } // else fall through to throw -+ } -+ -+ throw new IllegalArgumentException("Object " + to + " is not a valid type!"); -+ } -+ -+ public void addByte(final byte b); -+ -+ public void addByte(final int index, final byte b); -+ -+ public void addShort(final short s); -+ -+ public void addShort(final int index, final short s); -+ -+ public void addInt(final int i); -+ -+ public void addInt(final int index, final int i); -+ -+ public void addLong(final long l); -+ -+ public void addLong(final int index, final long l); -+ -+ public void addFloat(final float f); -+ -+ public void addFloat(final int index, final float f); -+ -+ public void addDouble(final double d); -+ -+ public void addDouble(final int index, final double d); -+ -+ public void addByteArray(final byte[] arr); -+ -+ public void addByteArray(final int index, final byte[] arr); -+ -+ public void addShortArray(final short[] arr); -+ -+ public void addShortArray(final int index, final short[] arr); -+ -+ public void addIntArray(final int[] arr); -+ -+ public void addIntArray(final int index, final int[] arr); -+ -+ public void addLongArray(final long[] arr); -+ -+ public void addLongArray(final int index, final long[] arr); -+ -+ public void addList(final ListType list); -+ -+ public void addList(final int index, final ListType list); -+ -+ public void addMap(final MapType map); -+ -+ public void addMap(final int index, final MapType map); -+ -+ public void addString(final String string); -+ -+ public void addString(final int index, final String string); -+ -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/MapType.java b/src/main/java/ca/spottedleaf/dataconverter/types/MapType.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b8dad91ad3b8692448134c4f12cf9853dc06fccc ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/types/MapType.java -@@ -0,0 +1,223 @@ -+package ca.spottedleaf.dataconverter.types; -+ -+import java.util.Set; -+ -+public interface MapType { -+ -+ public TypeUtil getTypeUtil(); -+ -+ @Override -+ public int hashCode(); -+ -+ @Override -+ public boolean equals(final Object other); -+ -+ public int size(); -+ -+ public boolean isEmpty(); -+ -+ public void clear(); -+ -+ public Set keys(); -+ -+ // Provides a deep copy of this map -+ public MapType copy(); -+ -+ public boolean hasKey(final K key); -+ -+ public boolean hasKey(final K key, final ObjectType type); -+ -+ public void remove(final K key); -+ -+ public Object getGeneric(final K key); -+ -+ // types here are not strict. if the key maps to a different type, default is always returned -+ // if default is not a parameter, then default is always null -+ -+ public Number getNumber(final K key); -+ -+ public Number getNumber(final K key, final Number dfl); -+ -+ public boolean getBoolean(final K key); -+ -+ public boolean getBoolean(final K key, final boolean dfl); -+ -+ public void setBoolean(final K key, final boolean val); -+ -+ // if the mapped value is a Number but not a byte, then the number is casted to byte. If the mapped value does not exist or is not a number, returns 0 -+ public byte getByte(final K key); -+ -+ // if the mapped value is a Number but not a byte, then the number is casted to byte. If the mapped value does not exist or is not a number, returns dfl -+ public byte getByte(final K key, final byte dfl); -+ -+ public void setByte(final K key, final byte val); -+ -+ // if the mapped value is a Number but not a short, then the number is casted to short. If the mapped value does not exist or is not a number, returns 0 -+ public short getShort(final K key); -+ -+ // if the mapped value is a Number but not a short, then the number is casted to short. If the mapped value does not exist or is not a number, returns dfl -+ public short getShort(final K key, final short dfl); -+ -+ public void setShort(final K key, final short val); -+ -+ // if the mapped value is a Number but not a int, then the number is casted to int. If the mapped value does not exist or is not a number, returns 0 -+ public int getInt(final K key); -+ -+ // if the mapped value is a Number but not a int, then the number is casted to int. If the mapped value does not exist or is not a number, returns dfl -+ public int getInt(final K key, final int dfl); -+ -+ public void setInt(final K key, final int val); -+ -+ // if the mapped value is a Number but not a long, then the number is casted to long. If the mapped value does not exist or is not a number, returns 0 -+ public long getLong(final K key); -+ -+ // if the mapped value is a Number but not a long, then the number is casted to long. If the mapped value does not exist or is not a number, returns dfl -+ public long getLong(final K key, final long dfl); -+ -+ public void setLong(final K key, final long val); -+ -+ // if the mapped value is a Number but not a float, then the number is casted to float. If the mapped value does not exist or is not a number, returns 0 -+ public float getFloat(final K key); -+ -+ // if the mapped value is a Number but not a float, then the number is casted to float. If the mapped value does not exist or is not a number, returns dfl -+ public float getFloat(final K key, final float dfl); -+ -+ public void setFloat(final K key, final float val); -+ -+ // if the mapped value is a Number but not a double, then the number is casted to double. If the mapped value does not exist or is not a number, returns 0 -+ public double getDouble(final K key); -+ -+ // if the mapped value is a Number but not a double, then the number is casted to double. If the mapped value does not exist or is not a number, returns dfl -+ public double getDouble(final K key, final double dfl); -+ -+ public void setDouble(final K key, final double val); -+ -+ public byte[] getBytes(final K key); -+ -+ public byte[] getBytes(final K key, final byte[] dfl); -+ -+ public void setBytes(final K key, final byte[] val); -+ -+ public short[] getShorts(final K key); -+ -+ public short[] getShorts(final K key, final short[] dfl); -+ -+ public void setShorts(final K key, final short[] val); -+ -+ public int[] getInts(final K key); -+ -+ public int[] getInts(final K key, final int[] dfl); -+ -+ public void setInts(final K key, final int[] val); -+ -+ public long[] getLongs(final K key); -+ -+ public long[] getLongs(final K key, final long[] dfl); -+ -+ public void setLongs(final K key, final long[] val); -+ -+ public ListType getListUnchecked(final K key); -+ -+ public ListType getListUnchecked(final K key, final ListType dfl); -+ -+ public default ListType getList(final K key, final ObjectType type) { -+ return this.getList(key, type, null); -+ } -+ -+ public default ListType getOrCreateList(final K key, final ObjectType type) { -+ ListType ret = this.getList(key, type); -+ if (ret == null) { -+ this.setList(key, ret = this.getTypeUtil().createEmptyList()); -+ } -+ -+ return ret; -+ } -+ -+ public default ListType getList(final K key, final ObjectType type, final ListType dfl) { -+ final ListType ret = this.getListUnchecked(key, null); -+ final ObjectType retType; -+ if (ret != null && ((retType = ret.getType()) == type || retType == ObjectType.UNDEFINED)) { -+ return ret; -+ } else { -+ return dfl; -+ } -+ } -+ -+ public void setList(final K key, final ListType val); -+ -+ public MapType getMap(final K key); -+ -+ public default MapType getOrCreateMap(final K key) { -+ MapType ret = this.getMap(key); -+ if (ret == null) { -+ this.setMap(key, ret = this.getTypeUtil().createEmptyMap()); -+ } -+ -+ return ret; -+ } -+ -+ public MapType getMap(final K key, final MapType dfl); -+ -+ public void setMap(final K key, final MapType val); -+ -+ public String getString(final K key); -+ -+ public String getString(final K key, final String dfl); -+ -+ public String getForcedString(final K key); -+ -+ public String getForcedString(final K key, final String dfl); -+ -+ public void setString(final K key, final String val); -+ -+ public default void setGeneric(final K key, final Object value) { -+ if (value instanceof Boolean) { -+ this.setBoolean(key, ((Boolean)value).booleanValue()); -+ } else if (value instanceof Number) { -+ if (value instanceof Byte) { -+ this.setByte(key, ((Byte)value).byteValue()); -+ return; -+ } else if (value instanceof Short) { -+ this.setShort(key, ((Short)value).shortValue()); -+ return; -+ } else if (value instanceof Integer) { -+ this.setInt(key, ((Integer)value).intValue()); -+ return; -+ } else if (value instanceof Long) { -+ this.setLong(key, ((Long)value).longValue()); -+ return; -+ } else if (value instanceof Float) { -+ this.setFloat(key, ((Float)value).floatValue()); -+ return; -+ } else if (value instanceof Double) { -+ this.setDouble(key, ((Double)value).doubleValue()); -+ return; -+ } // else fall through to throw -+ } else if (value instanceof MapType) { -+ this.setMap(key, (MapType)value); -+ return; -+ } else if (value instanceof ListType) { -+ this.setList(key, (ListType)value); -+ return; -+ } else if (value instanceof String) { -+ this.setString(key, (String)value); -+ return; -+ } else if (value.getClass().isArray()) { -+ if (value instanceof byte[]) { -+ this.setBytes(key, (byte[])value); -+ return; -+ } else if (value instanceof short[]) { -+ this.setShorts(key, (short[])value); -+ return; -+ } else if (value instanceof int[]) { -+ this.setInts(key, (int[])value); -+ return; -+ } else if (value instanceof long[]) { -+ this.setLongs(key, (long[])value); -+ return; -+ } // else fall through to throw -+ } -+ -+ throw new IllegalArgumentException("Object " + value + " is not a valid type!"); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/ObjectType.java b/src/main/java/ca/spottedleaf/dataconverter/types/ObjectType.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1aab91233ddb98c3af5d424bac120891f1ee16c7 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/types/ObjectType.java -@@ -0,0 +1,72 @@ -+package ca.spottedleaf.dataconverter.types; -+ -+public enum ObjectType { -+ NONE(null), -+ BYTE(Byte.class), -+ SHORT(Short.class), -+ INT(Integer.class), -+ LONG(Long.class), -+ FLOAT(Float.class), -+ DOUBLE(Double.class), -+ NUMBER(Number.class), -+ BYTE_ARRAY(byte[].class), -+ SHORT_ARRAY(short[].class), -+ INT_ARRAY(int[].class), -+ LONG_ARRAY(long[].class), -+ LIST(ListType.class), -+ MAP(MapType.class), -+ STRING(String.class), -+ UNDEFINED(null); -+ -+ private final Class clazz; -+ private final boolean isNumber; -+ -+ private ObjectType(final Class clazz) { -+ this.clazz = clazz; -+ this.isNumber = clazz != null && Number.class.isAssignableFrom(clazz); -+ } -+ -+ public boolean isNumber() { -+ return this.isNumber; -+ } -+ -+ public Class getObjectClass() { -+ return this.clazz; -+ } -+ -+ public static ObjectType getType(final Object object) { -+ if (object instanceof Number) { -+ if (object instanceof Byte) { -+ return BYTE; -+ } else if (object instanceof Short) { -+ return SHORT; -+ } else if (object instanceof Integer) { -+ return INT; -+ } else if (object instanceof Long) { -+ return LONG; -+ } else if (object instanceof Float) { -+ return FLOAT; -+ } else if (object instanceof Double) { -+ return DOUBLE; -+ } // else return null -+ } else if (object instanceof MapType) { -+ return MAP; -+ } else if (object instanceof ListType) { -+ return LIST; -+ } else if (object instanceof String) { -+ return STRING; -+ } else if (object.getClass().isArray()) { -+ if (object instanceof byte[]) { -+ return BYTE_ARRAY; -+ } else if (object instanceof short[]) { -+ return SHORT_ARRAY; -+ } else if (object instanceof int[]) { -+ return INT_ARRAY; -+ } else if (object instanceof long[]) { -+ return LONG_ARRAY; -+ } // else return null -+ } -+ -+ return null; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/TypeUtil.java b/src/main/java/ca/spottedleaf/dataconverter/types/TypeUtil.java -new file mode 100644 -index 0000000000000000000000000000000000000000..156a2ea46f8f88a02e88b50d7bb7be82ecd41919 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/types/TypeUtil.java -@@ -0,0 +1,9 @@ -+package ca.spottedleaf.dataconverter.types; -+ -+public interface TypeUtil { -+ -+ public ListType createEmptyList(); -+ -+ public MapType createEmptyMap(); -+ -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/Types.java b/src/main/java/ca/spottedleaf/dataconverter/types/Types.java -new file mode 100644 -index 0000000000000000000000000000000000000000..2ab9e3b579f20c9a189518496c522155630a36c4 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/types/Types.java -@@ -0,0 +1,15 @@ -+package ca.spottedleaf.dataconverter.types; -+ -+import ca.spottedleaf.dataconverter.types.json.JsonTypeCompressedUtil; -+import ca.spottedleaf.dataconverter.types.json.JsonTypeUtil; -+import ca.spottedleaf.dataconverter.types.nbt.NBTTypeUtil; -+ -+public interface Types { -+ -+ public static final TypeUtil NBT = new NBTTypeUtil(); -+ -+ public static final TypeUtil JSON = new JsonTypeUtil(); -+ -+ // why does this exist -+ public static final TypeUtil JSON_COMPRESSED = new JsonTypeCompressedUtil(); -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonListType.java b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonListType.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f6f57cb3a215876976b5eecae810b8b20925f2e2 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonListType.java -@@ -0,0 +1,415 @@ -+package ca.spottedleaf.dataconverter.types.json; -+ -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.TypeUtil; -+import ca.spottedleaf.dataconverter.types.Types; -+import com.google.gson.JsonArray; -+import com.google.gson.JsonElement; -+import com.google.gson.JsonObject; -+import com.google.gson.JsonPrimitive; -+ -+public final class JsonListType implements ListType { -+ -+ protected final JsonArray array; -+ protected final boolean compressed; -+ -+ public JsonListType(final boolean compressed) { -+ this.array = new JsonArray(); -+ this.compressed = compressed; -+ } -+ -+ public JsonListType(final JsonArray array, final boolean compressed) { -+ this.array = array; -+ this.compressed = compressed; -+ } -+ -+ @Override -+ public TypeUtil getTypeUtil() { -+ return this.compressed ? Types.JSON_COMPRESSED : Types.JSON; -+ } -+ -+ @Override -+ public boolean equals(final Object obj) { -+ if (this == obj) { -+ return true; -+ } -+ -+ if (obj == null || obj.getClass() != JsonListType.class) { -+ return false; -+ } -+ -+ return this.array.equals(((JsonListType)obj).array); -+ } -+ -+ @Override -+ public int hashCode() { -+ return this.array.hashCode(); -+ } -+ -+ @Override -+ public String toString() { -+ return "JsonListType{" + -+ "array=" + this.array + -+ ", compressed=" + this.compressed + -+ '}'; -+ } -+ -+ public JsonArray getJson() { -+ return this.array; -+ } -+ -+ @Override -+ public ListType copy() { -+ return new JsonListType(JsonTypeUtil.copyJson(this.array), this.compressed); -+ } -+ -+ @Override -+ public ObjectType getType() { -+ return ObjectType.UNDEFINED; -+ } -+ -+ @Override -+ public int size() { -+ return this.array.size(); -+ } -+ -+ @Override -+ public void remove(final int index) { -+ this.array.remove(index); -+ } -+ -+ @Override -+ public Number getNumber(final int index) { -+ final JsonElement element = this.array.get(index); -+ if (element instanceof JsonPrimitive) { -+ final JsonPrimitive primitive = (JsonPrimitive)element; -+ if (primitive.isNumber()) { -+ return primitive.getAsNumber(); -+ } else if (primitive.isBoolean()) { -+ return primitive.getAsBoolean() ? Byte.valueOf((byte)1) : Byte.valueOf((byte)0); -+ } else if (this.compressed && primitive.isString()) { -+ try { -+ return Integer.valueOf(Integer.parseInt(primitive.getAsString())); -+ } catch (final NumberFormatException ex) { -+ return null; -+ } -+ } -+ } -+ -+ return null; -+ } -+ -+ @Override -+ public byte getByte(final int index) { -+ final Number number = this.getNumber(index); -+ -+ return number == null ? 0 : number.byteValue(); -+ } -+ -+ @Override -+ public void setByte(final int index, final byte to) { -+ this.array.set(index, new JsonPrimitive(Byte.valueOf(to))); -+ } -+ -+ @Override -+ public short getShort(final int index) { -+ final Number number = this.getNumber(index); -+ -+ return number == null ? 0 : number.shortValue(); -+ } -+ -+ @Override -+ public void setShort(final int index, final short to) { -+ this.array.set(index, new JsonPrimitive(Short.valueOf(to))); -+ } -+ -+ @Override -+ public int getInt(final int index) { -+ final Number number = this.getNumber(index); -+ -+ return number == null ? 0 : number.intValue(); -+ } -+ -+ @Override -+ public void setInt(final int index, final int to) { -+ this.array.set(index, new JsonPrimitive(Integer.valueOf(to))); -+ } -+ -+ @Override -+ public long getLong(final int index) { -+ final Number number = this.getNumber(index); -+ -+ return number == null ? 0 : number.longValue(); -+ } -+ -+ @Override -+ public void setLong(final int index, final long to) { -+ this.array.set(index, new JsonPrimitive(Long.valueOf(to))); -+ } -+ -+ @Override -+ public float getFloat(final int index) { -+ final Number number = this.getNumber(index); -+ -+ return number == null ? 0 : number.floatValue(); -+ } -+ -+ @Override -+ public void setFloat(final int index, final float to) { -+ this.array.set(index, new JsonPrimitive(Float.valueOf(to))); -+ } -+ -+ @Override -+ public double getDouble(final int index) { -+ final Number number = this.getNumber(index); -+ -+ return number == null ? 0 : number.doubleValue(); -+ } -+ -+ @Override -+ public void setDouble(final int index, final double to) { -+ this.array.set(index, new JsonPrimitive(Double.valueOf(to))); -+ } -+ -+ @Override -+ public byte[] getBytes(final int index) { -+ // JSON does not support raw primitive arrays -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void setBytes(final int index, final byte[] to) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public short[] getShorts(final int index) { -+ // JSON does not support raw primitive arrays -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void setShorts(final int index, final short[] to) { -+ // JSON does not support raw primitive arrays -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public int[] getInts(final int index) { -+ // JSON does not support raw primitive arrays -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void setInts(final int index, final int[] to) { -+ // JSON does not support raw primitive arrays -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public long[] getLongs(final int index) { -+ // JSON does not support raw primitive arrays -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void setLongs(final int index, final long[] to) { -+ // JSON does not support raw primitive arrays -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public ListType getList(final int index) { -+ final JsonElement element = this.array.get(index); -+ if (element instanceof JsonArray) { -+ return new JsonListType((JsonArray)element, this.compressed); -+ } -+ return null; -+ } -+ -+ @Override -+ public void setList(final int index, final ListType list) { -+ this.array.set(index, ((JsonListType)list).array); -+ } -+ -+ @Override -+ public MapType getMap(final int index) { -+ final JsonElement element = this.array.get(index); -+ if (element instanceof JsonObject) { -+ return new JsonMapType((JsonObject)element, this.compressed); -+ } -+ return null; -+ } -+ -+ @Override -+ public void setMap(final int index, final MapType to) { -+ this.array.set(index, ((JsonMapType)to).map); -+ } -+ -+ @Override -+ public String getString(final int index) { -+ final JsonElement element = this.array.get(index); -+ if (element instanceof JsonPrimitive) { -+ final JsonPrimitive primitive = (JsonPrimitive)element; -+ if (primitive.isString() || (this.compressed && primitive.isNumber())) { -+ return primitive.getAsString(); -+ } -+ } -+ -+ return null; -+ } -+ -+ @Override -+ public void setString(final int index, final String to) { -+ this.array.set(index, new JsonPrimitive(to)); -+ } -+ -+ @Override -+ public void addByte(final byte b) { -+ this.array.add(Byte.valueOf(b)); -+ } -+ -+ @Override -+ public void addByte(final int index, final byte b) { -+ // doesn't implement any methods for adding at index... yee haw... -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void addShort(final short s) { -+ this.array.add(Short.valueOf(s)); -+ } -+ -+ @Override -+ public void addShort(final int index, final short s) { -+ // doesn't implement any methods for adding at index... yee haw... -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void addInt(final int i) { -+ this.array.add(Integer.valueOf(i)); -+ } -+ -+ @Override -+ public void addInt(final int index, final int i) { -+ // doesn't implement any methods for adding at index... yee haw... -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void addLong(final long l) { -+ this.array.add(Long.valueOf(l)); -+ } -+ -+ @Override -+ public void addLong(final int index, final long l) { -+ // doesn't implement any methods for adding at index... yee haw... -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void addFloat(final float f) { -+ this.array.add(Float.valueOf(f)); -+ } -+ -+ @Override -+ public void addFloat(final int index, final float f) { -+ // doesn't implement any methods for adding at index... yee haw... -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void addDouble(final double d) { -+ this.array.add(Double.valueOf(d)); -+ } -+ -+ @Override -+ public void addDouble(final int index, final double d) { -+ // doesn't implement any methods for adding at index... yee haw... -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void addByteArray(final byte[] arr) { -+ // JSON does not support raw primitive arrays -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void addByteArray(final int index, final byte[] arr) { -+ // JSON does not support raw primitive arrays -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void addShortArray(final short[] arr) { -+ // JSON does not support raw primitive arrays -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void addShortArray(final int index, final short[] arr) { -+ // JSON does not support raw primitive arrays -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void addIntArray(final int[] arr) { -+ // JSON does not support raw primitive arrays -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void addIntArray(final int index, final int[] arr) { -+ // JSON does not support raw primitive arrays -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void addLongArray(final long[] arr) { -+ // JSON does not support raw primitive arrays -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void addLongArray(final int index, final long[] arr) { -+ // JSON does not support raw primitive arrays -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void addList(final ListType list) { -+ this.array.add(((JsonListType)list).array); -+ } -+ -+ @Override -+ public void addList(final int index, final ListType list) { -+ // doesn't implement any methods for adding at index... yee haw... -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void addMap(final MapType map) { -+ this.array.add(((JsonMapType)map).map); -+ } -+ -+ @Override -+ public void addMap(final int index, final MapType map) { -+ // doesn't implement any methods for adding at index... yee haw... -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void addString(final String string) { -+ this.array.add(string); -+ } -+ -+ @Override -+ public void addString(final int index, final String string) { -+ // doesn't implement any methods for adding at index... yee haw... -+ throw new UnsupportedOperationException(); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonMapType.java b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonMapType.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b6ad4623894454675f4be52ecdb4655d6623b385 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonMapType.java -@@ -0,0 +1,474 @@ -+package ca.spottedleaf.dataconverter.types.json; -+ -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.TypeUtil; -+import ca.spottedleaf.dataconverter.types.Types; -+import com.google.gson.JsonArray; -+import com.google.gson.JsonElement; -+import com.google.gson.JsonObject; -+import com.google.gson.JsonPrimitive; -+import java.util.LinkedHashSet; -+import java.util.Map; -+import java.util.Set; -+ -+public final class JsonMapType implements MapType { -+ -+ protected final JsonObject map; -+ protected final boolean compressed; -+ -+ public JsonMapType(final boolean compressed) { -+ this.map = new JsonObject(); -+ this.compressed = compressed; -+ } -+ -+ public JsonMapType(final JsonObject map, final boolean compressed) { -+ this.map = map; -+ this.compressed = compressed; -+ } -+ -+ @Override -+ public TypeUtil getTypeUtil() { -+ return this.compressed ? Types.JSON_COMPRESSED : Types.JSON; -+ } -+ -+ @Override -+ public boolean equals(final Object obj) { -+ if (this == obj) { -+ return true; -+ } -+ -+ if (obj == null || obj.getClass() != JsonMapType.class) { -+ return false; -+ } -+ -+ return this.map.equals(((JsonMapType)obj).map); -+ } -+ -+ @Override -+ public int hashCode() { -+ return this.map.hashCode(); -+ } -+ -+ @Override -+ public String toString() { -+ return "JsonMapType{" + -+ "map=" + this.map + -+ ", compressed=" + this.compressed + -+ '}'; -+ } -+ -+ public JsonObject getJson() { -+ return this.map; -+ } -+ -+ @Override -+ public int size() { -+ return this.map.entrySet().size(); -+ } -+ -+ @Override -+ public boolean isEmpty() { -+ return this.map.entrySet().isEmpty(); -+ } -+ -+ @Override -+ public void clear() { -+ this.map.entrySet().clear(); -+ } -+ -+ @Override -+ public Set keys() { -+ // ah shit. no keyset method -+ final Set keys = new LinkedHashSet<>(); -+ -+ for (final Map.Entry entry : this.map.entrySet()) { -+ keys.add(entry.getKey()); -+ } -+ -+ return keys; -+ } -+ -+ @Override -+ public MapType copy() { -+ return new JsonMapType(JsonTypeUtil.copyJson(this.map), this.compressed); -+ } -+ -+ @Override -+ public boolean hasKey(final String key) { -+ return this.map.has(key); -+ } -+ -+ @Override -+ public boolean hasKey(final String key, final ObjectType type) { -+ final JsonElement element = this.map.get(key); -+ if (element == null) { -+ return false; -+ } -+ -+ if (type == ObjectType.UNDEFINED) { -+ return true; -+ } -+ -+ if (element.isJsonArray()) { -+ return type == ObjectType.LIST; -+ } else if (element.isJsonObject()) { -+ return type == ObjectType.MAP; -+ } else if (element.isJsonNull()) { -+ return false; -+ } -+ -+ final JsonPrimitive primitive = (JsonPrimitive)element; -+ if (primitive.isString()) { -+ return type == ObjectType.STRING || (this.compressed && type == ObjectType.NUMBER); -+ } else if (primitive.isBoolean()) { -+ return type.isNumber(); -+ } else { -+ // is number -+ final Number number = primitive.getAsNumber(); -+ if (number instanceof Byte) { -+ return type == ObjectType.BYTE || (this.compressed && type == ObjectType.STRING); -+ } else if (number instanceof Short) { -+ return type == ObjectType.SHORT || (this.compressed && type == ObjectType.STRING); -+ } else if (number instanceof Integer) { -+ return type == ObjectType.INT || (this.compressed && type == ObjectType.STRING); -+ } else if (number instanceof Long) { -+ return type == ObjectType.LONG || (this.compressed && type == ObjectType.STRING); -+ } else if (number instanceof Float) { -+ return type == ObjectType.FLOAT || (this.compressed && type == ObjectType.STRING); -+ } else { -+ return type == ObjectType.DOUBLE || (this.compressed && type == ObjectType.STRING); -+ } -+ } -+ } -+ -+ @Override -+ public void remove(final String key) { -+ this.map.remove(key); -+ } -+ -+ @Override -+ public Object getGeneric(final String key) { -+ final JsonElement element = this.map.get(key); -+ if (element == null || element.isJsonNull()) { -+ return null; -+ } else if (element.isJsonObject()) { -+ return new JsonMapType((JsonObject)element, this.compressed); -+ } else if (element.isJsonArray()) { -+ return new JsonListType((JsonArray)element, this.compressed); -+ } else { -+ // primitive -+ final JsonPrimitive primitive = (JsonPrimitive)element; -+ if (primitive.isNumber()) { -+ return primitive.getAsNumber(); -+ } else if (primitive.isString()) { -+ return primitive.getAsString(); -+ } else if (primitive.isBoolean()) { -+ return Boolean.valueOf(primitive.getAsBoolean()); -+ } else { -+ throw new IllegalStateException("Unknown json object " + element); -+ } -+ } -+ } -+ -+ @Override -+ public Number getNumber(final String key) { -+ return this.getNumber(key, null); -+ } -+ -+ @Override -+ public Number getNumber(final String key, final Number dfl) { -+ final JsonElement element = this.map.get(key); -+ if (element instanceof JsonPrimitive) { -+ final JsonPrimitive primitive = (JsonPrimitive)element; -+ if (primitive.isNumber()) { -+ return primitive.getAsNumber(); -+ } else if (primitive.isBoolean()) { -+ return primitive.getAsBoolean() ? Byte.valueOf((byte)1) : Byte.valueOf((byte)0); -+ } else if (this.compressed && primitive.isString()) { -+ try { -+ return Integer.valueOf(Integer.parseInt(primitive.getAsString())); -+ } catch (final NumberFormatException ex) { -+ return null; -+ } -+ } -+ } -+ -+ return dfl; -+ } -+ -+ @Override -+ public boolean getBoolean(final String key) { -+ return this.getBoolean(key, false); -+ } -+ -+ @Override -+ public boolean getBoolean(final String key, final boolean dfl) { -+ final JsonElement element = this.map.get(key); -+ if (element instanceof JsonPrimitive) { -+ final JsonPrimitive primitive = (JsonPrimitive)element; -+ if (primitive.isNumber()) { -+ return primitive.getAsNumber().byteValue() != 0; -+ } else if (primitive.isBoolean()) { -+ return primitive.getAsBoolean(); -+ } -+ } -+ -+ return dfl; -+ } -+ -+ @Override -+ public void setBoolean(final String key, final boolean val) { -+ this.map.addProperty(key, Boolean.valueOf(val)); -+ } -+ -+ @Override -+ public byte getByte(final String key) { -+ return this.getByte(key, (byte)0); -+ } -+ -+ @Override -+ public byte getByte(final String key, final byte dfl) { -+ final Number ret = this.getNumber(key, null); -+ return ret == null ? dfl : ret.byteValue(); -+ } -+ -+ @Override -+ public void setByte(final String key, final byte val) { -+ this.map.addProperty(key, Byte.valueOf(val)); -+ } -+ -+ @Override -+ public short getShort(final String key) { -+ return this.getShort(key, (short)0); -+ } -+ -+ @Override -+ public short getShort(final String key, final short dfl) { -+ final Number ret = this.getNumber(key, null); -+ return ret == null ? dfl : ret.shortValue(); -+ } -+ -+ @Override -+ public void setShort(final String key, final short val) { -+ this.map.addProperty(key, Short.valueOf(val)); -+ } -+ -+ @Override -+ public int getInt(final String key) { -+ return this.getInt(key, 0); -+ } -+ -+ @Override -+ public int getInt(final String key, final int dfl) { -+ final Number ret = this.getNumber(key, null); -+ return ret == null ? dfl : ret.intValue(); -+ } -+ -+ @Override -+ public void setInt(final String key, final int val) { -+ this.map.addProperty(key, Integer.valueOf(val)); -+ } -+ -+ @Override -+ public long getLong(final String key) { -+ return this.getLong(key, 0L); -+ } -+ -+ @Override -+ public long getLong(final String key, final long dfl) { -+ final Number ret = this.getNumber(key, null); -+ return ret == null ? dfl : ret.longValue(); -+ } -+ -+ @Override -+ public void setLong(final String key, final long val) { -+ this.map.addProperty(key, Long.valueOf(val)); -+ } -+ -+ @Override -+ public float getFloat(final String key) { -+ return this.getFloat(key, 0.0F); -+ } -+ -+ @Override -+ public float getFloat(final String key, final float dfl) { -+ final Number ret = this.getNumber(key, null); -+ return ret == null ? dfl : ret.floatValue(); -+ } -+ -+ @Override -+ public void setFloat(final String key, final float val) { -+ this.map.addProperty(key, Float.valueOf(val)); -+ } -+ -+ @Override -+ public double getDouble(final String key) { -+ return this.getDouble(key, 0.0D); -+ } -+ -+ @Override -+ public double getDouble(final String key, final double dfl) { -+ final Number ret = this.getNumber(key, null); -+ return ret == null ? dfl : ret.doubleValue(); -+ } -+ -+ @Override -+ public void setDouble(final String key, final double val) { -+ this.map.addProperty(key, Double.valueOf(val)); -+ } -+ -+ @Override -+ public byte[] getBytes(final String key) { -+ return this.getBytes(key, null); -+ } -+ -+ @Override -+ public byte[] getBytes(final String key, final byte[] dfl) { -+ return dfl; -+ } -+ -+ @Override -+ public void setBytes(final String key, final byte[] val) { -+ // JSON does not support raw primitive arrays -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public short[] getShorts(final String key) { -+ return this.getShorts(key, null); -+ } -+ -+ @Override -+ public short[] getShorts(final String key, final short[] dfl) { -+ return dfl; -+ } -+ -+ @Override -+ public void setShorts(final String key, final short[] val) { -+ // JSON does not support raw primitive arrays -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public int[] getInts(final String key) { -+ return this.getInts(key, null); -+ } -+ -+ @Override -+ public int[] getInts(final String key, final int[] dfl) { -+ return dfl; -+ } -+ -+ @Override -+ public void setInts(final String key, final int[] val) { -+ // JSON does not support raw primitive arrays -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public long[] getLongs(final String key) { -+ return this.getLongs(key, null); -+ } -+ -+ @Override -+ public long[] getLongs(final String key, final long[] dfl) { -+ return dfl; -+ } -+ -+ @Override -+ public void setLongs(final String key, final long[] val) { -+ // JSON does not support raw primitive arrays -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public ListType getListUnchecked(final String key) { -+ return this.getListUnchecked(key, null); -+ } -+ -+ @Override -+ public ListType getListUnchecked(final String key, final ListType dfl) { -+ final JsonElement element = this.map.get(key); -+ if (element instanceof JsonArray) { -+ return new JsonListType((JsonArray)element, this.compressed); -+ } -+ -+ return dfl; -+ } -+ -+ @Override -+ public void setList(final String key, final ListType val) { -+ this.map.add(key, ((JsonListType)val).getJson()); -+ } -+ -+ @Override -+ public MapType getMap(final String key) { -+ return this.getMap(key, null); -+ } -+ -+ @Override -+ public MapType getMap(final String key, final MapType dfl) { -+ final JsonElement element = this.map.get(key); -+ if (element instanceof JsonObject) { -+ return new JsonMapType((JsonObject)element, this.compressed); -+ } -+ -+ return dfl; -+ } -+ -+ @Override -+ public void setMap(final String key, final MapType val) { -+ this.map.add(key, ((JsonMapType)val).map); -+ } -+ -+ @Override -+ public String getString(final String key) { -+ return this.getString(key, null); -+ } -+ -+ @Override -+ public String getString(final String key, final String dfl) { -+ final JsonElement element = this.map.get(key); -+ if (element instanceof JsonPrimitive) { -+ final JsonPrimitive primitive = (JsonPrimitive)element; -+ if (primitive.isString()) { -+ return primitive.getAsString(); -+ } else if (this.compressed && primitive.isNumber()) { -+ return primitive.getAsString(); -+ } -+ } -+ -+ return dfl; -+ } -+ -+ @Override -+ public String getForcedString(final String key) { -+ return this.getForcedString(key, null); -+ } -+ -+ @Override -+ public String getForcedString(final String key, final String dfl) { -+ final JsonElement element = this.map.get(key); -+ if (element instanceof JsonPrimitive) { -+ final JsonPrimitive primitive = (JsonPrimitive)element; -+ if (primitive.isString()) { -+ return primitive.getAsString(); -+ } else if (primitive.isNumber()) { -+ return primitive.getAsString(); -+ } -+ -+ return primitive.toString(); -+ } else if (element != null) { -+ return element.toString(); -+ } -+ -+ return dfl; -+ } -+ -+ @Override -+ public void setString(final String key, final String val) { -+ this.map.addProperty(key, val); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonTypeCompressedUtil.java b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonTypeCompressedUtil.java -new file mode 100644 -index 0000000000000000000000000000000000000000..9c3093b66b847b5248bde923243fce78842bf67f ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonTypeCompressedUtil.java -@@ -0,0 +1,18 @@ -+package ca.spottedleaf.dataconverter.types.json; -+ -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.TypeUtil; -+ -+public final class JsonTypeCompressedUtil implements TypeUtil { -+ -+ @Override -+ public ListType createEmptyList() { -+ return new JsonListType(true); -+ } -+ -+ @Override -+ public MapType createEmptyMap() { -+ return new JsonMapType(true); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonTypeUtil.java b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonTypeUtil.java -new file mode 100644 -index 0000000000000000000000000000000000000000..9410ae68395a09c7710bdbb2ccc6acf6633cad23 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/types/json/JsonTypeUtil.java -@@ -0,0 +1,81 @@ -+package ca.spottedleaf.dataconverter.types.json; -+ -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.TypeUtil; -+import ca.spottedleaf.dataconverter.types.nbt.NBTListType; -+import ca.spottedleaf.dataconverter.types.nbt.NBTMapType; -+import com.google.gson.JsonArray; -+import com.google.gson.JsonElement; -+import com.google.gson.JsonNull; -+import com.google.gson.JsonObject; -+import com.google.gson.JsonPrimitive; -+import com.google.gson.internal.Streams; -+import com.google.gson.stream.JsonReader; -+import java.io.StringReader; -+import java.util.Map; -+ -+public final class JsonTypeUtil implements TypeUtil { -+ -+ @Override -+ public ListType createEmptyList() { -+ return new JsonListType(false); -+ } -+ -+ @Override -+ public MapType createEmptyMap() { -+ return new JsonMapType(false); -+ } -+ -+ public static T copyJson(final T from) { -+ // This is stupidly inefficient. However, deepCopy() is not exposed in this gson version. -+ final String out = from.toString(); -+ -+ return (T)Streams.parse(new JsonReader(new StringReader(out))); -+ } -+ -+ private static Object convertToGenericNBT(final JsonElement element, final boolean compressed) { -+ if (element instanceof JsonObject) { -+ return convertJsonToNBT(new JsonMapType((JsonObject)element, compressed)); -+ } else if (element instanceof JsonArray) { -+ return convertJsonToNBT(new JsonListType((JsonArray)element, compressed)); -+ } else if (element instanceof JsonNull) { -+ return null; -+ } else { -+ final JsonPrimitive primitive = (JsonPrimitive)element; -+ if (primitive.isBoolean()) { -+ return primitive.getAsBoolean() ? Byte.valueOf((byte)1) : Byte.valueOf((byte)0); -+ } else if (primitive.isNumber()) { -+ return primitive.getAsNumber(); -+ } else if (primitive.isString()) { -+ return primitive.getAsString(); -+ } -+ } -+ -+ throw new IllegalStateException("Unrecognized type " + element); -+ } -+ -+ public static NBTMapType convertJsonToNBT(final JsonMapType json) { -+ final NBTMapType ret = new NBTMapType(); -+ for (final Map.Entry entry : json.map.entrySet()) { -+ final Object obj = convertToGenericNBT(entry.getValue(), json.compressed); -+ if (obj == null) { -+ continue; -+ } -+ -+ ret.setGeneric(entry.getKey(), obj); -+ } -+ -+ return ret; -+ } -+ -+ public static NBTListType convertJsonToNBT(final JsonListType json) { -+ final NBTListType ret = new NBTListType(); -+ -+ for (int i = 0, len = json.size(); i < len; ++i) { -+ ret.addGeneric(convertToGenericNBT(json.array.get(i), json.compressed)); -+ } -+ -+ return ret; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTListType.java b/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTListType.java -new file mode 100644 -index 0000000000000000000000000000000000000000..bf4e9ea17222cfa8f7cee9e46775302c9c2e6328 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTListType.java -@@ -0,0 +1,440 @@ -+package ca.spottedleaf.dataconverter.types.nbt; -+ -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.TypeUtil; -+import ca.spottedleaf.dataconverter.types.Types; -+import net.minecraft.nbt.ByteArrayTag; -+import net.minecraft.nbt.ByteTag; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.nbt.DoubleTag; -+import net.minecraft.nbt.FloatTag; -+import net.minecraft.nbt.IntArrayTag; -+import net.minecraft.nbt.IntTag; -+import net.minecraft.nbt.ListTag; -+import net.minecraft.nbt.LongArrayTag; -+import net.minecraft.nbt.LongTag; -+import net.minecraft.nbt.NumericTag; -+import net.minecraft.nbt.ShortTag; -+import net.minecraft.nbt.StringTag; -+import net.minecraft.nbt.Tag; -+ -+public final class NBTListType implements ListType { -+ -+ private final ListTag list; -+ -+ public NBTListType() { -+ this.list = new ListTag(); -+ } -+ -+ public NBTListType(final ListTag tag) { -+ this.list = tag; -+ } -+ -+ @Override -+ public TypeUtil getTypeUtil() { -+ return Types.NBT; -+ } -+ -+ @Override -+ public boolean equals(final Object obj) { -+ if (this == obj) { -+ return true; -+ } -+ if (obj == null || obj.getClass() != NBTListType.class) { -+ return false; -+ } -+ -+ return this.list.equals(((NBTListType)obj).list); -+ } -+ -+ @Override -+ public int hashCode() { -+ return this.list.hashCode(); -+ } -+ -+ @Override -+ public String toString() { -+ return "NBTListType{" + -+ "list=" + this.list + -+ '}'; -+ } -+ -+ public ListTag getTag() { -+ return this.list; -+ } -+ -+ @Override -+ public ListType copy() { -+ return new NBTListType(this.list.copy()); -+ } -+ -+ protected static ObjectType getType(final byte id) { -+ switch (id) { -+ case 0: // END -+ return ObjectType.NONE; -+ case 1: // BYTE -+ return ObjectType.BYTE; -+ case 2: // SHORT -+ return ObjectType.SHORT; -+ case 3: // INT -+ return ObjectType.INT; -+ case 4: // LONG -+ return ObjectType.LONG; -+ case 5: // FLOAT -+ return ObjectType.FLOAT; -+ case 6: // DOUBLE -+ return ObjectType.DOUBLE; -+ case 7: // BYTE_ARRAY -+ return ObjectType.BYTE_ARRAY; -+ case 8: // STRING -+ return ObjectType.STRING; -+ case 9: // LIST -+ return ObjectType.LIST; -+ case 10: // COMPOUND -+ return ObjectType.MAP; -+ case 11: // INT_ARRAY -+ return ObjectType.INT_ARRAY; -+ case 12: // LONG_ARRAY -+ return ObjectType.LONG_ARRAY; -+ default: -+ throw new IllegalStateException("Unknown type: " + id); -+ } -+ } -+ -+ @Override -+ public ObjectType getType() { -+ return getType(this.list.getElementType()); -+ } -+ -+ @Override -+ public int size() { -+ return this.list.size(); -+ } -+ -+ @Override -+ public void remove(final int index) { -+ this.list.remove(index); -+ } -+ -+ @Override -+ public Number getNumber(final int index) { -+ final Tag tag = this.list.get(index); // does bound checking for us -+ if (!(tag instanceof NumericTag)) { -+ throw new IllegalStateException(); -+ } -+ return ((NumericTag)tag).getAsNumber(); -+ } -+ -+ @Override -+ public byte getByte(final int index) { -+ final Tag tag = this.list.get(index); // does bound checking for us -+ if (!(tag instanceof NumericTag)) { -+ throw new IllegalStateException(); -+ } -+ return ((NumericTag)tag).getAsByte(); -+ } -+ -+ @Override -+ public void setByte(final int index, final byte to) { -+ this.list.set(index, ByteTag.valueOf(to)); -+ } -+ -+ @Override -+ public short getShort(final int index) { -+ final Tag tag = this.list.get(index); // does bound checking for us -+ if (!(tag instanceof NumericTag)) { -+ throw new IllegalStateException(); -+ } -+ return ((NumericTag)tag).getAsShort(); -+ } -+ -+ @Override -+ public void setShort(final int index, final short to) { -+ this.list.set(index, ShortTag.valueOf(to)); -+ } -+ -+ @Override -+ public int getInt(final int index) { -+ final Tag tag = this.list.get(index); // does bound checking for us -+ if (!(tag instanceof NumericTag)) { -+ throw new IllegalStateException(); -+ } -+ return ((NumericTag)tag).getAsInt(); -+ } -+ -+ @Override -+ public void setInt(final int index, final int to) { -+ this.list.set(index, IntTag.valueOf(to)); -+ } -+ -+ @Override -+ public long getLong(final int index) { -+ final Tag tag = this.list.get(index); // does bound checking for us -+ if (!(tag instanceof NumericTag)) { -+ throw new IllegalStateException(); -+ } -+ return ((NumericTag)tag).getAsLong(); -+ } -+ -+ @Override -+ public void setLong(final int index, final long to) { -+ this.list.set(index, LongTag.valueOf(to)); -+ } -+ -+ @Override -+ public float getFloat(final int index) { -+ final Tag tag = this.list.get(index); // does bound checking for us -+ if (!(tag instanceof NumericTag)) { -+ throw new IllegalStateException(); -+ } -+ return ((NumericTag)tag).getAsFloat(); -+ } -+ -+ @Override -+ public void setFloat(final int index, final float to) { -+ this.list.set(index, FloatTag.valueOf(to)); -+ } -+ -+ @Override -+ public double getDouble(final int index) { -+ final Tag tag = this.list.get(index); // does bound checking for us -+ if (!(tag instanceof NumericTag)) { -+ throw new IllegalStateException(); -+ } -+ return ((NumericTag)tag).getAsDouble(); -+ } -+ -+ @Override -+ public void setDouble(final int index, final double to) { -+ this.list.set(index, DoubleTag.valueOf(to)); -+ } -+ -+ @Override -+ public byte[] getBytes(final int index) { -+ final Tag tag = this.list.get(index); // does bound checking for us -+ if (!(tag instanceof ByteArrayTag)) { -+ throw new IllegalStateException(); -+ } -+ return ((ByteArrayTag)tag).getAsByteArray(); -+ } -+ -+ @Override -+ public void setBytes(final int index, final byte[] to) { -+ this.list.set(index, new ByteArrayTag(to)); -+ } -+ -+ @Override -+ public short[] getShorts(final int index) { -+ // NBT does not support shorts -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void setShorts(final int index, final short[] to) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public int[] getInts(final int index) { -+ final Tag tag = this.list.get(index); // does bound checking for us -+ if (!(tag instanceof IntArrayTag)) { -+ throw new IllegalStateException(); -+ } -+ return ((IntArrayTag)tag).getAsIntArray(); -+ } -+ -+ @Override -+ public void setInts(final int index, final int[] to) { -+ this.list.set(index, new IntArrayTag(to)); -+ } -+ -+ @Override -+ public long[] getLongs(final int index) { -+ final Tag tag = this.list.get(index); // does bound checking for us -+ if (!(tag instanceof LongArrayTag)) { -+ throw new IllegalStateException(); -+ } -+ return ((LongArrayTag)tag).getAsLongArray(); -+ } -+ -+ @Override -+ public void setLongs(final int index, final long[] to) { -+ this.list.set(index, new LongArrayTag(to)); -+ } -+ -+ @Override -+ public ListType getList(final int index) { -+ final Tag tag = this.list.get(index); // does bound checking for us -+ if (!(tag instanceof ListTag)) { -+ throw new IllegalStateException(); -+ } -+ return new NBTListType((ListTag)tag); -+ } -+ -+ @Override -+ public void setList(final int index, final ListType list) { -+ this.list.set(index, ((NBTListType)list).getTag()); -+ } -+ -+ @Override -+ public MapType getMap(final int index) { -+ final Tag tag = this.list.get(index); // does bound checking for us -+ if (!(tag instanceof CompoundTag)) { -+ throw new IllegalStateException(); -+ } -+ return new NBTMapType((CompoundTag)tag); -+ } -+ -+ @Override -+ public void setMap(final int index, final MapType to) { -+ this.list.set(index, ((NBTMapType)to).getTag()); -+ } -+ -+ @Override -+ public String getString(final int index) { -+ final Tag tag = this.list.get(index); // does bound checking for us -+ if (!(tag instanceof StringTag)) { -+ throw new IllegalStateException(); -+ } -+ return ((StringTag)tag).getAsString(); -+ } -+ -+ @Override -+ public void setString(final int index, final String to) { -+ this.list.set(index, StringTag.valueOf(to)); -+ } -+ -+ @Override -+ public void addByte(final byte b) { -+ this.list.add(ByteTag.valueOf(b)); -+ } -+ -+ @Override -+ public void addByte(final int index, final byte b) { -+ this.list.add(index, ByteTag.valueOf(b)); -+ } -+ -+ @Override -+ public void addShort(final short s) { -+ this.list.add(ShortTag.valueOf(s)); -+ } -+ -+ @Override -+ public void addShort(final int index, final short s) { -+ this.list.add(index, ShortTag.valueOf(s)); -+ } -+ -+ @Override -+ public void addInt(final int i) { -+ this.list.add(IntTag.valueOf(i)); -+ } -+ -+ @Override -+ public void addInt(final int index, final int i) { -+ this.list.add(index, IntTag.valueOf(i)); -+ } -+ -+ @Override -+ public void addLong(final long l) { -+ this.list.add(LongTag.valueOf(l)); -+ } -+ -+ @Override -+ public void addLong(final int index, final long l) { -+ this.list.add(index, LongTag.valueOf(l)); -+ } -+ -+ @Override -+ public void addFloat(final float f) { -+ this.list.add(FloatTag.valueOf(f)); -+ } -+ -+ @Override -+ public void addFloat(final int index, final float f) { -+ this.list.add(index, FloatTag.valueOf(f)); -+ } -+ -+ @Override -+ public void addDouble(final double d) { -+ this.list.add(DoubleTag.valueOf(d)); -+ } -+ -+ @Override -+ public void addDouble(final int index, final double d) { -+ this.list.add(index, DoubleTag.valueOf(d)); -+ } -+ -+ @Override -+ public void addByteArray(final byte[] arr) { -+ this.list.add(new ByteArrayTag(arr)); -+ } -+ -+ @Override -+ public void addByteArray(final int index, final byte[] arr) { -+ this.list.add(index, new ByteArrayTag(arr)); -+ } -+ -+ @Override -+ public void addShortArray(final short[] arr) { -+ // NBT does not support short[] -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void addShortArray(final int index, final short[] arr) { -+ // NBT does not support short[] -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void addIntArray(final int[] arr) { -+ this.list.add(new IntArrayTag(arr)); -+ } -+ -+ @Override -+ public void addIntArray(final int index, final int[] arr) { -+ this.list.add(index, new IntArrayTag(arr)); -+ } -+ -+ @Override -+ public void addLongArray(final long[] arr) { -+ this.list.add(new LongArrayTag(arr)); -+ } -+ -+ @Override -+ public void addLongArray(final int index, final long[] arr) { -+ this.list.add(index, new LongArrayTag(arr)); -+ } -+ -+ @Override -+ public void addList(final ListType list) { -+ this.list.add(((NBTListType)list).getTag()); -+ } -+ -+ @Override -+ public void addList(final int index, final ListType list) { -+ this.list.add(index, ((NBTListType)list).getTag()); -+ } -+ -+ @Override -+ public void addMap(final MapType map) { -+ this.list.add(((NBTMapType)map).getTag()); -+ } -+ -+ @Override -+ public void addMap(final int index, final MapType map) { -+ this.list.add(index, ((NBTMapType)map).getTag()); -+ } -+ -+ @Override -+ public void addString(final String string) { -+ this.list.add(StringTag.valueOf(string)); -+ } -+ -+ @Override -+ public void addString(final int index, final String string) { -+ this.list.add(index, StringTag.valueOf(string)); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTMapType.java b/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTMapType.java -new file mode 100644 -index 0000000000000000000000000000000000000000..01b6796c6ac168a82f41cf4fddbd32a1c8a86484 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTMapType.java -@@ -0,0 +1,454 @@ -+package ca.spottedleaf.dataconverter.types.nbt; -+ -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.ObjectType; -+import ca.spottedleaf.dataconverter.types.TypeUtil; -+import ca.spottedleaf.dataconverter.types.Types; -+import net.minecraft.nbt.ByteArrayTag; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.nbt.IntArrayTag; -+import net.minecraft.nbt.ListTag; -+import net.minecraft.nbt.LongArrayTag; -+import net.minecraft.nbt.NumericTag; -+import net.minecraft.nbt.StringTag; -+import net.minecraft.nbt.Tag; -+ -+import java.util.Set; -+ -+public final class NBTMapType implements MapType { -+ -+ private final CompoundTag map; -+ -+ public NBTMapType() { -+ this.map = new CompoundTag(); -+ } -+ -+ public NBTMapType(final CompoundTag tag) { -+ this.map = tag; -+ } -+ -+ @Override -+ public boolean equals(final Object obj) { -+ if (this == obj) { -+ return true; -+ } -+ if (obj == null || obj.getClass() != NBTMapType.class) { -+ return false; -+ } -+ -+ return this.map.equals(((NBTMapType)obj).map); -+ } -+ -+ @Override -+ public TypeUtil getTypeUtil() { -+ return Types.NBT; -+ } -+ -+ @Override -+ public int hashCode() { -+ return this.map.hashCode(); -+ } -+ -+ @Override -+ public String toString() { -+ return "NBTMapType{" + -+ "map=" + this.map + -+ '}'; -+ } -+ -+ @Override -+ public int size() { -+ return this.map.size(); -+ } -+ -+ @Override -+ public boolean isEmpty() { -+ return this.map.isEmpty(); -+ } -+ -+ @Override -+ public void clear() { -+ this.map.getAllKeys().clear(); -+ } -+ -+ @Override -+ public Set keys() { -+ return this.map.getAllKeys(); -+ } -+ -+ public CompoundTag getTag() { -+ return this.map; -+ } -+ -+ @Override -+ public MapType copy() { -+ return new NBTMapType(this.map.copy()); -+ } -+ -+ @Override -+ public boolean hasKey(final String key) { -+ return this.map.get(key) != null; -+ } -+ -+ @Override -+ public boolean hasKey(final String key, final ObjectType type) { -+ final Tag tag = this.map.get(key); -+ if (tag == null) { -+ return false; -+ } -+ -+ final ObjectType valueType = NBTListType.getType(tag.getId()); -+ -+ return valueType == type || (type == ObjectType.NUMBER && valueType.isNumber()); -+ } -+ -+ @Override -+ public void remove(final String key) { -+ this.map.remove(key); -+ } -+ -+ @Override -+ public Object getGeneric(final String key) { -+ final Tag tag = this.map.get(key); -+ if (tag == null) { -+ return null; -+ } -+ -+ switch (NBTListType.getType(tag.getId())) { -+ case BYTE: -+ case SHORT: -+ case INT: -+ case LONG: -+ case FLOAT: -+ case DOUBLE: -+ return ((NumericTag)tag).getAsNumber(); -+ case MAP: -+ return new NBTMapType((CompoundTag)tag); -+ case LIST: -+ return new NBTListType((ListTag)tag); -+ case STRING: -+ return ((StringTag)tag).getAsString(); -+ case BYTE_ARRAY: -+ return ((ByteArrayTag)tag).getAsByteArray(); -+ // Note: No short array tag! -+ case INT_ARRAY: -+ return ((IntArrayTag)tag).getAsIntArray(); -+ case LONG_ARRAY: -+ return ((LongArrayTag)tag).getAsLongArray(); -+ } -+ -+ throw new IllegalStateException("Unrecognized type " + tag); -+ } -+ -+ @Override -+ public Number getNumber(final String key) { -+ return this.getNumber(key, null); -+ } -+ -+ @Override -+ public Number getNumber(final String key, final Number dfl) { -+ final Tag tag = this.map.get(key); -+ if (tag instanceof NumericTag) { -+ return ((NumericTag)tag).getAsNumber(); -+ } -+ return dfl; -+ } -+ -+ @Override -+ public boolean getBoolean(final String key) { -+ return this.getByte(key) != 0; -+ } -+ -+ @Override -+ public boolean getBoolean(final String key, final boolean dfl) { -+ return this.getByte(key, dfl ? (byte)1 : (byte)0) != 0; -+ } -+ -+ @Override -+ public void setBoolean(final String key, final boolean val) { -+ this.setByte(key, val ? (byte)1 : (byte)0); -+ } -+ -+ @Override -+ public byte getByte(final String key) { -+ final Tag tag = this.map.get(key); -+ if (tag instanceof NumericTag) { -+ return ((NumericTag)tag).getAsByte(); -+ } -+ return 0; -+ } -+ -+ @Override -+ public byte getByte(final String key, final byte dfl) { -+ final Tag tag = this.map.get(key); -+ if (tag instanceof NumericTag) { -+ return ((NumericTag)tag).getAsByte(); -+ } -+ return dfl; -+ } -+ -+ @Override -+ public void setByte(final String key, final byte val) { -+ this.map.putByte(key, val); -+ } -+ -+ @Override -+ public short getShort(final String key) { -+ final Tag tag = this.map.get(key); -+ if (tag instanceof NumericTag) { -+ return ((NumericTag)tag).getAsShort(); -+ } -+ return 0; -+ } -+ -+ @Override -+ public short getShort(final String key, final short dfl) { -+ final Tag tag = this.map.get(key); -+ if (tag instanceof NumericTag) { -+ return ((NumericTag)tag).getAsShort(); -+ } -+ return dfl; -+ } -+ -+ @Override -+ public void setShort(final String key, final short val) { -+ this.map.putShort(key, val); -+ } -+ -+ @Override -+ public int getInt(final String key) { -+ final Tag tag = this.map.get(key); -+ if (tag instanceof NumericTag) { -+ return ((NumericTag)tag).getAsInt(); -+ } -+ return 0; -+ } -+ -+ @Override -+ public int getInt(final String key, final int dfl) { -+ final Tag tag = this.map.get(key); -+ if (tag instanceof NumericTag) { -+ return ((NumericTag)tag).getAsInt(); -+ } -+ return dfl; -+ } -+ -+ @Override -+ public void setInt(final String key, final int val) { -+ this.map.putInt(key, val); -+ } -+ -+ @Override -+ public long getLong(final String key) { -+ final Tag tag = this.map.get(key); -+ if (tag instanceof NumericTag) { -+ return ((NumericTag)tag).getAsLong(); -+ } -+ return 0; -+ } -+ -+ @Override -+ public long getLong(final String key, final long dfl) { -+ final Tag tag = this.map.get(key); -+ if (tag instanceof NumericTag) { -+ return ((NumericTag)tag).getAsLong(); -+ } -+ return dfl; -+ } -+ -+ @Override -+ public void setLong(final String key, final long val) { -+ this.map.putLong(key, val); -+ } -+ -+ @Override -+ public float getFloat(final String key) { -+ final Tag tag = this.map.get(key); -+ if (tag instanceof NumericTag) { -+ return ((NumericTag)tag).getAsFloat(); -+ } -+ return 0; -+ } -+ -+ @Override -+ public float getFloat(final String key, final float dfl) { -+ final Tag tag = this.map.get(key); -+ if (tag instanceof NumericTag) { -+ return ((NumericTag)tag).getAsFloat(); -+ } -+ return dfl; -+ } -+ -+ @Override -+ public void setFloat(final String key, final float val) { -+ this.map.putFloat(key, val); -+ } -+ -+ @Override -+ public double getDouble(final String key) { -+ final Tag tag = this.map.get(key); -+ if (tag instanceof NumericTag) { -+ return ((NumericTag)tag).getAsDouble(); -+ } -+ return 0; -+ } -+ -+ @Override -+ public double getDouble(final String key, final double dfl) { -+ final Tag tag = this.map.get(key); -+ if (tag instanceof NumericTag) { -+ return ((NumericTag)tag).getAsDouble(); -+ } -+ return dfl; -+ } -+ -+ @Override -+ public void setDouble(final String key, final double val) { -+ this.map.putDouble(key, val); -+ } -+ -+ @Override -+ public byte[] getBytes(final String key) { -+ return this.getBytes(key, null); -+ } -+ -+ @Override -+ public byte[] getBytes(final String key, final byte[] dfl) { -+ final Tag tag = this.map.get(key); -+ if (tag instanceof ByteArrayTag) { -+ return ((ByteArrayTag)tag).getAsByteArray(); -+ } -+ return dfl; -+ } -+ -+ @Override -+ public void setBytes(final String key, final byte[] val) { -+ this.map.putByteArray(key, val); -+ } -+ -+ @Override -+ public short[] getShorts(final String key) { -+ return this.getShorts(key, null); -+ } -+ -+ @Override -+ public short[] getShorts(final String key, final short[] dfl) { -+ // NBT does not support short array -+ return dfl; -+ } -+ -+ @Override -+ public void setShorts(final String key, final short[] val) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public int[] getInts(final String key) { -+ return this.getInts(key, null); -+ } -+ -+ @Override -+ public int[] getInts(final String key, final int[] dfl) { -+ final Tag tag = this.map.get(key); -+ if (tag instanceof IntArrayTag) { -+ return ((IntArrayTag)tag).getAsIntArray(); -+ } -+ return dfl; -+ } -+ -+ @Override -+ public void setInts(final String key, final int[] val) { -+ this.map.putIntArray(key, val); -+ } -+ -+ @Override -+ public long[] getLongs(final String key) { -+ return this.getLongs(key, null); -+ } -+ -+ @Override -+ public long[] getLongs(final String key, final long[] dfl) { -+ final Tag tag = this.map.get(key); -+ if (tag instanceof LongArrayTag) { -+ return ((LongArrayTag)tag).getAsLongArray(); -+ } -+ return dfl; -+ } -+ -+ @Override -+ public void setLongs(final String key, final long[] val) { -+ this.map.putLongArray(key, val); -+ } -+ -+ @Override -+ public ListType getListUnchecked(final String key) { -+ return this.getListUnchecked(key, null); -+ } -+ -+ @Override -+ public ListType getListUnchecked(final String key, final ListType dfl) { -+ final Tag tag = this.map.get(key); -+ if (tag instanceof ListTag) { -+ return new NBTListType((ListTag)tag); -+ } -+ return dfl; -+ } -+ -+ @Override -+ public void setList(final String key, final ListType val) { -+ this.map.put(key, ((NBTListType)val).getTag()); -+ } -+ -+ @Override -+ public MapType getMap(final String key) { -+ return this.getMap(key, null); -+ } -+ -+ @Override -+ public MapType getMap(final String key, final MapType dfl) { -+ final Tag tag = this.map.get(key); -+ if (tag instanceof CompoundTag) { -+ return new NBTMapType((CompoundTag)tag); -+ } -+ return dfl; -+ } -+ -+ @Override -+ public void setMap(final String key, final MapType val) { -+ this.map.put(key, ((NBTMapType)val).getTag()); -+ } -+ -+ @Override -+ public String getString(final String key) { -+ return this.getString(key, null); -+ } -+ -+ @Override -+ public String getString(final String key, final String dfl) { -+ final Tag tag = this.map.get(key); -+ if (tag instanceof StringTag) { -+ return ((StringTag)tag).getAsString(); -+ } -+ return dfl; -+ } -+ -+ @Override -+ public String getForcedString(final String key) { -+ return this.getForcedString(key, null); -+ } -+ -+ @Override -+ public String getForcedString(final String key, final String dfl) { -+ final Tag tag = this.map.get(key); -+ if (tag != null) { -+ return tag.getAsString(); -+ } -+ return dfl; -+ } -+ -+ @Override -+ public void setString(final String key, final String val) { -+ this.map.putString(key, val); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTTypeUtil.java b/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTTypeUtil.java -new file mode 100644 -index 0000000000000000000000000000000000000000..62c0f4073aff301bf5b3187e0d4446fd8d0ac475 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/types/nbt/NBTTypeUtil.java -@@ -0,0 +1,18 @@ -+package ca.spottedleaf.dataconverter.types.nbt; -+ -+import ca.spottedleaf.dataconverter.types.ListType; -+import ca.spottedleaf.dataconverter.types.MapType; -+import ca.spottedleaf.dataconverter.types.TypeUtil; -+ -+public final class NBTTypeUtil implements TypeUtil { -+ -+ @Override -+ public ListType createEmptyList() { -+ return new NBTListType(); -+ } -+ -+ @Override -+ public MapType createEmptyMap() { -+ return new NBTMapType(); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/util/CommandArgumentUpgrader.java b/src/main/java/ca/spottedleaf/dataconverter/util/CommandArgumentUpgrader.java -new file mode 100644 -index 0000000000000000000000000000000000000000..24db54e29281076f87ef333e23da9365ad62e589 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/util/CommandArgumentUpgrader.java -@@ -0,0 +1,435 @@ -+package ca.spottedleaf.dataconverter.util; -+ -+import ca.spottedleaf.dataconverter.minecraft.MCDataConverter; -+import ca.spottedleaf.dataconverter.minecraft.MCVersions; -+import ca.spottedleaf.dataconverter.minecraft.converters.custom.V3818_Commands; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import com.google.gson.JsonElement; -+import com.google.gson.JsonParseException; -+import com.google.gson.internal.Streams; -+import com.google.gson.stream.JsonReader; -+import com.mojang.brigadier.CommandDispatcher; -+import com.mojang.brigadier.LiteralMessage; -+import com.mojang.brigadier.ParseResults; -+import com.mojang.brigadier.StringReader; -+import com.mojang.brigadier.arguments.ArgumentType; -+import com.mojang.brigadier.context.CommandContextBuilder; -+import com.mojang.brigadier.context.ParsedArgument; -+import com.mojang.brigadier.context.StringRange; -+import com.mojang.brigadier.exceptions.CommandSyntaxException; -+import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; -+import com.mojang.brigadier.tree.ArgumentCommandNode; -+import com.mojang.brigadier.tree.CommandNode; -+import com.mojang.brigadier.tree.LiteralCommandNode; -+import com.mojang.serialization.Lifecycle; -+import it.unimi.dsi.fastutil.Pair; -+import java.lang.reflect.Field; -+import java.util.ArrayList; -+import java.util.HashMap; -+import java.util.LinkedHashMap; -+import java.util.List; -+import java.util.Map; -+import java.util.Optional; -+import java.util.function.BiFunction; -+import java.util.function.Consumer; -+import java.util.function.Function; -+import java.util.stream.Stream; -+import net.minecraft.SharedConstants; -+import net.minecraft.Util; -+import net.minecraft.commands.CommandBuildContext; -+import net.minecraft.commands.CommandSource; -+import net.minecraft.commands.CommandSourceStack; -+import net.minecraft.commands.Commands; -+import net.minecraft.commands.arguments.ComponentArgument; -+import net.minecraft.commands.arguments.item.ItemArgument; -+import net.minecraft.core.Holder; -+import net.minecraft.core.HolderLookup; -+import net.minecraft.core.HolderSet; -+import net.minecraft.core.Registry; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.nbt.Tag; -+import net.minecraft.nbt.TagParser; -+import net.minecraft.network.chat.CommonComponents; -+import net.minecraft.resources.ResourceKey; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.commands.ExecuteCommand; -+import net.minecraft.server.commands.ReturnCommand; -+import net.minecraft.tags.TagKey; -+import net.minecraft.util.GsonHelper; -+import net.minecraft.world.phys.Vec2; -+import net.minecraft.world.phys.Vec3; -+import org.jetbrains.annotations.Nullable; -+ -+public final class CommandArgumentUpgrader { -+ private final CommandDispatcher dispatcher; -+ private final CommandBuildContext context; -+ private final CommandSourceStack source; -+ private final Map, BiFunction, CommandBuildContext, ArgumentType>> replacements; -+ -+ public static CommandArgumentUpgrader upgrader_1_20_4_to_1_20_5(final int functionPermissionLevel) { -+ return new CommandArgumentUpgrader(functionPermissionLevel, builder -> { -+ builder.registerReplacement(ItemArgument.class, (argument, ctx) -> new ItemParser_1_20_4()); -+ builder.registerReplacement(ComponentArgument.class, (argument, ctx) -> new ComponentParser_1_20_4()); -+ }); -+ } -+ -+ public CommandArgumentUpgrader( -+ final int functionPermissionLevel, -+ final Consumer consumer -+ ) { -+ this( -+ new Commands(Commands.CommandSelection.DEDICATED, makeDummyCommandBuildContext()).getDispatcher(), -+ functionPermissionLevel, -+ consumer -+ ); -+ } -+ -+ private CommandArgumentUpgrader( -+ final CommandDispatcher dispatcher, -+ final int functionPermissionLevel, -+ final Consumer consumer -+ ) { -+ final ReplacementsBuilder builder = new ReplacementsBuilder(); -+ consumer.accept(builder); -+ this.replacements = Map.copyOf(builder.replacements); -+ -+ final CommandBuildContext context = makeDummyCommandBuildContext(); -+ this.dispatcher = new CommandDispatcher<>(); -+ this.context = context; -+ final List> aliases = new ArrayList<>(); -+ for (final CommandNode child : dispatcher.getRoot().getChildren()) { -+ final CopyResult result = this.copyCommand(this.dispatcher.getRoot(), child, null); -+ if (result.replaced()) { -+ this.dispatcher.getRoot().addChild(result.root); -+ } -+ aliases.addAll(result.aliases); -+ } -+ aliases.forEach(redirectNode -> { -+ final CommandNode toNode = this.dispatcher.getRoot() -+ .getChild(redirectNode.getRedirect().getName()); -+ if (toNode != null) { -+ this.dispatcher.getRoot().addChild( -+ new LiteralCommandNode<>( -+ redirectNode.getName(), -+ null, -+ null, -+ toNode, -+ redirectNode.getRedirectModifier(), -+ redirectNode.isFork() -+ ) -+ ); -+ } -+ }); -+ ExecuteCommand.register(this.dispatcher, context); -+ ReturnCommand.register(this.dispatcher); -+ // This looks weird, but it's what vanilla does when loading functions for datapacks -+ this.source = new CommandSourceStack( -+ CommandSource.NULL, -+ Vec3.ZERO, -+ Vec2.ZERO, -+ null, -+ functionPermissionLevel, -+ "", -+ CommonComponents.EMPTY, -+ null, -+ null -+ ); -+ } -+ -+ public static final class ReplacementsBuilder { -+ private final Map, BiFunction, CommandBuildContext, ArgumentType>> replacements = -+ new HashMap<>(); -+ -+ private ReplacementsBuilder() { -+ } -+ -+ @SuppressWarnings({"unchecked", "rawtypes"}) -+ public > void registerReplacement( -+ final Class type, -+ final BiFunction> upgrader -+ ) { -+ this.replacements.put(type, (BiFunction) upgrader); -+ } -+ } -+ -+ public record UpgradedArgument(String upgraded) {} -+ -+ private static final class ItemParser_1_20_4 implements ArgumentType { -+ @Override -+ public UpgradedArgument parse(final StringReader reader) throws CommandSyntaxException { -+ final ResourceLocation id = ResourceLocation.read(reader); -+ -+ final CompoundTag itemNBT = new CompoundTag(); -+ itemNBT.putString("id", id.toString()); -+ itemNBT.putInt("Count", 1); -+ -+ if (reader.canRead() && reader.peek() == '{') { -+ itemNBT.put("tag", new TagParser(reader).readStruct()); -+ } -+ -+ final CompoundTag converted = MCDataConverter.convertTag( -+ MCTypeRegistry.ITEM_STACK, itemNBT, MCVersions.V1_20_4, SharedConstants.getCurrentVersion().getDataVersion().getVersion() -+ ); -+ -+ final String newId = converted.getString("id"); -+ -+ if (converted.contains("components", Tag.TAG_COMPOUND)) { -+ return new UpgradedArgument(newId + V3818_Commands.toCommandFormat(converted.getCompound("components"))); -+ } else { -+ return new UpgradedArgument(newId); -+ } -+ } -+ } -+ -+ private static final class ComponentParser_1_20_4 implements ArgumentType { -+ private static final Field JSON_READER_POS = Util.make(() -> { -+ try { -+ final Field field = JsonReader.class.getDeclaredField("pos"); -+ field.setAccessible(true); -+ return field; -+ } catch (final NoSuchFieldException var1) { -+ throw new IllegalStateException("Couldn't get field 'pos' for JsonReader", var1); -+ } -+ }); -+ -+ private static final Field JSON_READER_LINESTART = Util.make(() -> { -+ try { -+ final Field field = JsonReader.class.getDeclaredField("lineStart"); -+ field.setAccessible(true); -+ return field; -+ } catch (final NoSuchFieldException var1) { -+ throw new IllegalStateException("Couldn't get field 'lineStart' for JsonReader", var1); -+ } -+ }); -+ -+ @Override -+ public UpgradedArgument parse(final StringReader reader) throws CommandSyntaxException { -+ final JsonElement element; -+ try { -+ element = parseJson(reader); -+ } catch (final Exception e) { -+ throw new SimpleCommandExceptionType(new LiteralMessage(e.getMessage())).createWithContext(reader); -+ } -+ V3818_Commands.walkComponent(element); -+ return new UpgradedArgument(GsonHelper.toStableString(element)); -+ } -+ -+ public static JsonElement parseJson(final StringReader stringReader) { -+ final JsonReader jsonReader = new JsonReader(new java.io.StringReader(stringReader.getRemaining())); -+ jsonReader.setLenient(false); -+ -+ final JsonElement jsonElement; -+ try { -+ jsonElement = Streams.parse(jsonReader); -+ } catch (final StackOverflowError var9) { -+ throw new JsonParseException(var9); -+ } finally { -+ stringReader.setCursor(stringReader.getCursor() + getPos(jsonReader)); -+ } -+ return jsonElement; -+ } -+ -+ private static int getPos(final JsonReader jsonReader) { -+ try { -+ return JSON_READER_POS.getInt(jsonReader) - JSON_READER_LINESTART.getInt(jsonReader); -+ } catch (IllegalAccessException var2) { -+ throw new IllegalStateException("Couldn't read position of JsonReader", var2); -+ } -+ } -+ } -+ -+ // important: leadingSlash should not just be the result of a startsWith on command, -+ // it should reflect whether the command use is in a place that will skip a leading slash when parsing -+ public String upgradeCommandArguments(final String command, final boolean leadingSlash) { -+ final StringReader reader = new StringReader(command); -+ if (leadingSlash && reader.peek() == '/') { -+ reader.skip(); -+ } -+ final ParseResults parseResult = this.dispatcher.parse(reader, this.source); -+ if (!parseResult.getExceptions().isEmpty()) { -+ return command; -+ } -+ final Map replacements = new LinkedHashMap<>(); -+ final List>> mergedArguments = new ArrayList<>(); -+ addArguments(mergedArguments, parseResult.getContext()); -+ mergedArguments.forEach(pair -> { -+ if (pair.value().getResult() instanceof UpgradedArgument upgraded) { -+ replacements.put(pair.value().getRange(), upgraded.upgraded()); -+ } -+ }); -+ String upgradedCommand = command; -+ while (!replacements.isEmpty()) { -+ final Map.Entry next = replacements.entrySet().iterator().next(); -+ replacements.remove(next.getKey()); -+ upgradedCommand = upgradedCommand.substring(0, next.getKey().getStart()) + next.getValue() + upgradedCommand.substring(next.getKey().getEnd()); -+ // Update the offsets for the remaining replacements -+ final int diff = next.getValue().length() - next.getKey().getLength(); -+ final Map replacementsCopy = new LinkedHashMap<>(replacements); -+ replacements.clear(); -+ replacementsCopy.forEach((range, value) -> { -+ replacements.put(new StringRange(range.getStart() + diff, range.getEnd() + diff), value); -+ }); -+ } -+ return upgradedCommand; -+ } -+ -+ public String upgradeSingleArgument( -+ final Function> argumentFactory, -+ final String input -+ ) { -+ final ArgumentType argument = argumentFactory.apply(this.context); -+ final ArgumentType replaced = this.replaceArgumentType(this.context, argument); -+ if (argument == replaced) { -+ return input; -+ } -+ try { -+ final UpgradedArgument parsed = (UpgradedArgument) replaced.parse(new StringReader(input)); -+ return parsed.upgraded(); -+ } catch (final CommandSyntaxException e) { -+ return input; -+ } -+ } -+ -+ private static void addArguments( -+ final List>> mergedArguments, -+ final @Nullable CommandContextBuilder context -+ ) { -+ if (context == null) { -+ return; -+ } -+ context.getArguments().forEach((name, argument) -> mergedArguments.add(Pair.of(name, argument))); -+ addArguments(mergedArguments, context.getChild()); -+ } -+ -+ private ArgumentType replaceArgumentType(final CommandBuildContext ctx, final ArgumentType type) { -+ final BiFunction, CommandBuildContext, ArgumentType> upgrader = -+ this.replacements.get(type.getClass()); -+ if (upgrader != null) { -+ return upgrader.apply(type, ctx); -+ } -+ return type; -+ } -+ -+ record CopyResult( -+ CommandNode root, -+ boolean replaced, -+ List> aliases -+ ) { -+ CopyResult replacedResult() { -+ if (this.replaced) { -+ return this; -+ } -+ return new CopyResult(this.root, true, new ArrayList<>(this.aliases)); -+ } -+ } -+ -+ private CopyResult copyCommand( -+ final CommandNode parent, -+ final CommandNode node, -+ @Nullable CopyResult result -+ ) { -+ final CommandNode copy; -+ final boolean replaced; -+ if (node instanceof LiteralCommandNode literal) { -+ if (node.getName().equals("execute") || node.getName().equals("return")) { -+ return new CopyResult(parent, false, new ArrayList<>()); -+ } -+ if (node.getRedirect() != null) { -+ if (result != null) { -+ throw new IllegalStateException("Cannot handle non-root redirects"); -+ } -+ final List> aliases = new ArrayList<>(); -+ aliases.add(node); -+ return new CopyResult(parent, false, aliases); -+ } -+ copy = new LiteralCommandNode<>( -+ literal.getLiteral(), -+ node.getCommand(), -+ node.getRequirement(), -+ null, -+ node.getRedirectModifier(), -+ node.isFork() -+ ); -+ replaced = false; -+ } else if (node instanceof ArgumentCommandNode) { -+ final ArgumentCommandNode argument = -+ (ArgumentCommandNode) node; -+ final ArgumentType replacedType = this.replaceArgumentType(this.context, argument.getType()); -+ replaced = replacedType != argument.getType(); -+ copy = new ArgumentCommandNode<>( -+ node.getName(), -+ replacedType, -+ node.getCommand(), -+ node.getRequirement(), -+ null, -+ node.getRedirectModifier(), -+ node.isFork(), -+ argument.getCustomSuggestions() -+ ); -+ } else { -+ throw new UnsupportedOperationException(); -+ } -+ if (result == null) { -+ result = new CopyResult(copy, false, new ArrayList<>()); -+ } -+ if (replaced) { -+ result = result.replacedResult(); -+ } -+ for (final CommandNode child : node.getChildren()) { -+ result = this.copyCommand(copy, child, result); -+ } -+ if (parent != this.dispatcher.getRoot()) { -+ parent.addChild(copy); -+ } -+ return result; -+ } -+ -+ private static CommandBuildContext makeDummyCommandBuildContext() { -+ return Commands.createValidationContext( -+ new HolderLookup.Provider() { -+ -+ @Override -+ public Stream>> listRegistries() { -+ return Stream.of(); -+ } -+ -+ @Override -+ public Optional> lookup( -+ final ResourceKey> registryRef -+ ) { -+ return Optional.of(new HolderLookup.RegistryLookup() { -+ @Override -+ public ResourceKey> key() { -+ return registryRef; -+ } -+ -+ @Override -+ public Lifecycle registryLifecycle() { -+ return Lifecycle.stable(); -+ } -+ -+ @Override -+ public Stream> listElements() { -+ return Stream.of(); -+ } -+ -+ @Override -+ public Stream> listTags() { -+ return Stream.of(); -+ } -+ -+ @Override -+ public Optional> get(final ResourceKey key) { -+ return Optional.of(Holder.Reference.createStandAlone(this, key)); -+ } -+ -+ @Override -+ public Optional> get(final TagKey tag) { -+ return Optional.of(HolderSet.emptyNamed(this, tag)); -+ } -+ }); -+ } -+ } -+ ); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/util/Int2IntArraySortedMap.java b/src/main/java/ca/spottedleaf/dataconverter/util/Int2IntArraySortedMap.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6596de3d9ebae583c252aa061f0cfdf8778ea1a5 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/util/Int2IntArraySortedMap.java -@@ -0,0 +1,77 @@ -+package ca.spottedleaf.dataconverter.util; -+ -+import it.unimi.dsi.fastutil.ints.Int2IntFunction; -+ -+import java.util.Arrays; -+ -+public class Int2IntArraySortedMap { -+ -+ protected int[] key; -+ protected int[] val; -+ protected int size; -+ -+ public Int2IntArraySortedMap() { -+ this.key = new int[8]; -+ this.val = new int[8]; -+ } -+ -+ public int put(final int key, final int value) { -+ final int index = Arrays.binarySearch(this.key, 0, this.size, key); -+ if (index >= 0) { -+ final int current = this.val[index]; -+ this.val[index] = value; -+ return current; -+ } -+ final int insert = -(index + 1); -+ // shift entries down -+ if (this.size >= this.val.length) { -+ this.key = Arrays.copyOf(this.key, this.key.length * 2); -+ this.val = Arrays.copyOf(this.val, this.val.length * 2); -+ } -+ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); -+ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); -+ ++this.size; -+ -+ this.key[insert] = key; -+ this.val[insert] = value; -+ -+ return 0; -+ } -+ -+ public int computeIfAbsent(final int key, final Int2IntFunction producer) { -+ final int index = Arrays.binarySearch(this.key, 0, this.size, key); -+ if (index >= 0) { -+ return this.val[index]; -+ } -+ final int insert = -(index + 1); -+ // shift entries down -+ if (this.size >= this.val.length) { -+ this.key = Arrays.copyOf(this.key, this.key.length * 2); -+ this.val = Arrays.copyOf(this.val, this.val.length * 2); -+ } -+ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); -+ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); -+ ++this.size; -+ -+ this.key[insert] = key; -+ -+ return this.val[insert] = producer.apply(key); -+ } -+ -+ public int get(final int key) { -+ final int index = Arrays.binarySearch(this.key, 0, this.size, key); -+ if (index < 0) { -+ return 0; -+ } -+ return this.val[index]; -+ } -+ -+ public int getFloor(final int key) { -+ final int index = Arrays.binarySearch(this.key, 0, this.size, key); -+ if (index < 0) { -+ final int insert = -(index + 1) - 1; -+ return insert < 0 ? 0 : this.val[insert]; -+ } -+ return this.val[index]; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/util/Int2ObjectArraySortedMap.java b/src/main/java/ca/spottedleaf/dataconverter/util/Int2ObjectArraySortedMap.java -new file mode 100644 -index 0000000000000000000000000000000000000000..de9d632489609136c712a9adaee941fd38fad440 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/util/Int2ObjectArraySortedMap.java -@@ -0,0 +1,74 @@ -+package ca.spottedleaf.dataconverter.util; -+ -+import java.util.Arrays; -+import java.util.function.IntFunction; -+ -+public class Int2ObjectArraySortedMap { -+ -+ protected int[] key; -+ protected V[] val; -+ protected int size; -+ -+ public Int2ObjectArraySortedMap() { -+ this.key = new int[8]; -+ this.val = (V[])new Object[8]; -+ } -+ -+ public V put(final int key, final V value) { -+ final int index = Arrays.binarySearch(this.key, 0, this.size, key); -+ if (index >= 0) { -+ final V current = this.val[index]; -+ this.val[index] = value; -+ return current; -+ } -+ final int insert = -(index + 1); -+ // shift entries down -+ if (this.size >= this.val.length) { -+ this.key = Arrays.copyOf(this.key, this.key.length * 2); -+ this.val = Arrays.copyOf(this.val, this.val.length * 2); -+ } -+ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); -+ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); -+ -+ this.key[insert] = key; -+ this.val[insert] = value; -+ -+ return null; -+ } -+ -+ public V computeIfAbsent(final int key, final IntFunction producer) { -+ final int index = Arrays.binarySearch(this.key, 0, this.size, key); -+ if (index >= 0) { -+ return this.val[index]; -+ } -+ final int insert = -(index + 1); -+ // shift entries down -+ if (this.size >= this.val.length) { -+ this.key = Arrays.copyOf(this.key, this.key.length * 2); -+ this.val = Arrays.copyOf(this.val, this.val.length * 2); -+ } -+ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); -+ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); -+ -+ this.key[insert] = key; -+ -+ return this.val[insert] = producer.apply(key); -+ } -+ -+ public V get(final int key) { -+ final int index = Arrays.binarySearch(this.key, 0, this.size, key); -+ if (index < 0) { -+ return null; -+ } -+ return this.val[index]; -+ } -+ -+ public V getFloor(final int key) { -+ final int index = Arrays.binarySearch(this.key, 0, this.size, key); -+ if (index < 0) { -+ final int insert = -(index + 1); -+ return this.val[insert]; -+ } -+ return this.val[index]; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/util/IntegerUtil.java b/src/main/java/ca/spottedleaf/dataconverter/util/IntegerUtil.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4bbf38c812feeb30d2aa5f3fcf482bfcbed79d05 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/util/IntegerUtil.java -@@ -0,0 +1,239 @@ -+package ca.spottedleaf.dataconverter.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; -+ } -+ -+ public static long getDivisorNumbers(final int d) { -+ final int ad = 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 -+ -+ /* -+ Signed usage: -+ int number; -+ long magic = getDivisorNumbers(div); -+ long mul = magic >>> 32; -+ int sign = number >> 31; -+ int result = (int)(((long)number * mul) >>> magic) - sign; -+ */ -+ /* -+ Unsigned usage: -+ int number; -+ long magic = getDivisorNumbers(div); -+ long mul = magic >>> 32; -+ int result = (int)(((long)number * mul) >>> magic); -+ */ -+ -+ int p = 31; -+ -+ // all these variables are UNSIGNED! -+ int t = two31 + (d >>> 31); -+ int anc = t - 1 - (int)((t & mask)%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; -+ 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/ca/spottedleaf/dataconverter/util/Long2IntArraySortedMap.java b/src/main/java/ca/spottedleaf/dataconverter/util/Long2IntArraySortedMap.java -new file mode 100644 -index 0000000000000000000000000000000000000000..94705bb141b550589faa9a0408402d8636c61907 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/util/Long2IntArraySortedMap.java -@@ -0,0 +1,76 @@ -+package ca.spottedleaf.dataconverter.util; -+ -+import it.unimi.dsi.fastutil.longs.Long2IntFunction; -+import java.util.Arrays; -+ -+public class Long2IntArraySortedMap { -+ -+ protected long[] key; -+ protected int[] val; -+ protected int size; -+ -+ public Long2IntArraySortedMap() { -+ this.key = new long[8]; -+ this.val = new int[8]; -+ } -+ -+ public int put(final long key, final int value) { -+ final int index = Arrays.binarySearch(this.key, 0, this.size, key); -+ if (index >= 0) { -+ final int current = this.val[index]; -+ this.val[index] = value; -+ return current; -+ } -+ final int insert = -(index + 1); -+ // shift entries down -+ if (this.size >= this.val.length) { -+ this.key = Arrays.copyOf(this.key, this.key.length * 2); -+ this.val = Arrays.copyOf(this.val, this.val.length * 2); -+ } -+ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); -+ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); -+ ++this.size; -+ -+ this.key[insert] = key; -+ this.val[insert] = value; -+ -+ return 0; -+ } -+ -+ public int computeIfAbsent(final long key, final Long2IntFunction producer) { -+ final int index = Arrays.binarySearch(this.key, 0, this.size, key); -+ if (index >= 0) { -+ return this.val[index]; -+ } -+ final int insert = -(index + 1); -+ // shift entries down -+ if (this.size >= this.val.length) { -+ this.key = Arrays.copyOf(this.key, this.key.length * 2); -+ this.val = Arrays.copyOf(this.val, this.val.length * 2); -+ } -+ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); -+ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); -+ ++this.size; -+ -+ this.key[insert] = key; -+ -+ return this.val[insert] = producer.apply(key); -+ } -+ -+ public int get(final long key) { -+ final int index = Arrays.binarySearch(this.key, 0, this.size, key); -+ if (index < 0) { -+ return 0; -+ } -+ return this.val[index]; -+ } -+ -+ public int getFloor(final long key) { -+ final int index = Arrays.binarySearch(this.key, 0, this.size, key); -+ if (index < 0) { -+ final int insert = -(index + 1) - 1; -+ return insert < 0 ? 0 : this.val[insert]; -+ } -+ return this.val[index]; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/util/Long2ObjectArraySortedMap.java b/src/main/java/ca/spottedleaf/dataconverter/util/Long2ObjectArraySortedMap.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6f634c8825589a23f46ad7b54354475c9a95bd1b ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/util/Long2ObjectArraySortedMap.java -@@ -0,0 +1,76 @@ -+package ca.spottedleaf.dataconverter.util; -+ -+import java.util.Arrays; -+import java.util.function.LongFunction; -+ -+public class Long2ObjectArraySortedMap { -+ -+ protected long[] key; -+ protected V[] val; -+ protected int size; -+ -+ public Long2ObjectArraySortedMap() { -+ this.key = new long[8]; -+ this.val = (V[])new Object[8]; -+ } -+ -+ public V put(final long key, final V value) { -+ final int index = Arrays.binarySearch(this.key, 0, this.size, key); -+ if (index >= 0) { -+ final V current = this.val[index]; -+ this.val[index] = value; -+ return current; -+ } -+ final int insert = -(index + 1); -+ // shift entries down -+ if (this.size >= this.val.length) { -+ this.key = Arrays.copyOf(this.key, this.key.length * 2); -+ this.val = Arrays.copyOf(this.val, this.val.length * 2); -+ } -+ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); -+ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); -+ ++this.size; -+ -+ this.key[insert] = key; -+ this.val[insert] = value; -+ -+ return null; -+ } -+ -+ public V computeIfAbsent(final long key, final LongFunction producer) { -+ final int index = Arrays.binarySearch(this.key, 0, this.size, key); -+ if (index >= 0) { -+ return this.val[index]; -+ } -+ final int insert = -(index + 1); -+ // shift entries down -+ if (this.size >= this.val.length) { -+ this.key = Arrays.copyOf(this.key, this.key.length * 2); -+ this.val = Arrays.copyOf(this.val, this.val.length * 2); -+ } -+ System.arraycopy(this.key, insert, this.key, insert + 1, this.size - insert); -+ System.arraycopy(this.val, insert, this.val, insert + 1, this.size - insert); -+ ++this.size; -+ -+ this.key[insert] = key; -+ -+ return this.val[insert] = producer.apply(key); -+ } -+ -+ public V get(final long key) { -+ final int index = Arrays.binarySearch(this.key, 0, this.size, key); -+ if (index < 0) { -+ return null; -+ } -+ return this.val[index]; -+ } -+ -+ public V getFloor(final long key) { -+ final int index = Arrays.binarySearch(this.key, 0, this.size, key); -+ if (index < 0) { -+ final int insert = -(index + 1) - 1; -+ return insert < 0 ? null : this.val[insert]; -+ } -+ return this.val[index]; -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/dataconverter/util/NamespaceUtil.java b/src/main/java/ca/spottedleaf/dataconverter/util/NamespaceUtil.java -new file mode 100644 -index 0000000000000000000000000000000000000000..5a6536377c9c1e1753e930ff2a6bb98ea57055c7 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/dataconverter/util/NamespaceUtil.java -@@ -0,0 +1,39 @@ -+package ca.spottedleaf.dataconverter.util; -+ -+import ca.spottedleaf.dataconverter.types.MapType; -+import net.minecraft.resources.ResourceLocation; -+ -+public final class NamespaceUtil { -+ -+ private NamespaceUtil() {} -+ -+ public static void enforceForPath(final MapType data, final String path) { -+ if (data == null) { -+ return; -+ } -+ -+ final String id = data.getString(path); -+ if (id != null) { -+ final String replace = NamespaceUtil.correctNamespaceOrNull(id); -+ if (replace != null) { -+ data.setString(path, replace); -+ } -+ } -+ } -+ -+ public static String correctNamespace(final String value) { -+ if (value == null) { -+ return null; -+ } -+ final ResourceLocation resourceLocation = ResourceLocation.tryParse(value); -+ return resourceLocation != null ? resourceLocation.toString() : value; -+ } -+ -+ public static String correctNamespaceOrNull(final String value) { -+ if (value == null) { -+ return null; -+ } -+ final String correct = correctNamespace(value); -+ return correct.equals(value) ? null : correct; -+ } -+} -diff --git a/src/main/java/net/minecraft/data/structures/StructureUpdater.java b/src/main/java/net/minecraft/data/structures/StructureUpdater.java -index c0296e62fe46302e887b0c4528471e3bccde1c67..1327a33c594b56a2142d35fff8e6ef07f688797d 100644 ---- a/src/main/java/net/minecraft/data/structures/StructureUpdater.java -+++ b/src/main/java/net/minecraft/data/structures/StructureUpdater.java -@@ -25,7 +25,7 @@ public class StructureUpdater implements SnbtToNbt.Filter { - LOGGER.warn("SNBT Too old, do not forget to update: {} < {}: {}", i, 3798, name); - } - -- CompoundTag compoundTag = DataFixTypes.STRUCTURE.updateToCurrentVersion(DataFixers.getDataFixer(), nbt, i); -+ CompoundTag compoundTag = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.STRUCTURE, nbt, i, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()); // Paper - structureTemplate.load(BuiltInRegistries.BLOCK.asLookup(), compoundTag); - return structureTemplate.save(new CompoundTag()); - } -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java -index e605dbdb821b2d13217ac88426e50480a4e4741d..a62c90e10c0dfa4c6211a05c4071932756d7b218 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java -@@ -83,7 +83,7 @@ public class ChunkStorage implements AutoCloseable { - - try { - // CraftBukkit start -- if (i < 1466) { -+ if (false && i < 1466) { // Paper - no longer needed, data converter system / DFU handles it now - CompoundTag level = nbttagcompound.getCompound("Level"); - if (level.getBoolean("TerrainPopulated") && !level.getBoolean("LightPopulated")) { - ServerChunkCache cps = (generatoraccess == null) ? null : ((ServerLevel) generatoraccess).getChunkSource(); -@@ -95,7 +95,7 @@ public class ChunkStorage implements AutoCloseable { - // CraftBukkit end - - if (i < 1493) { -- nbttagcompound = DataFixTypes.CHUNK.update(this.fixerUpper, nbttagcompound, i, 1493); -+ ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.CHUNK, nbttagcompound, i, 1493); // Paper - replace chunk converter - if (nbttagcompound.getCompound("Level").getBoolean("hasLegacyStructureData")) { - LegacyStructureDataHandler persistentstructurelegacy = this.getLegacyStructureHandler(resourcekey, supplier); - -@@ -113,7 +113,7 @@ public class ChunkStorage implements AutoCloseable { - // Spigot end - - ChunkStorage.injectDatafixingContext(nbttagcompound, resourcekey, optional); -- nbttagcompound = DataFixTypes.CHUNK.updateToCurrentVersion(this.fixerUpper, nbttagcompound, Math.max(1493, i)); -+ nbttagcompound = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.CHUNK, nbttagcompound, Math.max(1493, i), SharedConstants.getCurrentVersion().getDataVersion().getVersion()); // Paper - replace chunk converter - if (i < SharedConstants.getCurrentVersion().getDataVersion().getVersion()) { - NbtUtils.addCurrentDataVersion(nbttagcompound); - } -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java -index cf44d7b48c774d4c2431d42e352b42d200ee1b71..fa4f9afb421c7924557372cbb2f20caf9e13c81c 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java -@@ -32,13 +32,30 @@ public class SimpleRegionStorage implements AutoCloseable { - return this.worker.store(pos, nbt); - } - -+ // Paper start - rewrite data conversion system -+ private ca.spottedleaf.dataconverter.minecraft.datatypes.MCDataType getDataConverterType() { -+ if (this.dataFixType == DataFixTypes.ENTITY_CHUNK) { -+ return ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.ENTITY_CHUNK; -+ } else if (this.dataFixType == DataFixTypes.POI_CHUNK) { -+ return ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.POI_CHUNK; -+ } else { -+ throw new UnsupportedOperationException("For " + this.dataFixType.name()); -+ } -+ } -+ // Paper end - rewrite data conversion system -+ - public CompoundTag upgradeChunkTag(CompoundTag nbt, int oldVersion) { -- int i = NbtUtils.getDataVersion(nbt, oldVersion); -- return this.dataFixType.updateToCurrentVersion(this.fixerUpper, nbt, i); -+ // Paper start - rewrite data conversion system -+ final int dataVer = NbtUtils.getDataVersion(nbt, oldVersion); -+ return ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(this.getDataConverterType(), nbt, dataVer, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()); -+ // Paper end - rewrite data conversion system - } - - public Dynamic upgradeChunkTag(Dynamic nbt, int oldVersion) { -- return this.dataFixType.updateToCurrentVersion(this.fixerUpper, nbt, oldVersion); -+ // Paper start - rewrite data conversion system -+ final CompoundTag converted = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(this.getDataConverterType(), (CompoundTag)nbt.getValue(), oldVersion, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()); -+ return new Dynamic<>(net.minecraft.nbt.NbtOps.INSTANCE, converted); -+ // Paper end - rewrite data conversion system - } - - public CompletableFuture synchronize(boolean sync) { -diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java -index c6181e14d85d454506534f9bbe856156c0d4a062..609100ed7aa0b23aa5a9c6fbf6878ea320bd3a93 100644 ---- a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java -+++ b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java -@@ -151,7 +151,7 @@ public class StructureCheck { - - CompoundTag compoundTag2; - try { -- compoundTag2 = DataFixTypes.CHUNK.updateToCurrentVersion(this.fixerUpper, compoundTag, i); -+ compoundTag2 = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.CHUNK, compoundTag, i, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()); // Paper - replace chunk converter - } catch (Exception var12) { - LOGGER.warn("Failed to partially datafix chunk {}", pos, var12); - return StructureCheckResult.CHUNK_LOAD_NEEDED; -diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager.java b/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager.java -index 39b4f29fb97464c2021b857bccb2fa933d4003d7..4d9dbf27c7c7d57a06e5f12b7fe30723d2cb69ef 100644 ---- a/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager.java -+++ b/src/main/java/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager.java -@@ -233,7 +233,7 @@ public class StructureTemplateManager { - public StructureTemplate readStructure(CompoundTag nbt) { - StructureTemplate structureTemplate = new StructureTemplate(); - int i = NbtUtils.getDataVersion(nbt, 500); -- structureTemplate.load(this.blockLookup, DataFixTypes.STRUCTURE.updateToCurrentVersion(this.fixerUpper, nbt, i)); -+ structureTemplate.load(this.blockLookup, ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.STRUCTURE, nbt, i, SharedConstants.getCurrentVersion().getDataVersion().getVersion())); // Paper - return structureTemplate; - } - -diff --git a/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java b/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java -index e69d805701def93466ad7fa94f019ec4610efb47..427ee4d6f12a7abd8da0c65e0b9081b25824df40 100644 ---- a/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java -+++ b/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java -@@ -277,12 +277,21 @@ public class LevelStorageSource { - static Dynamic readLevelDataTagFixed(Path path, DataFixer dataFixer) throws IOException { - CompoundTag nbttagcompound = LevelStorageSource.readLevelDataTagRaw(path); - CompoundTag nbttagcompound1 = nbttagcompound.getCompound("Data"); -- int i = NbtUtils.getDataVersion(nbttagcompound1, -1); -+ int i = NbtUtils.getDataVersion(nbttagcompound1, -1); final int version = i; // Paper - obfuscation helpers - Dynamic dynamic = DataFixTypes.LEVEL.updateToCurrentVersion(dataFixer, new Dynamic(NbtOps.INSTANCE, nbttagcompound1), i); - -+ // Paper start - replace data conversion system - dynamic = dynamic.update("Player", (dynamic1) -> { -- return DataFixTypes.PLAYER.updateToCurrentVersion(dataFixer, dynamic1, i); -+ return new Dynamic<>( -+ NbtOps.INSTANCE, -+ ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag( -+ ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.PLAYER, -+ (net.minecraft.nbt.CompoundTag)dynamic1.getValue(), -+ version, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion() -+ ) -+ ); - }); -+ // Paper end - replace data conversion system - dynamic = dynamic.update("WorldGenSettings", (dynamic1) -> { - return DataFixTypes.WORLD_GEN_SETTINGS.updateToCurrentVersion(dataFixer, dynamic1, i); - }); -diff --git a/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java b/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java -index 1d287dd7379e56f7fd4b425880b850cd843f5789..8ab7ca373a885fbe658013c9c6a2e38d32d77bb2 100644 ---- a/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java -+++ b/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java -@@ -137,7 +137,7 @@ public class PlayerDataStorage { - }).map((nbttagcompound) -> { - int i = NbtUtils.getDataVersion(nbttagcompound, -1); - -- nbttagcompound = DataFixTypes.PLAYER.updateToCurrentVersion(this.fixerUpper, nbttagcompound, i); -+ nbttagcompound = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.PLAYER, nbttagcompound, i, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()); // Paper - rewrite data conversion system - // entityhuman.load(nbttagcompound); // CraftBukkit - handled above - return nbttagcompound; - }); -diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -index 2f44452631256e3d6abce02f861cc5bfa5208b53..ca1240c2534f1e6c7182996fde23e58f9c252960 100644 ---- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -+++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -@@ -526,7 +526,7 @@ public final class CraftMagicNumbers implements UnsafeValues { - - net.minecraft.nbt.CompoundTag compound = deserializeNbtFromBytes(data); - final int dataVersion = compound.getInt("DataVersion"); -- compound = (net.minecraft.nbt.CompoundTag) MinecraftServer.getServer().fixerUpper.update(References.ITEM_STACK, new Dynamic<>(NbtOps.INSTANCE, compound), dataVersion, this.getDataVersion()).getValue(); -+ compound = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.ITEM_STACK, compound, dataVersion, this.getDataVersion()); // Paper - replace data conversion system - return CraftItemStack.asCraftMirror(net.minecraft.world.item.ItemStack.parse(MinecraftServer.getServer().registryAccess(), compound).orElseThrow()); - } - -@@ -547,7 +547,7 @@ public final class CraftMagicNumbers implements UnsafeValues { - - net.minecraft.nbt.CompoundTag compound = deserializeNbtFromBytes(data); - int dataVersion = compound.getInt("DataVersion"); -- compound = (net.minecraft.nbt.CompoundTag) MinecraftServer.getServer().fixerUpper.update(References.ENTITY, new Dynamic<>(NbtOps.INSTANCE, compound), dataVersion, this.getDataVersion()).getValue(); -+ compound = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.ENTITY, compound, dataVersion, this.getDataVersion()); - if (!preserveUUID) { - // Generate a new UUID so we don't have to worry about deserializing the same entity twice - compound.remove("UUID"); diff --git a/patches/server/0981-Suspicious-Effect-Entry-API.patch b/patches/server/0981-Suspicious-Effect-Entry-API.patch new file mode 100644 index 0000000000..3d193a8a75 --- /dev/null +++ b/patches/server/0981-Suspicious-Effect-Entry-API.patch @@ -0,0 +1,222 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> +Date: Sun, 3 Mar 2024 19:43:40 +0100 +Subject: [PATCH] Suspicious Effect Entry API + +Exposes a new suspicious effect entry type that properly represents +storable effects in the context of suspicious effects as they only +define the potion effect type and duration. + +This differentiates them from the existing PotionEffect API found in +bukkit and hence clarifies that storable values in the parts of the API +in which it replaces PotionEffect. + +Co-authored-by: Yannick Lamprecht + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMushroomCow.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMushroomCow.java +index 9cc81bcccbf1141f66fedada1359b7c0dfa8e22a..5c5b64bd058684520fa175bfd10622ff57856b7c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMushroomCow.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMushroomCow.java +@@ -37,16 +37,24 @@ public class CraftMushroomCow extends CraftCow implements MushroomCow, io.paperm + @Override + public boolean addEffectToNextStew(PotionEffect potionEffect, boolean overwrite) { + Preconditions.checkArgument(potionEffect != null, "PotionEffect cannot be null"); +- MobEffectInstance minecraftPotionEffect = CraftPotionUtil.fromBukkit(potionEffect); +- if (!overwrite && this.hasEffectForNextStew(potionEffect.getType())) { ++ // Paper start - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta ++ return this.addEffectToNextStew(io.papermc.paper.potion.SuspiciousEffectEntry.create(potionEffect.getType(), potionEffect.getDuration()), overwrite); ++ } ++ ++ @Override ++ public boolean addEffectToNextStew(io.papermc.paper.potion.SuspiciousEffectEntry suspiciousEffectEntry, boolean overwrite) { ++ Preconditions.checkArgument(suspiciousEffectEntry != null, "SuspiciousEffectEntry cannot be null"); ++ Holder minecraftPotionEffect = CraftPotionEffectType.bukkitToMinecraftHolder(suspiciousEffectEntry.effect()); ++ // Paper end - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta ++ if (!overwrite && this.hasEffectForNextStew(suspiciousEffectEntry.effect())) { + return false; + } + SuspiciousStewEffects stewEffects = this.getHandle().stewEffects; + if (stewEffects == null) { + stewEffects = SuspiciousStewEffects.EMPTY; + } +- SuspiciousStewEffects.Entry recordSuspiciousEffect = new SuspiciousStewEffects.Entry(minecraftPotionEffect.getEffect(), minecraftPotionEffect.getDuration()); +- this.removeEffectFromNextStew(potionEffect.getType()); // Avoid duplicates of effects ++ SuspiciousStewEffects.Entry recordSuspiciousEffect = new SuspiciousStewEffects.Entry(minecraftPotionEffect, suspiciousEffectEntry.duration()); // Paper - sus effect entry API ++ this.removeEffectFromNextStew(suspiciousEffectEntry.effect()); // Avoid duplicates of effects // Paper - sus effect entry API + this.getHandle().stewEffects = stewEffects.withEffectAdded(recordSuspiciousEffect); + return true; + } +@@ -101,6 +109,43 @@ public class CraftMushroomCow extends CraftCow implements MushroomCow, io.paperm + this.getHandle().setVariant(net.minecraft.world.entity.animal.MushroomCow.MushroomType.values()[variant.ordinal()]); + } + ++ // Paper start ++ @Override ++ public List getStewEffects() { ++ if (this.getHandle().stewEffects == null) { ++ return List.of(); ++ } ++ ++ final List effectEntries = new java.util.ArrayList<>(this.getHandle().stewEffects.effects().size()); ++ for (final SuspiciousStewEffects.Entry effect : this.getHandle().stewEffects.effects()) { ++ effectEntries.add(io.papermc.paper.potion.SuspiciousEffectEntry.create( ++ org.bukkit.craftbukkit.potion.CraftPotionEffectType.minecraftHolderToBukkit(effect.effect()), ++ effect.duration() ++ )); ++ } ++ ++ return java.util.Collections.unmodifiableList(effectEntries); ++ } ++ ++ @Override ++ public void setStewEffects(final List effects) { ++ if (effects.isEmpty()) { ++ this.getHandle().stewEffects = null; ++ return; ++ } ++ ++ List nmsPairs = new java.util.ArrayList<>(effects.size()); ++ for (final io.papermc.paper.potion.SuspiciousEffectEntry effect : effects) { ++ nmsPairs.add(new SuspiciousStewEffects.Entry( ++ org.bukkit.craftbukkit.potion.CraftPotionEffectType.bukkitToMinecraftHolder(effect.effect()), ++ effect.duration() ++ )); ++ } ++ ++ this.getHandle().stewEffects = new SuspiciousStewEffects(nmsPairs); ++ } ++ // Paper end ++ + @Override + public String toString() { + return "CraftMushroomCow"; +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaSuspiciousStew.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaSuspiciousStew.java +index 8fc3cd507d333d2bdea759d7c102a56e88ad5f5a..4825edab85c7969e96018eb9ff47ef0fb3703c69 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaSuspiciousStew.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaSuspiciousStew.java +@@ -22,7 +22,7 @@ public class CraftMetaSuspiciousStew extends CraftMetaItem implements Suspicious + + static final ItemMetaKeyType EFFECTS = new ItemMetaKeyType<>(DataComponents.SUSPICIOUS_STEW_EFFECTS, "effects"); + +- private List customEffects; ++ private List customEffects; // Paper - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta + + CraftMetaSuspiciousStew(CraftMetaItem meta) { + super(meta); +@@ -48,7 +48,7 @@ public class CraftMetaSuspiciousStew extends CraftMetaItem implements Suspicious + continue; + } + int duration = effect.duration(); +- this.customEffects.add(new PotionEffect(type, duration, 0)); ++ this.customEffects.add(io.papermc.paper.potion.SuspiciousEffectEntry.create(type, duration)); // Paper - use suspicious effect entry for suspicious stew meta + } + }); + } +@@ -74,8 +74,8 @@ public class CraftMetaSuspiciousStew extends CraftMetaItem implements Suspicious + if (this.customEffects != null) { + List effectList = new ArrayList<>(); + +- for (PotionEffect effect : this.customEffects) { +- effectList.add(new net.minecraft.world.item.component.SuspiciousStewEffects.Entry(CraftPotionEffectType.bukkitToMinecraftHolder(effect.getType()), effect.getDuration())); ++ for (io.papermc.paper.potion.SuspiciousEffectEntry effect : this.customEffects) { ++ effectList.add(new net.minecraft.world.item.component.SuspiciousStewEffects.Entry(CraftPotionEffectType.bukkitToMinecraftHolder(effect.effect()), effect.duration())); // Paper - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta + } + tag.put(CraftMetaSuspiciousStew.EFFECTS, new SuspiciousStewEffects(effectList)); + } +@@ -112,7 +112,7 @@ public class CraftMetaSuspiciousStew extends CraftMetaItem implements Suspicious + @Override + public List getCustomEffects() { + if (this.hasCustomEffects()) { +- return ImmutableList.copyOf(this.customEffects); ++ return this.customEffects.stream().map(suspiciousEffectEntry -> suspiciousEffectEntry.effect().createEffect(suspiciousEffectEntry.duration(), 0)).toList(); // Paper - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta + } + return ImmutableList.of(); + } +@@ -120,27 +120,46 @@ public class CraftMetaSuspiciousStew extends CraftMetaItem implements Suspicious + @Override + public boolean addCustomEffect(PotionEffect effect, boolean overwrite) { + Preconditions.checkArgument(effect != null, "Potion effect cannot be null"); ++ return addCustomEffect(io.papermc.paper.potion.SuspiciousEffectEntry.create(effect.getType(), effect.getDuration()), overwrite); // Paper - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta ++ } + +- int index = this.indexOfEffect(effect.getType()); +- if (index != -1) { ++ // Paper start - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta ++ @Override ++ public boolean addCustomEffect(final io.papermc.paper.potion.SuspiciousEffectEntry suspiciousEffectEntry, final boolean overwrite) { ++ Preconditions.checkArgument(suspiciousEffectEntry != null, "Suspicious effect entry cannot be null"); ++ final List matchingEffects = this.customEffects.stream().filter( ++ entry -> entry.effect() == suspiciousEffectEntry.effect() ++ ).toList(); ++ if (!matchingEffects.isEmpty()) { + if (overwrite) { +- PotionEffect old = this.customEffects.get(index); +- if (old.getDuration() == effect.getDuration()) { ++ boolean foundMatchingDuration = false; ++ boolean mutated = false; ++ for (final io.papermc.paper.potion.SuspiciousEffectEntry matchingEffect : matchingEffects) { ++ if (matchingEffect.duration() != suspiciousEffectEntry.duration()) { ++ this.customEffects.remove(suspiciousEffectEntry); ++ mutated = true; ++ } else { ++ foundMatchingDuration = true; ++ } ++ } ++ if (foundMatchingDuration && !mutated) { + return false; ++ } else if (!foundMatchingDuration) { ++ this.customEffects.add(suspiciousEffectEntry); + } +- this.customEffects.set(index, effect); + return true; +- } else { ++ } else { + return false; + } + } else { + if (this.customEffects == null) { + this.customEffects = new ArrayList<>(); + } +- this.customEffects.add(effect); ++ this.customEffects.add(suspiciousEffectEntry); + return true; + } + } ++ // Paper end - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta + + @Override + public boolean removeCustomEffect(PotionEffectType type) { +@@ -151,10 +170,12 @@ public class CraftMetaSuspiciousStew extends CraftMetaItem implements Suspicious + } + + boolean changed = false; +- Iterator iterator = this.customEffects.iterator(); ++ // Paper start - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta ++ Iterator iterator = this.customEffects.iterator(); + while (iterator.hasNext()) { +- PotionEffect effect = iterator.next(); +- if (type.equals(effect.getType())) { ++ io.papermc.paper.potion.SuspiciousEffectEntry effect = iterator.next(); ++ if (type.equals(effect.effect())) { ++ // Paper end - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta + iterator.remove(); + changed = true; + } +@@ -177,7 +198,7 @@ public class CraftMetaSuspiciousStew extends CraftMetaItem implements Suspicious + } + + for (int i = 0; i < this.customEffects.size(); i++) { +- if (this.customEffects.get(i).getType().equals(type)) { ++ if (this.customEffects.get(i).effect().equals(type)) { // Paper - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta + return i; + } + } +@@ -222,7 +243,7 @@ public class CraftMetaSuspiciousStew extends CraftMetaItem implements Suspicious + super.serialize(builder); + + if (this.hasCustomEffects()) { +- builder.put(CraftMetaSuspiciousStew.EFFECTS.BUKKIT, ImmutableList.copyOf(this.customEffects)); ++ builder.put(CraftMetaSuspiciousStew.EFFECTS.BUKKIT, ImmutableList.copyOf(com.google.common.collect.Lists.transform(this.customEffects, s -> new PotionEffect(s.effect(), s.duration(), 0)))); // Paper - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta - convert back to potion effect for bukkit legacy item serialisation to maintain backwards compatibility for the written format. + } + + return builder; diff --git a/patches/server/0982-Suspicious-Effect-Entry-API.patch b/patches/server/0982-Suspicious-Effect-Entry-API.patch deleted file mode 100644 index 3d193a8a75..0000000000 --- a/patches/server/0982-Suspicious-Effect-Entry-API.patch +++ /dev/null @@ -1,222 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> -Date: Sun, 3 Mar 2024 19:43:40 +0100 -Subject: [PATCH] Suspicious Effect Entry API - -Exposes a new suspicious effect entry type that properly represents -storable effects in the context of suspicious effects as they only -define the potion effect type and duration. - -This differentiates them from the existing PotionEffect API found in -bukkit and hence clarifies that storable values in the parts of the API -in which it replaces PotionEffect. - -Co-authored-by: Yannick Lamprecht - -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftMushroomCow.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftMushroomCow.java -index 9cc81bcccbf1141f66fedada1359b7c0dfa8e22a..5c5b64bd058684520fa175bfd10622ff57856b7c 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftMushroomCow.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftMushroomCow.java -@@ -37,16 +37,24 @@ public class CraftMushroomCow extends CraftCow implements MushroomCow, io.paperm - @Override - public boolean addEffectToNextStew(PotionEffect potionEffect, boolean overwrite) { - Preconditions.checkArgument(potionEffect != null, "PotionEffect cannot be null"); -- MobEffectInstance minecraftPotionEffect = CraftPotionUtil.fromBukkit(potionEffect); -- if (!overwrite && this.hasEffectForNextStew(potionEffect.getType())) { -+ // Paper start - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta -+ return this.addEffectToNextStew(io.papermc.paper.potion.SuspiciousEffectEntry.create(potionEffect.getType(), potionEffect.getDuration()), overwrite); -+ } -+ -+ @Override -+ public boolean addEffectToNextStew(io.papermc.paper.potion.SuspiciousEffectEntry suspiciousEffectEntry, boolean overwrite) { -+ Preconditions.checkArgument(suspiciousEffectEntry != null, "SuspiciousEffectEntry cannot be null"); -+ Holder minecraftPotionEffect = CraftPotionEffectType.bukkitToMinecraftHolder(suspiciousEffectEntry.effect()); -+ // Paper end - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta -+ if (!overwrite && this.hasEffectForNextStew(suspiciousEffectEntry.effect())) { - return false; - } - SuspiciousStewEffects stewEffects = this.getHandle().stewEffects; - if (stewEffects == null) { - stewEffects = SuspiciousStewEffects.EMPTY; - } -- SuspiciousStewEffects.Entry recordSuspiciousEffect = new SuspiciousStewEffects.Entry(minecraftPotionEffect.getEffect(), minecraftPotionEffect.getDuration()); -- this.removeEffectFromNextStew(potionEffect.getType()); // Avoid duplicates of effects -+ SuspiciousStewEffects.Entry recordSuspiciousEffect = new SuspiciousStewEffects.Entry(minecraftPotionEffect, suspiciousEffectEntry.duration()); // Paper - sus effect entry API -+ this.removeEffectFromNextStew(suspiciousEffectEntry.effect()); // Avoid duplicates of effects // Paper - sus effect entry API - this.getHandle().stewEffects = stewEffects.withEffectAdded(recordSuspiciousEffect); - return true; - } -@@ -101,6 +109,43 @@ public class CraftMushroomCow extends CraftCow implements MushroomCow, io.paperm - this.getHandle().setVariant(net.minecraft.world.entity.animal.MushroomCow.MushroomType.values()[variant.ordinal()]); - } - -+ // Paper start -+ @Override -+ public List getStewEffects() { -+ if (this.getHandle().stewEffects == null) { -+ return List.of(); -+ } -+ -+ final List effectEntries = new java.util.ArrayList<>(this.getHandle().stewEffects.effects().size()); -+ for (final SuspiciousStewEffects.Entry effect : this.getHandle().stewEffects.effects()) { -+ effectEntries.add(io.papermc.paper.potion.SuspiciousEffectEntry.create( -+ org.bukkit.craftbukkit.potion.CraftPotionEffectType.minecraftHolderToBukkit(effect.effect()), -+ effect.duration() -+ )); -+ } -+ -+ return java.util.Collections.unmodifiableList(effectEntries); -+ } -+ -+ @Override -+ public void setStewEffects(final List effects) { -+ if (effects.isEmpty()) { -+ this.getHandle().stewEffects = null; -+ return; -+ } -+ -+ List nmsPairs = new java.util.ArrayList<>(effects.size()); -+ for (final io.papermc.paper.potion.SuspiciousEffectEntry effect : effects) { -+ nmsPairs.add(new SuspiciousStewEffects.Entry( -+ org.bukkit.craftbukkit.potion.CraftPotionEffectType.bukkitToMinecraftHolder(effect.effect()), -+ effect.duration() -+ )); -+ } -+ -+ this.getHandle().stewEffects = new SuspiciousStewEffects(nmsPairs); -+ } -+ // Paper end -+ - @Override - public String toString() { - return "CraftMushroomCow"; -diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaSuspiciousStew.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaSuspiciousStew.java -index 8fc3cd507d333d2bdea759d7c102a56e88ad5f5a..4825edab85c7969e96018eb9ff47ef0fb3703c69 100644 ---- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaSuspiciousStew.java -+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaSuspiciousStew.java -@@ -22,7 +22,7 @@ public class CraftMetaSuspiciousStew extends CraftMetaItem implements Suspicious - - static final ItemMetaKeyType EFFECTS = new ItemMetaKeyType<>(DataComponents.SUSPICIOUS_STEW_EFFECTS, "effects"); - -- private List customEffects; -+ private List customEffects; // Paper - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta - - CraftMetaSuspiciousStew(CraftMetaItem meta) { - super(meta); -@@ -48,7 +48,7 @@ public class CraftMetaSuspiciousStew extends CraftMetaItem implements Suspicious - continue; - } - int duration = effect.duration(); -- this.customEffects.add(new PotionEffect(type, duration, 0)); -+ this.customEffects.add(io.papermc.paper.potion.SuspiciousEffectEntry.create(type, duration)); // Paper - use suspicious effect entry for suspicious stew meta - } - }); - } -@@ -74,8 +74,8 @@ public class CraftMetaSuspiciousStew extends CraftMetaItem implements Suspicious - if (this.customEffects != null) { - List effectList = new ArrayList<>(); - -- for (PotionEffect effect : this.customEffects) { -- effectList.add(new net.minecraft.world.item.component.SuspiciousStewEffects.Entry(CraftPotionEffectType.bukkitToMinecraftHolder(effect.getType()), effect.getDuration())); -+ for (io.papermc.paper.potion.SuspiciousEffectEntry effect : this.customEffects) { -+ effectList.add(new net.minecraft.world.item.component.SuspiciousStewEffects.Entry(CraftPotionEffectType.bukkitToMinecraftHolder(effect.effect()), effect.duration())); // Paper - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta - } - tag.put(CraftMetaSuspiciousStew.EFFECTS, new SuspiciousStewEffects(effectList)); - } -@@ -112,7 +112,7 @@ public class CraftMetaSuspiciousStew extends CraftMetaItem implements Suspicious - @Override - public List getCustomEffects() { - if (this.hasCustomEffects()) { -- return ImmutableList.copyOf(this.customEffects); -+ return this.customEffects.stream().map(suspiciousEffectEntry -> suspiciousEffectEntry.effect().createEffect(suspiciousEffectEntry.duration(), 0)).toList(); // Paper - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta - } - return ImmutableList.of(); - } -@@ -120,27 +120,46 @@ public class CraftMetaSuspiciousStew extends CraftMetaItem implements Suspicious - @Override - public boolean addCustomEffect(PotionEffect effect, boolean overwrite) { - Preconditions.checkArgument(effect != null, "Potion effect cannot be null"); -+ return addCustomEffect(io.papermc.paper.potion.SuspiciousEffectEntry.create(effect.getType(), effect.getDuration()), overwrite); // Paper - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta -+ } - -- int index = this.indexOfEffect(effect.getType()); -- if (index != -1) { -+ // Paper start - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta -+ @Override -+ public boolean addCustomEffect(final io.papermc.paper.potion.SuspiciousEffectEntry suspiciousEffectEntry, final boolean overwrite) { -+ Preconditions.checkArgument(suspiciousEffectEntry != null, "Suspicious effect entry cannot be null"); -+ final List matchingEffects = this.customEffects.stream().filter( -+ entry -> entry.effect() == suspiciousEffectEntry.effect() -+ ).toList(); -+ if (!matchingEffects.isEmpty()) { - if (overwrite) { -- PotionEffect old = this.customEffects.get(index); -- if (old.getDuration() == effect.getDuration()) { -+ boolean foundMatchingDuration = false; -+ boolean mutated = false; -+ for (final io.papermc.paper.potion.SuspiciousEffectEntry matchingEffect : matchingEffects) { -+ if (matchingEffect.duration() != suspiciousEffectEntry.duration()) { -+ this.customEffects.remove(suspiciousEffectEntry); -+ mutated = true; -+ } else { -+ foundMatchingDuration = true; -+ } -+ } -+ if (foundMatchingDuration && !mutated) { - return false; -+ } else if (!foundMatchingDuration) { -+ this.customEffects.add(suspiciousEffectEntry); - } -- this.customEffects.set(index, effect); - return true; -- } else { -+ } else { - return false; - } - } else { - if (this.customEffects == null) { - this.customEffects = new ArrayList<>(); - } -- this.customEffects.add(effect); -+ this.customEffects.add(suspiciousEffectEntry); - return true; - } - } -+ // Paper end - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta - - @Override - public boolean removeCustomEffect(PotionEffectType type) { -@@ -151,10 +170,12 @@ public class CraftMetaSuspiciousStew extends CraftMetaItem implements Suspicious - } - - boolean changed = false; -- Iterator iterator = this.customEffects.iterator(); -+ // Paper start - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta -+ Iterator iterator = this.customEffects.iterator(); - while (iterator.hasNext()) { -- PotionEffect effect = iterator.next(); -- if (type.equals(effect.getType())) { -+ io.papermc.paper.potion.SuspiciousEffectEntry effect = iterator.next(); -+ if (type.equals(effect.effect())) { -+ // Paper end - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta - iterator.remove(); - changed = true; - } -@@ -177,7 +198,7 @@ public class CraftMetaSuspiciousStew extends CraftMetaItem implements Suspicious - } - - for (int i = 0; i < this.customEffects.size(); i++) { -- if (this.customEffects.get(i).getType().equals(type)) { -+ if (this.customEffects.get(i).effect().equals(type)) { // Paper - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta - return i; - } - } -@@ -222,7 +243,7 @@ public class CraftMetaSuspiciousStew extends CraftMetaItem implements Suspicious - super.serialize(builder); - - if (this.hasCustomEffects()) { -- builder.put(CraftMetaSuspiciousStew.EFFECTS.BUKKIT, ImmutableList.copyOf(this.customEffects)); -+ builder.put(CraftMetaSuspiciousStew.EFFECTS.BUKKIT, ImmutableList.copyOf(com.google.common.collect.Lists.transform(this.customEffects, s -> new PotionEffect(s.effect(), s.duration(), 0)))); // Paper - add overloads to use suspicious effect entry to mushroom cow and suspicious stew meta - convert back to potion effect for bukkit legacy item serialisation to maintain backwards compatibility for the written format. - } - - return builder; diff --git a/patches/server/0982-check-if-itemstack-is-stackable-first.patch b/patches/server/0982-check-if-itemstack-is-stackable-first.patch new file mode 100644 index 0000000000..6143642997 --- /dev/null +++ b/patches/server/0982-check-if-itemstack-is-stackable-first.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: granny +Date: Sat, 24 Feb 2024 19:33:01 -0800 +Subject: [PATCH] check if itemstack is stackable first + + +diff --git a/src/main/java/net/minecraft/world/entity/player/Inventory.java b/src/main/java/net/minecraft/world/entity/player/Inventory.java +index 1e77482a98869e464c1f0a873cff8febf7924c8c..ca7fbe4f8c1e1d2fb90095aa35be4dda3029c23e 100644 +--- a/src/main/java/net/minecraft/world/entity/player/Inventory.java ++++ b/src/main/java/net/minecraft/world/entity/player/Inventory.java +@@ -111,7 +111,7 @@ public class Inventory implements Container, Nameable { + } + + private boolean hasRemainingSpaceForItem(ItemStack existingStack, ItemStack stack) { +- return !existingStack.isEmpty() && ItemStack.isSameItemSameComponents(existingStack, stack) && existingStack.isStackable() && existingStack.getCount() < this.getMaxStackSize(existingStack); ++ return !existingStack.isEmpty() && existingStack.isStackable() && existingStack.getCount() < this.getMaxStackSize(existingStack) && ItemStack.isSameItemSameComponents(existingStack, stack); // Paper - check if itemstack is stackable first + } + + // CraftBukkit start - Watch method above! :D diff --git a/patches/server/0983-check-if-itemstack-is-stackable-first.patch b/patches/server/0983-check-if-itemstack-is-stackable-first.patch deleted file mode 100644 index 6143642997..0000000000 --- a/patches/server/0983-check-if-itemstack-is-stackable-first.patch +++ /dev/null @@ -1,19 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: granny -Date: Sat, 24 Feb 2024 19:33:01 -0800 -Subject: [PATCH] check if itemstack is stackable first - - -diff --git a/src/main/java/net/minecraft/world/entity/player/Inventory.java b/src/main/java/net/minecraft/world/entity/player/Inventory.java -index 1e77482a98869e464c1f0a873cff8febf7924c8c..ca7fbe4f8c1e1d2fb90095aa35be4dda3029c23e 100644 ---- a/src/main/java/net/minecraft/world/entity/player/Inventory.java -+++ b/src/main/java/net/minecraft/world/entity/player/Inventory.java -@@ -111,7 +111,7 @@ public class Inventory implements Container, Nameable { - } - - private boolean hasRemainingSpaceForItem(ItemStack existingStack, ItemStack stack) { -- return !existingStack.isEmpty() && ItemStack.isSameItemSameComponents(existingStack, stack) && existingStack.isStackable() && existingStack.getCount() < this.getMaxStackSize(existingStack); -+ return !existingStack.isEmpty() && existingStack.isStackable() && existingStack.getCount() < this.getMaxStackSize(existingStack) && ItemStack.isSameItemSameComponents(existingStack, stack); // Paper - check if itemstack is stackable first - } - - // CraftBukkit start - Watch method above! :D diff --git a/patches/server/0983-disable-forced-empty-world-ticks.patch b/patches/server/0983-disable-forced-empty-world-ticks.patch new file mode 100644 index 0000000000..6ef5295591 --- /dev/null +++ b/patches/server/0983-disable-forced-empty-world-ticks.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Shane Freeder +Date: Tue, 21 Mar 2023 23:51:46 +0000 +Subject: [PATCH] disable forced empty world ticks + + +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 5eab8532bfe3b7082123a12b8a3e62d25a4463d3..7dfbb3dd6100180abd2ebab4d1e9e34928666f9e 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -533,7 +533,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + this.handlingTick = false; + gameprofilerfiller.pop(); +- boolean flag1 = true || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players ++ boolean flag1 = !paperConfig().unsupportedSettings.disableWorldTickingWhenEmpty || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players // Paper - restore this + + if (flag1) { + this.resetEmptyTime(); diff --git a/patches/server/0984-Starlight.patch b/patches/server/0984-Starlight.patch new file mode 100644 index 0000000000..72b56b3d53 --- /dev/null +++ b/patches/server/0984-Starlight.patch @@ -0,0 +1,5429 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Wed, 28 Oct 2020 16:51:55 -0700 +Subject: [PATCH] Starlight + +See https://github.com/PaperMC/Starlight + +== AT == +public net.minecraft.server.level.ChunkHolder broadcast(Lnet/minecraft/network/protocol/Packet;Z)V + +diff --git a/src/main/java/ca/spottedleaf/starlight/common/light/BlockStarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/common/light/BlockStarLightEngine.java +new file mode 100644 +index 0000000000000000000000000000000000000000..3732a940d9603cf502983afbc4663113d1400be8 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/starlight/common/light/BlockStarLightEngine.java +@@ -0,0 +1,275 @@ ++package ca.spottedleaf.starlight.common.light; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.chunk.LevelChunkSection; ++import net.minecraft.world.level.chunk.LightChunkGetter; ++import net.minecraft.world.level.chunk.PalettedContainer; ++import net.minecraft.world.phys.shapes.Shapes; ++import net.minecraft.world.phys.shapes.VoxelShape; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Set; ++ ++public final class BlockStarLightEngine extends StarLightEngine { ++ ++ public BlockStarLightEngine(final Level world) { ++ super(false, world); ++ } ++ ++ @Override ++ protected boolean[] getEmptinessMap(final ChunkAccess chunk) { ++ return chunk.getBlockEmptinessMap(); ++ } ++ ++ @Override ++ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) { ++ chunk.setBlockEmptinessMap(to); ++ } ++ ++ @Override ++ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) { ++ return chunk.getBlockNibbles(); ++ } ++ ++ @Override ++ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) { ++ chunk.setBlockNibbles(to); ++ } ++ ++ @Override ++ protected boolean canUseChunk(final ChunkAccess chunk) { ++ return chunk.getStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect()); ++ } ++ ++ @Override ++ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) { ++ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); ++ if (nibble != null) { ++ // de-initialisation is not as straightforward as with sky data, since deinit of block light is typically ++ // because a block was removed - which can decrease light. with sky data, block breaking can only result ++ // in increases, and thus the existing sky block check will actually correctly propagate light through ++ // a null section. so in order to propagate decreases correctly, we can do a couple of things: not remove ++ // the data section, or do edge checks on ALL axis (x, y, z). however I do not want edge checks running ++ // for clients at all, as they are expensive. so we don't remove the section, but to maintain the appearence ++ // of vanilla data management we "hide" them. ++ nibble.setHidden(); ++ } ++ } ++ ++ @Override ++ protected void initNibble(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; ++ } ++ ++ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); ++ if (nibble == null) { ++ if (!initRemovedNibbles) { ++ throw new IllegalStateException(); ++ } else { ++ this.setNibbleInCache(chunkX, chunkY, chunkZ, new SWMRNibbleArray()); ++ } ++ } else { ++ nibble.setNonNull(); ++ } ++ } ++ ++ @Override ++ protected final void checkBlock(final LightChunkGetter 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 int currentLevel = this.getLightLevel(worldX, worldY, worldZ); ++ final BlockState blockState = this.getBlockState(worldX, worldY, worldZ); ++ final int emittedLevel = blockState.getLightEmission() & 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 BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos(); ++ protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos(); ++ ++ @Override ++ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, ++ final int expect) { ++ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ); ++ int level = centerState.getLightEmission() & 0xF; ++ ++ if (level >= (15 - 1) || level > expect) { ++ return level; ++ } ++ ++ final int sectionOffset = this.chunkSectionIndexOffset; ++ final BlockState conditionallyOpaqueState; ++ int opacity = centerState.getOpacityIfCached(); ++ ++ if (opacity == -1) { ++ this.recalcCenterPos.set(worldX, worldY, worldZ); ++ opacity = centerState.getLightBlock(lightAccess.getLevel(), 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 BlockState 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.set(offX, offY, offZ); ++ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms); ++ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms); ++ if (Shapes.faceShapeOccludes(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 LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions) { ++ for (final BlockPos pos : positions) { ++ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); ++ } ++ ++ this.performLightDecrease(lightAccess); ++ } ++ ++ protected List getSources(final LightChunkGetter lightAccess, final ChunkAccess chunk) { ++ final List sources = new ArrayList<>(); ++ ++ final int offX = chunk.getPos().x << 4; ++ final int offZ = chunk.getPos().z << 4; ++ ++ final LevelChunkSection[] sections = chunk.getSections(); ++ for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) { ++ final LevelChunkSection section = sections[sectionY - this.minSection]; ++ if (section == null || section.hasOnlyAir()) { ++ // no sources in empty sections ++ continue; ++ } ++ if (!section.maybeHas((final BlockState state) -> { ++ return state.getLightEmission() > 0; ++ })) { ++ // no light sources in palette ++ continue; ++ } ++ final PalettedContainer states = section.states; ++ final int offY = sectionY << 4; ++ ++ for (int index = 0; index < (16 * 16 * 16); ++index) { ++ final BlockState state = states.get(index); ++ if (state.getLightEmission() <= 0) { ++ continue; ++ } ++ ++ // index = x | (z << 4) | (y << 8) ++ sources.add(new BlockPos(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15))); ++ } ++ } ++ ++ return sources; ++ } ++ ++ @Override ++ public void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) { ++ // setup sources ++ final int emittedMask = this.emittedLightMask; ++ final List positions = this.getSources(lightAccess, chunk); ++ for (int i = 0, len = positions.size(); i < len; ++i) { ++ final BlockPos pos = positions.get(i); ++ final BlockState blockState = this.getBlockState(pos.getX(), pos.getY(), pos.getZ()); ++ final int emittedLight = blockState.getLightEmission() & 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/ca/spottedleaf/starlight/common/light/SWMRNibbleArray.java b/src/main/java/ca/spottedleaf/starlight/common/light/SWMRNibbleArray.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4ffb4ffe01c4628d52742c5c0bbd35220eea6294 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/starlight/common/light/SWMRNibbleArray.java +@@ -0,0 +1,440 @@ ++package ca.spottedleaf.starlight.common.light; ++ ++import net.minecraft.world.level.chunk.DataLayer; ++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 ++ protected static final int INIT_STATE_HIDDEN = 3; // initialised, but conversion to Vanilla data should be treated as if NULL ++ ++ public static final int ARRAY_SIZE = 16 * 16 * 16 / (8/4); // blocks / bytes per block ++ // 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); ++ } ++ ++ public static SWMRNibbleArray fromVanilla(final DataLayer nibble) { ++ if (nibble == null) { ++ return new SWMRNibbleArray(null, true); ++ } else if (nibble.isEmpty()) { ++ return new SWMRNibbleArray(); ++ } else { ++ return new SWMRNibbleArray(nibble.getData().clone()); // make sure we don't write to the parameter later ++ } ++ } ++ ++ protected int stateUpdating; ++ protected volatile int stateVisible; ++ ++ protected byte[] storageUpdating; ++ protected boolean updatingDirty; // only returns whether storageUpdating is dirty ++ protected volatile 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("Data of wrong length: " + bytes.length); ++ } ++ this.stateVisible = this.stateUpdating = bytes == null ? (isNullNibble ? INIT_STATE_NULL : INIT_STATE_UNINIT) : INIT_STATE_INIT; ++ this.storageUpdating = this.storageVisible = bytes; ++ } ++ ++ public SWMRNibbleArray(final byte[] bytes, final int state) { ++ if (bytes != null && bytes.length != ARRAY_SIZE) { ++ throw new IllegalArgumentException("Data of wrong length: " + bytes.length); ++ } ++ if (bytes == null && (state == INIT_STATE_INIT || state == INIT_STATE_HIDDEN)) { ++ throw new IllegalArgumentException("Data cannot be null and have state be initialised"); ++ } ++ this.stateUpdating = this.stateVisible = state; ++ this.storageUpdating = this.storageVisible = bytes; ++ } ++ ++ @Override ++ public String toString() { ++ StringBuilder stringBuilder = new StringBuilder(); ++ stringBuilder.append("State: "); ++ switch (this.stateVisible) { ++ case INIT_STATE_NULL: ++ stringBuilder.append("null"); ++ break; ++ case INIT_STATE_UNINIT: ++ stringBuilder.append("uninitialised"); ++ break; ++ case INIT_STATE_INIT: ++ stringBuilder.append("initialised"); ++ break; ++ case INIT_STATE_HIDDEN: ++ stringBuilder.append("hidden"); ++ break; ++ default: ++ stringBuilder.append("unknown"); ++ break; ++ } ++ stringBuilder.append("\nData:\n"); ++ ++ final byte[] data = this.storageVisible; ++ if (data != null) { ++ for (int i = 0; i < 4096; ++i) { ++ // Copied from NibbleArray#toString ++ final int level = ((data[i >>> 1] >>> ((i & 1) << 2)) & 0xF); ++ ++ stringBuilder.append(Integer.toHexString(level)); ++ if ((i & 15) == 15) { ++ stringBuilder.append("\n"); ++ } ++ ++ if ((i & 255) == 255) { ++ stringBuilder.append("\n"); ++ } ++ } ++ } else { ++ stringBuilder.append("null"); ++ } ++ ++ return stringBuilder.toString(); ++ } ++ ++ public SaveState getSaveState() { ++ synchronized (this) { ++ final int state = this.stateVisible; ++ final byte[] data = this.storageVisible; ++ if (state == INIT_STATE_NULL) { ++ return null; ++ } ++ if (state == INIT_STATE_UNINIT) { ++ return new SaveState(null, state); ++ } ++ final boolean zero = isAllZero(data); ++ if (zero) { ++ return state == INIT_STATE_INIT ? new SaveState(null, INIT_STATE_UNINIT) : null; ++ } else { ++ return new SaveState(data.clone(), state); ++ } ++ } ++ } ++ ++ protected static boolean isAllZero(final byte[] data) { ++ for (int i = 0; i < (ARRAY_SIZE >>> 4); ++i) { ++ byte whole = data[i << 4]; ++ ++ for (int k = 1; k < (1 << 4); ++k) { ++ whole |= data[(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.updatingDirty) { ++ if (this.storageUpdating != null) { ++ into = this.storageUpdating = allocateBytes(); ++ } else { ++ this.storageUpdating = into = allocateBytes(); ++ this.stateUpdating = INIT_STATE_INIT; ++ } ++ this.updatingDirty = true; ++ } else { ++ into = this.storageUpdating; ++ } ++ ++ 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() { ++ if (this.stateUpdating != INIT_STATE_HIDDEN) { ++ 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() { ++ if (this.stateUpdating != INIT_STATE_HIDDEN) { ++ 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_HIDDEN) { ++ this.stateUpdating = INIT_STATE_INIT; ++ return; ++ } ++ 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 void setHidden() { ++ if (this.stateUpdating == INIT_STATE_HIDDEN) { ++ return; ++ } ++ if (this.stateUpdating != INIT_STATE_INIT) { ++ this.setNull(); ++ } else { ++ this.stateUpdating = INIT_STATE_HIDDEN; ++ } ++ } ++ ++ // 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 ++ public boolean isHiddenUpdating() { ++ return this.stateUpdating == INIT_STATE_HIDDEN; ++ } ++ ++ // operation type: updating ++ public boolean isHiddenVisible() { ++ return this.stateVisible == INIT_STATE_HIDDEN; ++ } ++ ++ // 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); ++ } ++ ++ if (this.stateUpdating != INIT_STATE_HIDDEN) { ++ 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 { ++ if (this.storageUpdating != this.storageVisible) { ++ System.arraycopy(this.storageUpdating, 0, this.storageVisible, 0, ARRAY_SIZE); ++ } ++ } ++ ++ if (this.storageUpdating != this.storageVisible) { ++ freeBytes(this.storageUpdating); ++ } ++ this.storageUpdating = this.storageVisible; ++ } ++ this.updatingDirty = false; ++ this.stateVisible = this.stateUpdating; ++ } ++ ++ return true; ++ } ++ ++ // operation type: visible ++ public DataLayer toVanillaNibble() { ++ synchronized (this) { ++ switch (this.stateVisible) { ++ case INIT_STATE_HIDDEN: ++ case INIT_STATE_NULL: ++ return null; ++ case INIT_STATE_UNINIT: ++ return new DataLayer(); ++ case INIT_STATE_INIT: ++ return new DataLayer(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) { ++ // 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)); ++ } ++ ++ public static final class SaveState { ++ ++ public final byte[] data; ++ public final int state; ++ ++ public SaveState(final byte[] data, final int state) { ++ this.data = data; ++ this.state = state; ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/starlight/common/light/SkyStarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/common/light/SkyStarLightEngine.java +new file mode 100644 +index 0000000000000000000000000000000000000000..43a2cce467d29f81ba57d77c03608e57857dd579 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/starlight/common/light/SkyStarLightEngine.java +@@ -0,0 +1,709 @@ ++package ca.spottedleaf.starlight.common.light; ++ ++import ca.spottedleaf.starlight.common.util.WorldUtil; ++import it.unimi.dsi.fastutil.shorts.ShortCollection; ++import it.unimi.dsi.fastutil.shorts.ShortIterator; ++import net.minecraft.core.BlockPos; ++import net.minecraft.world.level.BlockGetter; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.chunk.LevelChunkSection; ++import net.minecraft.world.level.chunk.LightChunkGetter; ++import net.minecraft.world.phys.shapes.Shapes; ++import net.minecraft.world.phys.shapes.VoxelShape; ++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 Level world) { ++ super(true, world); ++ this.nullPropagationCheckCache = new boolean[WorldUtil.getTotalLightSections(world)]; ++ } ++ ++ @Override ++ protected void initNibble(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.initNibble(nibble, chunkX, chunkY, chunkZ, extrude); ++ } ++ ++ @Override ++ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) { ++ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); ++ if (nibble != null) { ++ nibble.setNull(); ++ } ++ } ++ ++ protected final void initNibble(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 LevelChunkSection current = this.getChunkSection(chunkX, currY, chunkZ); ++ if (current == null || current.hasOnlyAir()) { ++ 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 ++ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); ++ nibble.setNonNull(); ++ nibble.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.setNonNull(); ++ currNibble.extrudeLower(nibble); ++ break; ++ } ++ } ++ } else { ++ currNibble.setNonNull(); ++ } ++ } ++ ++ protected final void rewriteNibbleCacheForSkylight(final ChunkAccess 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.initNibble(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 ChunkAccess chunk) { ++ return chunk.getSkyEmptinessMap(); ++ } ++ ++ @Override ++ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) { ++ chunk.setSkyEmptinessMap(to); ++ } ++ ++ @Override ++ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) { ++ return chunk.getSkyNibbles(); ++ } ++ ++ @Override ++ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) { ++ chunk.setSkyNibbles(to); ++ } ++ ++ @Override ++ protected boolean canUseChunk(final ChunkAccess chunk) { ++ // can only use chunks for sky stuff if their sections have been init'd ++ return chunk.getStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect()); ++ } ++ ++ @Override ++ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess 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 LightChunkGetter lightAccess, final ChunkAccess 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); ++ } ++ ++ @Override ++ protected void checkBlock(final LightChunkGetter 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)) ++ ); ++ } ++ ++ protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos(); ++ protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos(); ++ ++ @Override ++ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, ++ final int expect) { ++ if (expect == 15) { ++ return expect; ++ } ++ ++ final int sectionOffset = this.chunkSectionIndexOffset; ++ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ); ++ int opacity = centerState.getOpacityIfCached(); ++ ++ final BlockState conditionallyOpaqueState; ++ if (opacity < 0) { ++ this.recalcCenterPos.set(worldX, worldY, worldZ); ++ opacity = Math.max(1, centerState.getLightBlock(lightAccess.getLevel(), 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 BlockState 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.set(offX, offY, offZ); ++ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms); ++ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms); ++ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) { ++ // not allowed to propagate ++ continue; ++ } ++ } ++ ++ final int calculated = neighbourLevel - opacity; ++ level = Math.max(calculated, level); ++ if (level > expect) { ++ return level; ++ } ++ } ++ ++ return level; ++ } ++ ++ @Override ++ protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions) { ++ this.rewriteNibbleCacheForSkylight(atChunk); ++ Arrays.fill(this.nullPropagationCheckCache, false); ++ ++ final BlockGetter world = lightAccess.getLevel(); ++ 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 BlockPos 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 BlockPos pos : positions) { ++ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); ++ } ++ ++ this.performLightDecrease(lightAccess); ++ } ++ ++ protected final int[] heightMapGen = new int[32 * 32]; ++ ++ @Override ++ protected void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) { ++ this.rewriteNibbleCacheForSkylight(chunk); ++ Arrays.fill(this.nullPropagationCheckCache, false); ++ ++ final BlockGetter world = lightAccess.getLevel(); ++ final ChunkPos chunkPos = chunk.getPos(); ++ final int chunkX = chunkPos.x; ++ final int chunkZ = chunkPos.z; ++ ++ final LevelChunkSection[] sections = chunk.getSections(); ++ ++ int highestNonEmptySection = this.maxSection; ++ while (highestNonEmptySection == (this.minSection - 1) || ++ sections[highestNonEmptySection - this.minSection] == null || sections[highestNonEmptySection - this.minSection].hasOnlyAir()) { ++ 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 BlockGetter world, final int worldX, int startY, final int worldZ, ++ final boolean extrudeInitialised, final boolean delayLightSet) { ++ final BlockPos.MutableBlockPos 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); ++ ++ BlockState above = this.getBlockState(worldX, startY + 1, worldZ); ++ ++ 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); ++ } ++ final BlockState current = this.getBlockState(worldX, startY, worldZ); ++ ++ final VoxelShape fromShape; ++ if (above.isConditionallyFullOpaque()) { ++ this.mutablePos2.set(worldX, startY + 1, worldZ); ++ fromShape = above.getFaceOcclusionShape(world, this.mutablePos2, AxisDirection.NEGATIVE_Y.nms); ++ if (Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { ++ // above wont let us propagate ++ break; ++ } ++ } else { ++ fromShape = Shapes.empty(); ++ } ++ ++ 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.set(worldX, startY, worldZ); ++ long flags = 0L; ++ if (current.isConditionallyFullOpaque()) { ++ final VoxelShape cullingFace = current.getFaceOcclusionShape(world, mutablePos, AxisDirection.POSITIVE_Y.nms); ++ ++ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { ++ // can't propagate here, we're done on this column. ++ break; ++ } ++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; ++ } ++ ++ final int opacity = current.getLightBlock(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/ca/spottedleaf/starlight/common/light/StarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightEngine.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ad1eeebe6de219143492b94da309cb54ae9e0a5b +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightEngine.java +@@ -0,0 +1,1572 @@ ++package ca.spottedleaf.starlight.common.light; ++ ++import ca.spottedleaf.starlight.common.util.CoordinateUtils; ++import ca.spottedleaf.starlight.common.util.IntegerUtil; ++import ca.spottedleaf.starlight.common.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.core.BlockPos; ++import net.minecraft.core.Direction; ++import net.minecraft.core.SectionPos; ++import net.minecraft.world.level.BlockGetter; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.LevelHeightAccessor; ++import net.minecraft.world.level.LightLayer; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.LevelChunkSection; ++import net.minecraft.world.level.chunk.LightChunkGetter; ++import net.minecraft.world.phys.shapes.Shapes; ++import net.minecraft.world.phys.shapes.VoxelShape; ++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 BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState(); ++ ++ 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 Direction 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 = Direction.fromDelta(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 LevelChunkSection[] 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. ++ // index = x + (z * 5) ++ protected final ChunkAccess[] chunkCache = new ChunkAccess[5 * 5]; ++ ++ // index = x + (z * 5) ++ protected final boolean[][] emptinessMapCache = new boolean[5 * 5][]; ++ ++ protected final BlockPos.MutableBlockPos mutablePos1 = new BlockPos.MutableBlockPos(); ++ protected final BlockPos.MutableBlockPos mutablePos2 = new BlockPos.MutableBlockPos(); ++ protected final BlockPos.MutableBlockPos mutablePos3 = new BlockPos.MutableBlockPos(); ++ ++ 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 final boolean isClientSide; ++ ++ protected final Level world; ++ protected final int minLightSection; ++ protected final int maxLightSection; ++ protected final int minSection; ++ protected final int maxSection; ++ ++ protected StarLightEngine(final boolean skylightPropagator, final Level world) { ++ this.skylightPropagator = skylightPropagator; ++ this.emittedLightMask = skylightPropagator ? 0 : 0xF; ++ this.isClientSide = world.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 LevelChunkSection[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 LightChunkGetter 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 ChunkAccess chunk = (ChunkAccess)chunkProvider.getChunkForLighting(cx, cz); ++ ++ 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 ChunkAccess 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 ChunkAccess chunk) { ++ this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = chunk; ++ } ++ ++ protected final LevelChunkSection 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 LevelChunkSection section) { ++ this.sectionCache[chunkX + 5*chunkZ + 5*5*chunkY + this.chunkSectionIndexOffset] = section; ++ } ++ ++ protected final void setBlocksForChunkInCache(final int chunkX, final int chunkZ, final LevelChunkSection[] 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)); ++ } ++ } ++ ++ 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 LightChunkGetter 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 ySections = (this.maxSection - this.minSection) + 1; ++ final int chunkY = ((index / (5*5)) % (ySections + 2 + 2)) - this.chunkOffsetY; ++ if ((nibble != null && nibble.updateVisible()) || this.notifyUpdateCache[index]) { ++ lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(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 BlockState getBlockState(final int worldX, final int worldY, final int worldZ) { ++ final LevelChunkSection section = this.sectionCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset]; ++ ++ if (section != null) { ++ return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.getBlockState(worldX & 15, worldY & 15, worldZ & 15); ++ } ++ ++ return AIR_BLOCK_STATE; ++ } ++ ++ protected final BlockState getBlockState(final int sectionIndex, final int localIndex) { ++ final LevelChunkSection section = this.sectionCache[sectionIndex]; ++ ++ if (section != null) { ++ return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.states.get(localIndex); ++ } ++ ++ return AIR_BLOCK_STATE; ++ } ++ ++ 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; ++ } ++ ++ public static SWMRNibbleArray[] getFilledEmptyLight(final LevelHeightAccessor 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 ChunkAccess chunk); ++ ++ protected abstract void setEmptinessMap(final ChunkAccess chunk, final boolean[] to); ++ ++ protected abstract SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk); ++ ++ protected abstract void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to); ++ ++ protected abstract boolean canUseChunk(final ChunkAccess chunk); ++ ++ public final void blocksChangedInChunk(final LightChunkGetter 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 ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); ++ if (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 LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions); ++ ++ protected abstract void checkBlock(final LightChunkGetter 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 LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, ++ final int expect); ++ ++ protected final int[] chunkCheckDelayedUpdatesCenter = new int[16 * 16]; ++ protected final int[] chunkCheckDelayedUpdatesNeighbour = new int[16 * 16]; ++ ++ protected void checkChunkEdge(final LightChunkGetter lightAccess, final ChunkAccess 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; ++ } ++ ++ 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) != currentLevel) { ++ this.chunkCheckDelayedUpdatesCenter[centerDelayedChecks++] = currentIndex; ++ } ++ ++ if (this.calculateLightValue(lightAccess, neighbourX, currY, neighbourZ, neighbourLevel) != 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 LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) { ++ final ChunkPos 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 LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) { ++ final ChunkPos 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 LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) { ++ final ChunkPos 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 ChunkAccess chunk) { ++ final LevelChunkSection[] sections = chunk.getSections(); ++ final Boolean[] ret = new Boolean[sections.length]; ++ ++ for (int i = 0; i < sections.length; ++i) { ++ if (sections[i] == null || sections[i].hasOnlyAir()) { ++ ret[i] = Boolean.TRUE; ++ } else { ++ ret[i] = Boolean.FALSE; ++ } ++ } ++ ++ return ret; ++ } ++ ++ public final void forceHandleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess 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 LightChunkGetter lightAccess, final int chunkX, final int chunkZ, ++ final Boolean[] emptinessChanges) { ++ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); ++ try { ++ final ChunkAccess 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(); ++ } ++ } ++ ++ protected abstract void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles); ++ ++ protected abstract void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ); ++ ++ // 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 final boolean[] handleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, ++ final Boolean[] emptinessChanges, final boolean unlit) { ++ final Level world = (Level)lightAccess.getLevel(); ++ 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) { ++ Boolean valueBoxed = emptinessChanges[sectionIndex]; ++ if (valueBoxed == null) { ++ if (!needsInit) { ++ continue; ++ } ++ final LevelChunkSection section = this.getChunkSection(chunkX, sectionIndex + this.minSection, chunkZ); ++ emptinessChanges[sectionIndex] = valueBoxed = section == null || section.hasOnlyAir() ? Boolean.TRUE : Boolean.FALSE; ++ } ++ 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.initNibble(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) { ++ // 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 LevelChunkSection section = this.getChunkSection(dx + dx2 + chunkX, y, dz + dz2 + chunkZ); ++ if (section != null && !section.hasOnlyAir()) { ++ 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 ++ this.setNibbleNull(dx + chunkX, sectionY, dz + chunkZ); ++ } else if (!allEmpty) { ++ // must init ++ final boolean extrude = (dx | dz) != 0 || !unlit; ++ this.initNibble(dx + chunkX, sectionY, dz + chunkZ, extrude, false); ++ } ++ } ++ } ++ } ++ ++ return ret; ++ } ++ ++ public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ) { ++ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false); ++ try { ++ final ChunkAccess 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 LightChunkGetter lightAccess, final int chunkX, final int chunkZ, final ShortCollection sections) { ++ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false); ++ try { ++ final ChunkAccess 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 LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks); ++ ++ public final void light(final LightChunkGetter lightAccess, final ChunkAccess 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); ++ this.setNibbles(chunk, nibbles); ++ this.updateVisible(lightAccess); ++ } finally { ++ this.destroyCaches(); ++ } ++ } ++ ++ public final void relightChunks(final LightChunkGetter 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 ChunkPos chunkPos : chunks) { ++ final int chunkX = chunkPos.x; ++ final int chunkZ = chunkPos.z; ++ final ChunkAccess chunk = (ChunkAccess)lightAccess.getChunkForLighting(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 ChunkAccess neighbour = (ChunkAccess)lightAccess.getChunkForLighting(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 ChunkAccess neighbour2 = (ChunkAccess)lightAccess.getChunkForLighting(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 ChunkPos(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.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, y, chunkX)); ++ } ++ ++ // now do callback ++ if (chunkLightCallback != null) { ++ chunkLightCallback.accept(chunkPos); ++ } ++ ++lightCalls; ++ } ++ ++ if (onComplete != null) { ++ onComplete.accept(lightCalls); ++ } ++ } ++ ++ // 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 3 bits: state flags ++ // state flags: ++ // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading light ++ // updates for block sources ++ protected static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 2; ++ // 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 LightChunkGetter lightAccess) { ++ final BlockGetter world = lightAccess.getLevel(); ++ 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; ++ } ++ } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) { ++ // these are used to restore block sources after a propagation decrease ++ this.setLightLevel(posX, posY, posZ, propagatedLightLevel); ++ } ++ ++ 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 BlockState 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.set(offX, offY, offZ); ++ long flags = 0; ++ if (blockState.isConditionallyFullOpaque()) { ++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); ++ ++ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { ++ continue; ++ } ++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; ++ } ++ ++ final int opacity = blockState.getLightBlock(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 BlockState fromBlock = this.getBlockState(posX, posY, posZ); ++ this.mutablePos2.set(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.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty(); ++ ++ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), 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 BlockState 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.set(offX, offY, offZ); ++ long flags = 0; ++ if (blockState.isConditionallyFullOpaque()) { ++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); ++ ++ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { ++ continue; ++ } ++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; ++ } ++ ++ final int opacity = blockState.getLightBlock(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 LightChunkGetter lightAccess) { ++ final BlockGetter world = lightAccess.getLevel(); ++ 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; ++ ++ 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 BlockState 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 = blockState.getLightEmission() & emittedMask; ++ if (emittedLight != 0) { ++ // re-propagate source ++ // note: do not set recheck level, or else the propagation will fail ++ 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_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL); ++ } ++ ++ currentNibble.set(localIndex, 0); ++ 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.set(offX, offY, offZ); ++ long flags = 0; ++ if (blockState.isConditionallyFullOpaque()) { ++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); ++ ++ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { ++ continue; ++ } ++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; ++ } ++ ++ final int opacity = blockState.getLightBlock(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 = blockState.getLightEmission() & emittedMask; ++ if (emittedLight != 0) { ++ // re-propagate source ++ // note: do not set recheck level, or else the propagation will fail ++ 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 | FLAG_WRITE_LEVEL); ++ } ++ ++ currentNibble.set(localIndex, 0); ++ 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 BlockState fromBlock = this.getBlockState(posX, posY, posZ); ++ this.mutablePos2.set(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.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty(); ++ ++ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), 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 BlockState 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 = blockState.getLightEmission() & emittedMask; ++ if (emittedLight != 0) { ++ // re-propagate source ++ // note: do not set recheck level, or else the propagation will fail ++ 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_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL); ++ } ++ ++ currentNibble.set(localIndex, 0); ++ 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.set(offX, offY, offZ); ++ long flags = 0; ++ if (blockState.isConditionallyFullOpaque()) { ++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); ++ ++ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { ++ continue; ++ } ++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; ++ } ++ ++ final int opacity = blockState.getLightBlock(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 = blockState.getLightEmission() & emittedMask; ++ if (emittedLight != 0) { ++ // re-propagate source ++ // note: do not set recheck level, or else the propagation will fail ++ 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 | FLAG_WRITE_LEVEL); ++ } ++ ++ currentNibble.set(localIndex, 0); ++ 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/ca/spottedleaf/starlight/common/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e0338db4d6fa359029ed5edeacc3646aa98701f5 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java +@@ -0,0 +1,674 @@ ++package ca.spottedleaf.starlight.common.light; ++ ++import ca.spottedleaf.starlight.common.util.CoordinateUtils; ++import ca.spottedleaf.starlight.common.util.WorldUtil; ++import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.shorts.ShortCollection; ++import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; ++import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.SectionPos; ++import net.minecraft.server.level.ServerChunkCache; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.chunk.DataLayer; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.LightChunkGetter; ++import net.minecraft.world.level.lighting.LayerLightEventListener; ++import net.minecraft.world.level.lighting.LevelLightEngine; ++import java.util.ArrayDeque; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Set; ++import java.util.concurrent.CompletableFuture; ++import java.util.function.Consumer; ++import java.util.function.IntConsumer; ++ ++public final class StarLightInterface { ++ ++ public static final TicketType CHUNK_WORK_TICKET = TicketType.create("starlight_chunk_work_ticket", (p1, p2) -> Long.compare(p1.toLong(), p2.toLong())); ++ ++ /** ++ * Can be {@code null}, indicating the light is all empty. ++ */ ++ protected final Level world; ++ protected final LightChunkGetter lightAccess; ++ ++ protected final ArrayDeque cachedSkyPropagators; ++ protected final ArrayDeque cachedBlockPropagators; ++ ++ protected final LightQueue lightQueue = new LightQueue(this); ++ ++ protected final LayerLightEventListener skyReader; ++ protected final LayerLightEventListener blockReader; ++ protected final boolean isClientSide; ++ ++ protected final int minSection; ++ protected final int maxSection; ++ protected final int minLightSection; ++ protected final int maxLightSection; ++ ++ public final LevelLightEngine lightEngine; ++ ++ private final boolean hasBlockLight; ++ private final boolean hasSkyLight; ++ ++ public StarLightInterface(final LightChunkGetter lightAccess, final boolean hasSkyLight, final boolean hasBlockLight, final LevelLightEngine lightEngine) { ++ this.lightAccess = lightAccess; ++ this.world = lightAccess == null ? null : (Level)lightAccess.getLevel(); ++ this.cachedSkyPropagators = hasSkyLight && lightAccess != null ? new ArrayDeque<>() : null; ++ this.cachedBlockPropagators = hasBlockLight && lightAccess != null ? new ArrayDeque<>() : null; ++ this.isClientSide = !(this.world instanceof ServerLevel); ++ if (this.world == null) { ++ this.minSection = -4; ++ this.maxSection = 19; ++ this.minLightSection = -5; ++ this.maxLightSection = 20; ++ } 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.lightEngine = lightEngine; ++ this.hasBlockLight = hasBlockLight; ++ this.hasSkyLight = hasSkyLight; ++ this.skyReader = !hasSkyLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() { ++ @Override ++ public void checkBlock(final BlockPos blockPos) { ++ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable()); ++ } ++ ++ @Override ++ public void propagateLightSources(final ChunkPos chunkPos) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean hasLightWork() { ++ // not really correct... ++ return StarLightInterface.this.hasUpdates(); ++ } ++ ++ @Override ++ public int runLightUpdates() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public DataLayer getDataLayerData(final SectionPos pos) { ++ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ()); ++ if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect()) || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { ++ return null; ++ } ++ ++ final int sectionY = pos.getY(); ++ ++ if (sectionY > StarLightInterface.this.maxLightSection || sectionY < StarLightInterface.this.minLightSection) { ++ return null; ++ } ++ ++ if (chunk.getSkyEmptinessMap() == null) { ++ return null; ++ } ++ ++ return chunk.getSkyNibbles()[sectionY - StarLightInterface.this.minLightSection].toVanillaNibble(); ++ } ++ ++ @Override ++ public int getLightValue(final BlockPos blockPos) { ++ return StarLightInterface.this.getSkyLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4)); ++ } ++ ++ @Override ++ public void updateSectionStatus(final SectionPos pos, final boolean notReady) { ++ StarLightInterface.this.sectionChange(pos, notReady); ++ } ++ }; ++ this.blockReader = !hasBlockLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() { ++ @Override ++ public void checkBlock(final BlockPos blockPos) { ++ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable()); ++ } ++ ++ @Override ++ public void propagateLightSources(final ChunkPos chunkPos) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean hasLightWork() { ++ // not really correct... ++ return StarLightInterface.this.hasUpdates(); ++ } ++ ++ @Override ++ public int runLightUpdates() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public DataLayer getDataLayerData(final SectionPos pos) { ++ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ()); ++ ++ if (chunk == null || pos.getY() < StarLightInterface.this.minLightSection || pos.getY() > StarLightInterface.this.maxLightSection) { ++ return null; ++ } ++ ++ return chunk.getBlockNibbles()[pos.getY() - StarLightInterface.this.minLightSection].toVanillaNibble(); ++ } ++ ++ @Override ++ public int getLightValue(final BlockPos blockPos) { ++ return StarLightInterface.this.getBlockLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4)); ++ } ++ ++ @Override ++ public void updateSectionStatus(final SectionPos pos, final boolean notReady) { ++ StarLightInterface.this.sectionChange(pos, notReady); ++ } ++ }; ++ } ++ ++ public boolean hasSkyLight() { ++ return this.hasSkyLight; ++ } ++ ++ public boolean hasBlockLight() { ++ return this.hasBlockLight; ++ } ++ ++ public int getSkyLightValue(final BlockPos blockPos, final ChunkAccess chunk) { ++ if (!this.hasSkyLight) { ++ return 0; ++ } ++ final int x = blockPos.getX(); ++ int y = blockPos.getY(); ++ final int z = blockPos.getZ(); ++ ++ final int minSection = this.minSection; ++ final int maxSection = this.maxSection; ++ final int minLightSection = this.minLightSection; ++ final int maxLightSection = this.maxLightSection; ++ ++ if (chunk == null || (!this.isClientSide && !chunk.isLightCorrect()) || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { ++ return 15; ++ } ++ ++ int sectionY = y >> 4; ++ ++ if (sectionY > maxLightSection) { ++ return 15; ++ } ++ ++ if (sectionY < minLightSection) { ++ sectionY = minLightSection; ++ y = sectionY << 4; ++ } ++ ++ final SWMRNibbleArray[] nibbles = chunk.getSkyNibbles(); ++ final SWMRNibbleArray immediate = nibbles[sectionY - minLightSection]; ++ ++ if (!immediate.isNullNibbleVisible()) { ++ return immediate.getVisible(x, y, z); ++ } ++ ++ final boolean[] emptinessMap = chunk.getSkyEmptinessMap(); ++ ++ if (emptinessMap == null) { ++ return 15; ++ } ++ ++ // are we above this chunk's lowest empty section? ++ int lowestY = minLightSection - 1; ++ for (int currY = maxSection; currY >= minSection; --currY) { ++ if (emptinessMap[currY - 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 <= maxLightSection; ++currY) { ++ final SWMRNibbleArray nibble = nibbles[currY - minLightSection]; ++ if (!nibble.isNullNibbleVisible()) { ++ return nibble.getVisible(x, 0, z); ++ } ++ } ++ ++ // should never reach here ++ return 15; ++ } ++ ++ public int getBlockLightValue(final BlockPos blockPos, final ChunkAccess chunk) { ++ if (!this.hasBlockLight) { ++ return 0; ++ } ++ final int y = blockPos.getY(); ++ final int cy = y >> 4; ++ ++ final int minLightSection = this.minLightSection; ++ final int maxLightSection = this.maxLightSection; ++ ++ if (cy < minLightSection || cy > maxLightSection) { ++ return 0; ++ } ++ ++ if (chunk == null) { ++ return 0; ++ } ++ ++ final SWMRNibbleArray nibble = chunk.getBlockNibbles()[cy - minLightSection]; ++ return nibble.getVisible(blockPos.getX(), y, blockPos.getZ()); ++ } ++ ++ public int getRawBrightness(final BlockPos pos, final int ambientDarkness) { ++ final ChunkAccess chunk = this.getAnyChunkNow(pos.getX() >> 4, pos.getZ() >> 4); ++ ++ final int sky = this.getSkyLightValue(pos, chunk) - ambientDarkness; ++ // Don't fetch the block light level if the skylight level is 15, since the value will never be higher. ++ if (sky == 15) return 15; ++ final int block = this.getBlockLightValue(pos, chunk); ++ return Math.max(sky, block); ++ } ++ ++ public LayerLightEventListener getSkyReader() { ++ return this.skyReader; ++ } ++ ++ public LayerLightEventListener getBlockReader() { ++ return this.blockReader; ++ } ++ ++ public boolean isClientSide() { ++ return this.isClientSide; ++ } ++ ++ public ChunkAccess getAnyChunkNow(final int chunkX, final int chunkZ) { ++ if (this.world == null) { ++ // empty world ++ return null; ++ } ++ ++ final ServerChunkCache chunkProvider = ((ServerLevel)this.world).getChunkSource(); ++ final LevelChunk fullLoaded = chunkProvider.getChunkAtIfLoadedImmediately(chunkX, chunkZ); ++ if (fullLoaded != null) { ++ return fullLoaded; ++ } ++ ++ return chunkProvider.getChunkAtImmediately(chunkX, chunkZ); ++ } ++ ++ public boolean hasUpdates() { ++ return !this.lightQueue.isEmpty(); ++ } ++ ++ public Level getWorld() { ++ return this.world; ++ } ++ ++ public LightChunkGetter 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 LightQueue.ChunkTasks blockChange(final BlockPos pos) { ++ if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world ++ return null; ++ } ++ ++ return this.lightQueue.queueBlockChange(pos); ++ } ++ ++ public LightQueue.ChunkTasks sectionChange(final SectionPos pos, final boolean newEmptyValue) { ++ if (this.world == null) { // empty world ++ return null; ++ } ++ ++ return this.lightQueue.queueSectionChange(pos, newEmptyValue); ++ } ++ ++ public void forceLoadInChunk(final ChunkAccess 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 ChunkAccess 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 scheduleChunkLight(final ChunkPos pos, final Runnable run) { ++ this.lightQueue.queueChunkLighting(pos, run); ++ } ++ ++ public void removeChunkTasks(final ChunkPos pos) { ++ this.lightQueue.removeChunk(pos); ++ } ++ ++ public void propagateChanges() { ++ if (this.lightQueue.isEmpty()) { ++ return; ++ } ++ ++ final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); ++ final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); ++ ++ try { ++ LightQueue.ChunkTasks task; ++ while ((task = this.lightQueue.removeFirstTask()) != null) { ++ if (task.lightTasks != null) { ++ for (final Runnable run : task.lightTasks) { ++ run.run(); ++ } ++ } ++ ++ final long coordinate = task.chunkCoordinate; ++ final int chunkX = CoordinateUtils.getChunkX(coordinate); ++ final int chunkZ = CoordinateUtils.getChunkZ(coordinate); ++ ++ final Set positions = task.changedPositions; ++ final Boolean[] sectionChanges = task.changedSectionSet; ++ ++ if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) { ++ skyEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); ++ } ++ if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) { ++ blockEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); ++ } ++ ++ if (skyEngine != null && task.queuedEdgeChecksSky != null) { ++ skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksSky); ++ } ++ if (blockEngine != null && task.queuedEdgeChecksBlock != null) { ++ blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksBlock); ++ } ++ ++ task.onComplete.complete(null); ++ } ++ } finally { ++ this.releaseSkyLightEngine(skyEngine); ++ this.releaseBlockLightEngine(blockEngine); ++ } ++ } ++ ++ public static final class LightQueue { ++ ++ protected final Long2ObjectLinkedOpenHashMap chunkTasks = new Long2ObjectLinkedOpenHashMap<>(); ++ protected final StarLightInterface manager; ++ ++ public LightQueue(final StarLightInterface manager) { ++ this.manager = manager; ++ } ++ ++ public synchronized boolean isEmpty() { ++ return this.chunkTasks.isEmpty(); ++ } ++ ++ public synchronized LightQueue.ChunkTasks queueBlockChange(final BlockPos pos) { ++ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); ++ tasks.changedPositions.add(pos.immutable()); ++ return tasks; ++ } ++ ++ public synchronized LightQueue.ChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) { ++ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); ++ ++ if (tasks.changedSectionSet == null) { ++ tasks.changedSectionSet = new Boolean[this.manager.maxSection - this.manager.minSection + 1]; ++ } ++ tasks.changedSectionSet[pos.getY() - this.manager.minSection] = Boolean.valueOf(newEmptyValue); ++ ++ return tasks; ++ } ++ ++ public synchronized LightQueue.ChunkTasks queueChunkLighting(final ChunkPos pos, final Runnable lightTask) { ++ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); ++ if (tasks.lightTasks == null) { ++ tasks.lightTasks = new ArrayList<>(); ++ } ++ tasks.lightTasks.add(lightTask); ++ ++ return tasks; ++ } ++ ++ public synchronized LightQueue.ChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) { ++ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); ++ ++ ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksSky; ++ if (queuedEdges == null) { ++ queuedEdges = tasks.queuedEdgeChecksSky = new ShortOpenHashSet(); ++ } ++ queuedEdges.addAll(sections); ++ ++ return tasks; ++ } ++ ++ public synchronized LightQueue.ChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) { ++ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); ++ ++ ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksBlock; ++ if (queuedEdges == null) { ++ queuedEdges = tasks.queuedEdgeChecksBlock = new ShortOpenHashSet(); ++ } ++ queuedEdges.addAll(sections); ++ ++ return tasks; ++ } ++ ++ public void removeChunk(final ChunkPos pos) { ++ final ChunkTasks tasks; ++ synchronized (this) { ++ tasks = this.chunkTasks.remove(CoordinateUtils.getChunkKey(pos)); ++ } ++ if (tasks != null) { ++ tasks.onComplete.complete(null); ++ } ++ } ++ ++ public synchronized ChunkTasks removeFirstTask() { ++ if (this.chunkTasks.isEmpty()) { ++ return null; ++ } ++ return this.chunkTasks.removeFirst(); ++ } ++ ++ public static final class ChunkTasks { ++ ++ public final Set changedPositions = new ObjectOpenHashSet<>(); ++ public Boolean[] changedSectionSet; ++ public ShortOpenHashSet queuedEdgeChecksSky; ++ public ShortOpenHashSet queuedEdgeChecksBlock; ++ public List lightTasks; ++ ++ public boolean isTicketAdded = false; ++ public final CompletableFuture onComplete = new CompletableFuture<>(); ++ ++ public final long chunkCoordinate; ++ ++ public ChunkTasks(final long chunkCoordinate) { ++ this.chunkCoordinate = chunkCoordinate; ++ } ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/starlight/common/util/CoordinateUtils.java b/src/main/java/ca/spottedleaf/starlight/common/util/CoordinateUtils.java +new file mode 100644 +index 0000000000000000000000000000000000000000..16a4a14e7ccf9e4d7fdf1166674fe8f529c06d39 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/starlight/common/util/CoordinateUtils.java +@@ -0,0 +1,128 @@ ++package ca.spottedleaf.starlight.common.util; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.SectionPos; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.ChunkPos; ++ ++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 BlockPos pos) { ++ return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL); ++ } ++ ++ public static long getChunkKey(final Entity entity) { ++ return ((long)(Mth.floor(entity.getZ()) >> 4) << 32) | ((Mth.floor(entity.getX()) >> 4) & 0xFFFFFFFFL); ++ } ++ ++ public static long getChunkKey(final ChunkPos pos) { ++ return ((long)pos.z << 32) | (pos.x & 0xFFFFFFFFL); ++ } ++ ++ public static long getChunkKey(final SectionPos 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 Mth.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 SectionPos 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 ChunkPos 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 BlockPos 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 ((Mth.lfloor(entity.getX()) << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) | ++ ((Mth.lfloor(entity.getY()) >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) | ++ ((Mth.lfloor(entity.getZ()) << (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 Mth.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 BlockPos 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.getX() & 0x7FFFFFF) | (((long)entity.getZ() & 0x7FFFFFF) << 27) | ((long)entity.getY() << 54); ++ } ++ ++ private CoordinateUtils() { ++ throw new RuntimeException(); ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/starlight/common/util/IntegerUtil.java b/src/main/java/ca/spottedleaf/starlight/common/util/IntegerUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fabf1e97c019c7365212f40018dcd08d3b828113 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/starlight/common/util/IntegerUtil.java +@@ -0,0 +1,242 @@ ++package ca.spottedleaf.starlight.common.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 = 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 ++ ++ /* ++ Signed usage: ++ int number; ++ long magic = getDivisorNumbers(div); ++ long mul = magic >>> 32; ++ int sign = number >> 31; ++ int result = (int)(((long)number * mul) >>> magic) - sign; ++ */ ++ /* ++ Unsigned usage: ++ int number; ++ long magic = getDivisorNumbers(div); ++ long mul = magic >>> 32; ++ int result = (int)(((long)number * mul) >>> magic); ++ */ ++ ++ int p = 31; ++ ++ // all these variables are UNSIGNED! ++ int t = two31 + (d >>> 31); ++ int anc = t - 1 - (int)((t & mask)%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; ++ 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/ca/spottedleaf/starlight/common/util/SaveUtil.java b/src/main/java/ca/spottedleaf/starlight/common/util/SaveUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c2903150c8fc6955f4f4f71acc932b6c2ac83484 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/starlight/common/util/SaveUtil.java +@@ -0,0 +1,192 @@ ++package ca.spottedleaf.starlight.common.util; ++ ++import ca.spottedleaf.starlight.common.light.SWMRNibbleArray; ++import ca.spottedleaf.starlight.common.light.StarLightEngine; ++import com.mojang.logging.LogUtils; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.ListTag; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import org.slf4j.Logger; ++ ++public final class SaveUtil { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ private static final int STARLIGHT_LIGHT_VERSION = 9; ++ ++ public static int getLightVersion() { ++ return STARLIGHT_LIGHT_VERSION; ++ } ++ ++ private static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state"; ++ private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state"; ++ private static final String STARLIGHT_VERSION_TAG = "starlight.light_version"; ++ ++ public static void saveLightHook(final Level world, final ChunkAccess chunk, final CompoundTag nbt) { ++ try { ++ saveLightHookReal(world, chunk, nbt); ++ } catch (final Throwable ex) { ++ // failing to inject is not fatal so we catch anything here. if it fails, it will have correctly set lit to false ++ // for Vanilla to relight on load and it will not set our lit tag so we will relight on load ++ if (ex instanceof ThreadDeath) { ++ throw (ThreadDeath)ex; ++ } ++ LOGGER.warn("Failed to inject light data into save data for chunk " + chunk.getPos() + ", chunk light will be recalculated on its next load", ex); ++ } ++ } ++ ++ private static void saveLightHookReal(final Level world, final ChunkAccess chunk, final CompoundTag tag) { ++ if (tag == null) { ++ return; ++ } ++ ++ final int minSection = WorldUtil.getMinLightSection(world); ++ final int maxSection = WorldUtil.getMaxLightSection(world); ++ ++ SWMRNibbleArray[] blockNibbles = chunk.getBlockNibbles(); ++ SWMRNibbleArray[] skyNibbles = chunk.getSkyNibbles(); ++ ++ boolean lit = chunk.isLightCorrect() || !(world instanceof ServerLevel); ++ // diff start - store our tag for whether light data is init'd ++ if (lit) { ++ tag.putBoolean("isLightOn", false); ++ } ++ // diff end - store our tag for whether light data is init'd ++ ChunkStatus status = ChunkStatus.byName(tag.getString("Status")); ++ ++ CompoundTag[] sections = new CompoundTag[maxSection - minSection + 1]; ++ ++ ListTag sectionsStored = tag.getList("sections", 10); ++ ++ for (int i = 0; i < sectionsStored.size(); ++i) { ++ CompoundTag sectionStored = sectionsStored.getCompound(i); ++ int k = sectionStored.getByte("Y"); ++ ++ // strip light data ++ sectionStored.remove("BlockLight"); ++ sectionStored.remove("SkyLight"); ++ ++ if (!sectionStored.isEmpty()) { ++ sections[k - minSection] = sectionStored; ++ } ++ } ++ ++ if (lit && status.isOrAfter(ChunkStatus.LIGHT)) { ++ for (int i = minSection; i <= maxSection; ++i) { ++ SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection].getSaveState(); ++ SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection].getSaveState(); ++ if (blockNibble != null || skyNibble != null) { ++ CompoundTag section = sections[i - minSection]; ++ if (section == null) { ++ section = new CompoundTag(); ++ section.putByte("Y", (byte)i); ++ sections[i - minSection] = section; ++ } ++ ++ // we store under the same key so mod programs editing nbt ++ // can still read the data, hopefully. ++ // however, for compatibility we store chunks as unlit so vanilla ++ // is forced to re-light them if it encounters our data. It's too much of a burden ++ // to try and maintain compatibility with a broken and inferior skylight management system. ++ ++ if (blockNibble != null) { ++ if (blockNibble.data != null) { ++ section.putByteArray("BlockLight", blockNibble.data); ++ } ++ section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state); ++ } ++ ++ if (skyNibble != null) { ++ if (skyNibble.data != null) { ++ section.putByteArray("SkyLight", skyNibble.data); ++ } ++ section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state); ++ } ++ } ++ } ++ } ++ ++ // rewrite section list ++ sectionsStored.clear(); ++ for (CompoundTag section : sections) { ++ if (section != null) { ++ sectionsStored.add(section); ++ } ++ } ++ tag.put("sections", sectionsStored); ++ if (lit) { ++ tag.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // only mark as fully lit after we have successfully injected our data ++ } ++ } ++ ++ public static void loadLightHook(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) { ++ try { ++ loadLightHookReal(world, pos, tag, into); ++ } catch (final Throwable ex) { ++ // failing to inject is not fatal so we catch anything here. if it fails, then we simply relight. Not a problem, we get correct ++ // lighting in both cases. ++ if (ex instanceof ThreadDeath) { ++ throw (ThreadDeath)ex; ++ } ++ LOGGER.warn("Failed to load light for chunk " + pos + ", light will be recalculated", ex); ++ } ++ } ++ ++ private static void loadLightHookReal(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) { ++ if (into == null) { ++ return; ++ } ++ final int minSection = WorldUtil.getMinLightSection(world); ++ final int maxSection = WorldUtil.getMaxLightSection(world); ++ ++ into.setLightCorrect(false); // mark as unlit in case we fail parsing ++ ++ SWMRNibbleArray[] blockNibbles = StarLightEngine.getFilledEmptyLight(world); ++ SWMRNibbleArray[] skyNibbles = StarLightEngine.getFilledEmptyLight(world); ++ ++ ++ // start copy from the original method ++ boolean lit = tag.get("isLightOn") != null && tag.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION; ++ boolean canReadSky = world.dimensionType().hasSkyLight(); ++ ChunkStatus status = ChunkStatus.byName(tag.getString("Status")); ++ if (lit && status.isOrAfter(ChunkStatus.LIGHT)) { // diff - we add the status check here ++ ListTag sections = tag.getList("sections", 10); ++ ++ for (int i = 0; i < sections.size(); ++i) { ++ CompoundTag sectionData = sections.getCompound(i); ++ int y = sectionData.getByte("Y"); ++ ++ if (sectionData.contains("BlockLight", 7)) { ++ // this is where our diff is ++ blockNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety ++ } else { ++ blockNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG)); ++ } ++ ++ if (canReadSky) { ++ if (sectionData.contains("SkyLight", 7)) { ++ // we store under the same key so mod programs editing nbt ++ // can still read the data, hopefully. ++ // however, for compatibility we store chunks as unlit so vanilla ++ // is forced to re-light them if it encounters our data. It's too much of a burden ++ // to try and maintain compatibility with a broken and inferior skylight management system. ++ skyNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety ++ } else { ++ skyNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG)); ++ } ++ } ++ } ++ } ++ // end copy from vanilla ++ ++ into.setBlockNibbles(blockNibbles); ++ into.setSkyNibbles(skyNibbles); ++ into.setLightCorrect(lit); // now we set lit here, only after we've correctly parsed data ++ } ++ ++ private SaveUtil() {} ++} +diff --git a/src/main/java/ca/spottedleaf/starlight/common/util/WorldUtil.java b/src/main/java/ca/spottedleaf/starlight/common/util/WorldUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..dd995e25ae620ae36cd5eecb2fe10ad034ba50d2 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/starlight/common/util/WorldUtil.java +@@ -0,0 +1,47 @@ ++package ca.spottedleaf.starlight.common.util; ++ ++import net.minecraft.world.level.LevelHeightAccessor; ++ ++public final class WorldUtil { ++ ++ // min, max are inclusive ++ ++ public static int getMaxSection(final LevelHeightAccessor world) { ++ return world.getMaxSection() - 1; // getMaxSection() is exclusive ++ } ++ ++ public static int getMinSection(final LevelHeightAccessor world) { ++ return world.getMinSection(); ++ } ++ ++ public static int getMaxLightSection(final LevelHeightAccessor world) { ++ return getMaxSection(world) + 1; ++ } ++ ++ public static int getMinLightSection(final LevelHeightAccessor world) { ++ return getMinSection(world) - 1; ++ } ++ ++ ++ ++ public static int getTotalSections(final LevelHeightAccessor world) { ++ return getMaxSection(world) - getMinSection(world) + 1; ++ } ++ ++ public static int getTotalLightSections(final LevelHeightAccessor world) { ++ return getMaxLightSection(world) - getMinLightSection(world) + 1; ++ } ++ ++ public static int getMinBlockY(final LevelHeightAccessor world) { ++ return getMinSection(world) << 4; ++ } ++ ++ public static int getMaxBlockY(final LevelHeightAccessor world) { ++ return (getMaxSection(world) << 4) | 15; ++ } ++ ++ private WorldUtil() { ++ throw new RuntimeException(); ++ } ++ ++} +diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java +index 534d9c380f26d6cce3c99fa88ad2e15410535094..e47fb2aa5e885162cae5cbfc9f33ff7864bf538e 100644 +--- a/src/main/java/io/papermc/paper/command/PaperCommand.java ++++ b/src/main/java/io/papermc/paper/command/PaperCommand.java +@@ -42,6 +42,7 @@ public final class PaperCommand extends Command { + commands.put(Set.of("dumpitem"), new DumpItemCommand()); + commands.put(Set.of("mobcaps", "playermobcaps"), new MobcapsCommand()); + commands.put(Set.of("dumplisteners"), new DumpListenersCommand()); ++ commands.put(Set.of("fixlight"), new FixLightCommand()); + + return commands.entrySet().stream() + .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue()))) +diff --git a/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..56524cbe4303901007e1e7fb3703a19efbf79ae7 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java +@@ -0,0 +1,109 @@ ++package io.papermc.paper.command.subcommands; ++ ++import io.papermc.paper.command.PaperSubcommand; ++import io.papermc.paper.util.MCUtil; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.server.level.ThreadedLevelLightEngine; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import org.bukkit.command.CommandSender; ++import org.bukkit.craftbukkit.entity.CraftPlayer; ++import org.bukkit.entity.Player; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++import static net.kyori.adventure.text.Component.text; ++import static net.kyori.adventure.text.format.NamedTextColor.BLUE; ++import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA; ++import static net.kyori.adventure.text.format.NamedTextColor.RED; ++ ++@DefaultQualifier(NonNull.class) ++public final class FixLightCommand implements PaperSubcommand { ++ @Override ++ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { ++ this.doFixLight(sender, args); ++ return true; ++ } ++ ++ private void doFixLight(final CommandSender sender, final String[] args) { ++ if (!(sender instanceof Player)) { ++ sender.sendMessage(text("Only players can use this command", RED)); ++ return; ++ } ++ @Nullable Runnable post = null; ++ int radius = 2; ++ if (args.length > 0) { ++ try { ++ final int parsed = Integer.parseInt(args[0]); ++ if (parsed < 0) { ++ sender.sendMessage(text("Radius cannot be negative!", RED)); ++ return; ++ } ++ final int maxRadius = 32; ++ radius = Math.min(maxRadius, parsed); ++ if (radius != parsed) { ++ post = () -> sender.sendMessage(text("Radius '" + parsed + "' was not in the required range [0, " + maxRadius + "], it was lowered to the maximum (" + maxRadius + " chunks).", RED)); ++ } ++ } catch (final Exception e) { ++ sender.sendMessage(text("'" + args[0] + "' is not a valid number.", RED)); ++ return; ++ } ++ } ++ ++ CraftPlayer player = (CraftPlayer) sender; ++ ServerPlayer handle = player.getHandle(); ++ ServerLevel world = (ServerLevel) handle.level(); ++ ThreadedLevelLightEngine lightengine = world.getChunkSource().getLightEngine(); ++ this.starlightFixLight(handle, world, lightengine, radius, post); ++ } ++ ++ private void starlightFixLight( ++ final ServerPlayer sender, ++ final ServerLevel world, ++ final ThreadedLevelLightEngine lightengine, ++ final int radius, ++ final @Nullable Runnable done ++ ) { ++ final long start = System.nanoTime(); ++ final java.util.LinkedHashSet chunks = new java.util.LinkedHashSet<>(MCUtil.getSpiralOutChunks(sender.blockPosition(), radius)); // getChunkCoordinates is actually just bad mappings, this function rets position as blockpos ++ ++ final int[] pending = new int[1]; ++ for (java.util.Iterator iterator = chunks.iterator(); iterator.hasNext(); ) { ++ final ChunkPos chunkPos = iterator.next(); ++ ++ final @Nullable ChunkAccess chunk = (ChunkAccess) world.getChunkSource().getChunkForLighting(chunkPos.x, chunkPos.z); ++ if (chunk == null || !chunk.isLightCorrect() || !chunk.getStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) { ++ // cannot relight this chunk ++ iterator.remove(); ++ continue; ++ } ++ ++ ++pending[0]; ++ } ++ ++ final int[] relitChunks = new int[1]; ++ lightengine.relight(chunks, ++ (final ChunkPos chunkPos) -> { ++ ++relitChunks[0]; ++ sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append( ++ text("Relit chunk ", BLUE), text(chunkPos.toString()), ++ text(", progress: ", BLUE), text((int) (Math.round(100.0 * (double) (relitChunks[0]) / (double) pending[0])) + "%") ++ )); ++ }, ++ (final int totalRelit) -> { ++ final long end = System.nanoTime(); ++ final long diff = Math.round(1.0e-6 * (end - start)); ++ sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append( ++ text("Relit ", BLUE), text(totalRelit), ++ text(" chunks. Took ", BLUE), text(diff + "ms") ++ )); ++ if (done != null) { ++ done.run(); ++ } ++ } ++ ); ++ sender.getBukkitEntity().sendMessage(text().color(BLUE).append(text("Relighting "), text(pending[0], DARK_AQUA), text(" chunks"))); ++ } ++} +diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java +index b12921579cb9ab3cbf5607841cc84f2f843624ea..88729d92878f98729eb5669cce5ae5b1418865a1 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java ++++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java +@@ -51,7 +51,7 @@ public class ChunkHolder { + private volatile CompletableFuture> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage + private volatile CompletableFuture> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage + private volatile CompletableFuture> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage +- private CompletableFuture chunkToSave; ++ public CompletableFuture chunkToSave; // Paper - public + @Nullable + private final DebugBuffer chunkToSaveHistory; + public int oldTicketLevel; +@@ -261,6 +261,12 @@ public class ChunkHolder { + } + } + ++ // Paper start - starlight ++ public void broadcast(Packet packet, boolean onChunkViewEdge) { ++ this.broadcast(this.playerProvider.getPlayers(this.pos, onChunkViewEdge), packet); ++ } ++ // Paper end - starlight ++ + public void broadcastChanges(LevelChunk chunk) { + if (this.hasChangedSections || !this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty()) { + Level world = chunk.getLevel(); +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index 35f627c58e93c03ee58b44877398432bba57dc2d..d3f63185edd1db9fab3887ea3f08982435b3a23c 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -128,7 +128,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + private final LongSet entitiesInLevel; + public final ServerLevel level; + private final ThreadedLevelLightEngine lightEngine; +- private final BlockableEventLoop mainThreadExecutor; ++ public final BlockableEventLoop mainThreadExecutor; // Paper - public + public ChunkGenerator generator; + private final RandomState randomState; + private final ChunkGeneratorStructureState chunkGeneratorState; +diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java +index c473cb1888e9ab0e91ba44f1439b81742758304e..7a48ae2ba962ff56d0abff581b51f28b48bd9aae 100644 +--- a/src/main/java/net/minecraft/server/level/DistanceManager.java ++++ b/src/main/java/net/minecraft/server/level/DistanceManager.java +@@ -379,7 +379,7 @@ public abstract class DistanceManager { + } + + public void removeTicketsOnClosing() { +- ImmutableSet> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.LIGHT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve ++ ImmutableSet> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.LIGHT, TicketType.FUTURE_AWAIT, TicketType.CHUNK_RELIGHT, ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET); // Paper - add additional tickets to preserve + ObjectIterator>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); + + while (objectiterator.hasNext()) { +diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java +index 1dfae40ec19c4df0a97359941cf2c948cd1c9cb2..f206df06a7d8895175db31d4a840d7467ffe826f 100644 +--- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java ++++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java +@@ -23,6 +23,17 @@ import net.minecraft.world.level.chunk.LightChunkGetter; + import net.minecraft.world.level.lighting.LevelLightEngine; + import org.slf4j.Logger; + ++// Paper start ++import ca.spottedleaf.starlight.common.light.StarLightEngine; ++import io.papermc.paper.util.CoordinateUtils; ++import java.util.function.Supplier; ++import net.minecraft.world.level.lighting.LayerLightEventListener; ++import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; ++import it.unimi.dsi.fastutil.longs.LongArrayList; ++import it.unimi.dsi.fastutil.longs.LongIterator; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++// Paper end ++ + public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable { + public static final int DEFAULT_BATCH_SIZE = 1000; + private static final Logger LOGGER = LogUtils.getLogger(); +@@ -33,6 +44,12 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + private final int taskPerBatch = 1000; + private final AtomicBoolean scheduled = new AtomicBoolean(); + ++ // Paper start - replace light engine impl ++ protected final ca.spottedleaf.starlight.common.light.StarLightInterface theLightEngine; ++ public final boolean hasBlockLight; ++ public final boolean hasSkyLight; ++ // Paper end - replace light engine impl ++ + public ThreadedLevelLightEngine( + LightChunkGetter chunkProvider, + ChunkMap chunkStorage, +@@ -40,11 +57,153 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + ProcessorMailbox processor, + ProcessorHandle> executor + ) { +- super(chunkProvider, true, hasBlockLight); ++ super(chunkProvider, false, false); // Paper - destroy vanilla light engine state + this.chunkMap = chunkStorage; + this.sorterMailbox = executor; + this.taskMailbox = processor; ++ // Paper start - replace light engine impl ++ this.hasBlockLight = true; ++ this.hasSkyLight = hasBlockLight; // Nice variable name. ++ this.theLightEngine = new ca.spottedleaf.starlight.common.light.StarLightInterface(chunkProvider, this.hasSkyLight, this.hasBlockLight, this); ++ // Paper end - replace light engine impl ++ } ++ ++ // Paper start - replace light engine impl ++ protected final ChunkAccess getChunk(final int chunkX, final int chunkZ) { ++ return ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().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 ChunkPos chunkPos = iterator.next(); ++ ++ final ChunkAccess chunk = (ChunkAccess)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().getChunkForLighting(chunkPos.x, chunkPos.z); ++ if (chunk == null || !chunk.isLightCorrect() || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { ++ // cannot relight this chunk ++ iterator.remove(); ++ continue; ++ } ++ ++ final Long id = Long.valueOf(this.relightCounter++); ++ ++ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().addTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, io.papermc.paper.util.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), id); ++ ticketIds.put(chunkPos, id); ++ ++ ++totalChunks; ++ } ++ ++ this.taskMailbox.tell(() -> { ++ this.theLightEngine.relightChunks(chunks, (ChunkPos chunkPos) -> { ++ chunkLightCallback.accept(chunkPos); ++ ((java.util.concurrent.Executor)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().mainThreadProcessor).execute(() -> { ++ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().chunkMap.getUpdatingChunkIfPresent(chunkPos.toLong()).broadcast(new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket(chunkPos, ThreadedLevelLightEngine.this, null, null), false); ++ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().removeTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, io.papermc.paper.util.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), ticketIds.get(chunkPos)); ++ }); ++ }, onComplete); ++ }); ++ this.tryScheduleUpdate(); ++ ++ return totalChunks; ++ } ++ ++ private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap(); ++ ++ private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ, ++ final Supplier runnable) { ++ final ServerLevel world = (ServerLevel)this.theLightEngine.getWorld(); ++ ++ final ChunkAccess center = this.theLightEngine.getAnyChunkNow(chunkX, chunkZ); ++ if (center == null || !center.getStatus().isOrAfter(ChunkStatus.LIGHT)) { ++ // do not accept updates in unlit chunks, unless we might be generating a chunk. thanks to the amazing ++ // chunk scheduling, we could be lighting and generating a chunk at the same time ++ return; ++ } ++ ++ if (center.getStatus() != ChunkStatus.FULL) { ++ // do not keep chunk loaded, we are probably in a gen thread ++ // if we proceed to add a ticket the chunk will be loaded, which is not what we want (avoid cascading gen) ++ runnable.get(); ++ return; ++ } ++ ++ if (!world.getChunkSource().chunkMap.mainThreadExecutor.isSameThread()) { ++ // ticket logic is not safe to run off-main, re-schedule ++ world.getChunkSource().chunkMap.mainThreadExecutor.execute(() -> { ++ this.queueTaskForSection(chunkX, chunkY, chunkZ, runnable); ++ }); ++ return; ++ } ++ ++ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ final ca.spottedleaf.starlight.common.light.StarLightInterface.LightQueue.ChunkTasks updateFuture = runnable.get(); ++ ++ if (updateFuture == null) { ++ // not scheduled ++ return; ++ } ++ ++ if (updateFuture.isTicketAdded) { ++ // ticket already added ++ return; ++ } ++ updateFuture.isTicketAdded = true; ++ ++ final int references = this.chunksBeingWorkedOn.addTo(key, 1); ++ if (references == 0) { ++ final ChunkPos pos = new ChunkPos(chunkX, chunkZ); ++ world.getChunkSource().addRegionTicket(ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); ++ } ++ ++ updateFuture.onComplete.thenAcceptAsync((final Void ignore) -> { ++ final int newReferences = this.chunksBeingWorkedOn.get(key); ++ if (newReferences == 1) { ++ this.chunksBeingWorkedOn.remove(key); ++ final ChunkPos pos = new ChunkPos(chunkX, chunkZ); ++ world.getChunkSource().removeRegionTicket(ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); ++ } else { ++ this.chunksBeingWorkedOn.put(key, newReferences - 1); ++ } ++ }, world.getChunkSource().chunkMap.mainThreadExecutor).whenComplete((final Void ignore, final Throwable thr) -> { ++ if (thr != null) { ++ LOGGER.error("Failed to remove ticket level for post chunk task " + new ChunkPos(chunkX, chunkZ), thr); ++ } ++ }); ++ } ++ ++ @Override ++ public boolean hasLightWork() { ++ // route to new light engine ++ return this.theLightEngine.hasUpdates(); ++ } ++ ++ @Override ++ public LayerLightEventListener getLayerListener(final LightLayer lightType) { ++ return lightType == LightLayer.BLOCK ? this.theLightEngine.getBlockReader() : this.theLightEngine.getSkyReader(); ++ } ++ ++ @Override ++ public int getRawBrightness(final BlockPos pos, final int ambientDarkness) { ++ // need to use new light hooks for this ++ final int sky = this.theLightEngine.getSkyReader().getLightValue(pos) - ambientDarkness; ++ // Don't fetch the block light level if the skylight level is 15, since the value will never be higher. ++ if (sky == 15) return 15; ++ final int block = this.theLightEngine.getBlockReader().getLightValue(pos); ++ return Math.max(sky, block); + } ++ // Paper end - replace light engine imp + + @Override + public void close() { +@@ -57,16 +216,16 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + + @Override + public void checkBlock(BlockPos pos) { +- BlockPos blockPos = pos.immutable(); +- this.addTask( +- SectionPos.blockToSectionCoord(pos.getX()), +- SectionPos.blockToSectionCoord(pos.getZ()), +- ThreadedLevelLightEngine.TaskType.PRE_UPDATE, +- Util.name(() -> super.checkBlock(blockPos), () -> "checkBlock " + blockPos) +- ); ++ // Paper start - replace light engine impl ++ final BlockPos posCopy = pos.immutable(); ++ this.queueTaskForSection(posCopy.getX() >> 4, posCopy.getY() >> 4, posCopy.getZ() >> 4, () -> { ++ return this.theLightEngine.blockChange(posCopy); ++ }); ++ // Paper end - replace light engine impl + } + + protected void updateChunkStatus(ChunkPos pos) { ++ if (true) return; // Paper - replace light engine impl + this.addTask(pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { + super.retainData(pos, false); + super.setLightEnabled(pos, false); +@@ -84,17 +243,16 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + + @Override + public void updateSectionStatus(SectionPos pos, boolean notReady) { +- this.addTask( +- pos.x(), +- pos.z(), +- () -> 0, +- ThreadedLevelLightEngine.TaskType.PRE_UPDATE, +- Util.name(() -> super.updateSectionStatus(pos, notReady), () -> "updateSectionStatus " + pos + " " + notReady) +- ); ++ // Paper start - replace light engine impl ++ this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> { ++ return this.theLightEngine.sectionChange(pos, notReady); ++ }); ++ // Paper end - replace light engine impl + } + + @Override + public void propagateLightSources(ChunkPos chunkPos) { ++ if (true) return; // Paper - replace light engine impl + this.addTask( + chunkPos.x, + chunkPos.z, +@@ -105,6 +263,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + + @Override + public void setLightEnabled(ChunkPos pos, boolean retainData) { ++ if (true) return; // Paper - replace light engine impl + this.addTask( + pos.x, + pos.z, +@@ -115,6 +274,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + + @Override + public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles) { ++ if (true) return; // Paper - replace light engine impl + this.addTask( + pos.x(), + pos.z(), +@@ -139,12 +299,14 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + + @Override + public void retainData(ChunkPos pos, boolean retainData) { ++ if (true) return; // Paper - replace light engine impl + this.addTask( + pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> super.retainData(pos, retainData), () -> "retainData " + pos) + ); + } + + public CompletableFuture initializeLight(ChunkAccess chunk, boolean bl) { ++ if (true) return CompletableFuture.completedFuture(chunk); // Paper - replace light engine impl + ChunkPos chunkPos = chunk.getPos(); + this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { + LevelChunkSection[] levelChunkSections = chunk.getSections(); +@@ -165,6 +327,37 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + } + + public CompletableFuture lightChunk(ChunkAccess chunk, boolean excludeBlocks) { ++ // Paper start - replace light engine impl ++ if (true) { ++ boolean lit = excludeBlocks; ++ final ChunkPos chunkPos = chunk.getPos(); ++ ++ return CompletableFuture.supplyAsync(() -> { ++ final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(chunk); ++ if (!lit) { ++ chunk.setLightCorrect(false); ++ this.theLightEngine.lightChunk(chunk, emptySections); ++ chunk.setLightCorrect(true); ++ } else { ++ this.theLightEngine.forceLoadInChunk(chunk, 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(chunkPos.x, chunkPos.z); ++ } ++ ++ this.chunkMap.releaseLightTicket(chunkPos); ++ return chunk; ++ }, (runnable) -> { ++ this.theLightEngine.scheduleChunkLight(chunkPos, runnable); ++ this.tryScheduleUpdate(); ++ }).whenComplete((final ChunkAccess c, final Throwable throwable) -> { ++ if (throwable != null) { ++ LOGGER.error("Failed to light chunk " + chunkPos, throwable); ++ } ++ }); ++ } ++ // Paper end - replace light engine impl + ChunkPos chunkPos = chunk.getPos(); + chunk.setLightCorrect(false); + this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { +@@ -180,7 +373,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + } + + public void tryScheduleUpdate() { +- if ((!this.lightTasks.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) { ++ if (this.hasLightWork() && this.scheduled.compareAndSet(false, true)) { // Paper // Paper - rewrite light engine + this.taskMailbox.tell(() -> { + this.runUpdate(); + this.scheduled.set(false); +@@ -201,7 +394,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + } + + objectListIterator.back(j); +- super.runLightUpdates(); ++ this.theLightEngine.propagateChanges(); // Paper - rewrite light engine + + for (int var5 = 0; objectListIterator.hasNext() && var5 < i; var5++) { + Pair pair2 = objectListIterator.next(); +diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java +index 0d536d72ac918fbd403397ff369d10143ee9c204..6051e5f272838ef23276a90e21c2fc821ca155d1 100644 +--- a/src/main/java/net/minecraft/server/level/TicketType.java ++++ b/src/main/java/net/minecraft/server/level/TicketType.java +@@ -26,6 +26,7 @@ public class TicketType { + public static final TicketType UNKNOWN = TicketType.create("unknown", Comparator.comparingLong(ChunkPos::toLong), 1); + public static final TicketType PLUGIN = TicketType.create("plugin", (a, b) -> 0); // CraftBukkit + public static final TicketType PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit ++ public static final TicketType CHUNK_RELIGHT = create("light_update", Long::compareTo); // Paper - ensure chunks stay loaded for lighting + + public static TicketType create(String name, Comparator argumentComparator) { + return new TicketType<>(name, argumentComparator, 0L); +diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +index 333a02e08cccf5cb0efa2076582cbd69e95ff0c0..ca4c8e256047a4af45811c3e772b5a959e2ae941 100644 +--- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java ++++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +@@ -109,6 +109,27 @@ public class WorldGenRegion implements WorldGenLevel { + } + } + ++ // Paper start - starlight ++ @Override ++ public int getBrightness(final net.minecraft.world.level.LightLayer lightLayer, final BlockPos blockPos) { ++ final ChunkAccess chunk = this.getChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4); ++ if (!chunk.isLightCorrect()) { ++ return 0; ++ } ++ return this.getLightEngine().getLayerListener(lightLayer).getLightValue(blockPos); ++ } ++ ++ ++ @Override ++ public int getRawBrightness(final BlockPos blockPos, final int subtract) { ++ final ChunkAccess chunk = this.getChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4); ++ if (!chunk.isLightCorrect()) { ++ return 0; ++ } ++ return this.getLightEngine().getRawBrightness(blockPos, subtract); ++ } ++ // Paper end - starlight ++ + public boolean isOldChunkAround(ChunkPos chunkPos, int checkRadius) { + return this.level.getChunkSource().chunkMap.isOldChunkAround(chunkPos, checkRadius); + } +diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +index f87d9cb38caf3bf92fd32f2118f76799ede418db..c7da359c525522b55763e594a1db0c26a026b73f 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java ++++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +@@ -812,6 +812,7 @@ public abstract class BlockBehaviour implements FeatureElement { + this.spawnTerrainParticles = blockbase_info.spawnTerrainParticles; + this.instrument = blockbase_info.instrument; + this.replaceable = blockbase_info.replaceable; ++ this.conditionallyFullOpaque = this.canOcclude & this.useShapeForLightOcclusion; // Paper + } + // Paper start - Perf: impl cached craft block data, lazy load to fix issue with loading at the wrong time + private org.bukkit.craftbukkit.block.data.CraftBlockData cachedCraftBlockData; +@@ -848,6 +849,18 @@ public abstract class BlockBehaviour implements FeatureElement { + return this.shapeExceedsCube; + } + // Paper end ++ // Paper start - starlight ++ 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; ++ } ++ // Paper end - starlight + + public void initCache() { + this.fluidState = ((Block) this.owner).getFluidState(this.asState()); +@@ -856,6 +869,7 @@ public abstract class BlockBehaviour implements FeatureElement { + this.cache = new BlockBehaviour.BlockStateBase.Cache(this.asState()); + } + this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here ++ this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque() ? -1 : this.cache.lightBlock; // Paper - starlight - cache opacity for light + + this.legacySolid = this.calculateSolid(); + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java +index f4e3bd2ae4f63e6d3d25463a3635b8f89fecc068..1f8c72b6c7d8683d67880fa175843c73b3d39b78 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java +@@ -77,7 +77,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom + @Nullable + protected BlendingData blendingData; + public final Map heightmaps = Maps.newEnumMap(Heightmap.Types.class); +- protected ChunkSkyLightSources skyLightSources; ++ // Paper - starlight - remove skyLightSources + private final Map structureStarts = Maps.newHashMap(); + private final Map structuresRefences = Maps.newHashMap(); + protected final Map pendingBlockEntities = Maps.newHashMap(); +@@ -89,8 +89,55 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom + private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry(); + public org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer(ChunkAccess.DATA_TYPE_REGISTRY); + // CraftBukkit end ++ // Paper start - rewrite light engine ++ private volatile ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] blockNibbles; ++ ++ private volatile ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] skyNibbles; ++ ++ private volatile boolean[] skyEmptinessMap; ++ ++ private volatile boolean[] blockEmptinessMap; ++ ++ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getBlockNibbles() { ++ return this.blockNibbles; ++ } ++ ++ public void setBlockNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) { ++ this.blockNibbles = nibbles; ++ } ++ ++ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getSkyNibbles() { ++ return this.skyNibbles; ++ } ++ ++ public void setSkyNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) { ++ this.skyNibbles = nibbles; ++ } ++ ++ public boolean[] getSkyEmptinessMap() { ++ return this.skyEmptinessMap; ++ } ++ ++ public void setSkyEmptinessMap(final boolean[] emptinessMap) { ++ this.skyEmptinessMap = emptinessMap; ++ } ++ ++ public boolean[] getBlockEmptinessMap() { ++ return this.blockEmptinessMap; ++ } ++ ++ public void setBlockEmptinessMap(final boolean[] emptinessMap) { ++ this.blockEmptinessMap = emptinessMap; ++ } ++ // Paper end - rewrite light engine + + public ChunkAccess(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry biomeRegistry, long inhabitedTime, @Nullable LevelChunkSection[] sectionArray, @Nullable BlendingData blendingData) { ++ // Paper start - rewrite light engine ++ if (!(this instanceof ImposterProtoChunk)) { ++ this.setBlockNibbles(ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(heightLimitView)); ++ this.setSkyNibbles(ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(heightLimitView)); ++ } ++ // Paper end - rewrite light engine + this.locX = pos.x; this.locZ = pos.z; // Paper - reduce need for field lookups + this.chunkPos = pos; this.coordinateKey = ChunkPos.asLong(locX, locZ); // Paper - cache long key + this.upgradeData = upgradeData; +@@ -99,7 +146,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom + this.inhabitedTime = inhabitedTime; + this.postProcessing = new ShortList[heightLimitView.getSectionsCount()]; + this.blendingData = blendingData; +- this.skyLightSources = new ChunkSkyLightSources(heightLimitView); ++ // Paper - starlight - remove skyLightSources + if (sectionArray != null) { + if (this.sections.length == sectionArray.length) { + System.arraycopy(sectionArray, 0, this.sections, 0, this.sections.length); +@@ -510,12 +557,12 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom + } + + public void initializeLightSources() { +- this.skyLightSources.fillFrom(this); ++ // Paper - starlight - remove skyLightSources + } + + @Override + public ChunkSkyLightSources getSkyLightSources() { +- return this.skyLightSources; ++ return null; // Paper - starlight - remove skyLightSources + } + + public static record TicksToSave(SerializableTickContainer blocks, SerializableTickContainer fluids) { +diff --git a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java +index 2ee1658532cb00d7bcd1d11e03f19d21ca7f2a9e..ac754827172a4de600d0a57a7d11853481a2dbf2 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java +@@ -21,6 +21,40 @@ public class EmptyLevelChunk extends LevelChunk { + this.biome = biomeEntry; + } + ++ // Paper start - starlight ++ @Override ++ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getBlockNibbles() { ++ return ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(this.getLevel()); ++ } ++ ++ @Override ++ public void setBlockNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) {} ++ ++ @Override ++ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getSkyNibbles() { ++ return ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(this.getLevel()); ++ } ++ ++ @Override ++ public void setSkyNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) {} ++ ++ @Override ++ public boolean[] getSkyEmptinessMap() { ++ return null; ++ } ++ ++ @Override ++ public void setSkyEmptinessMap(final boolean[] emptinessMap) {} ++ ++ @Override ++ public boolean[] getBlockEmptinessMap() { ++ return null; ++ } ++ ++ @Override ++ public void setBlockEmptinessMap(final boolean[] emptinessMap) {} ++ // Paper end - starlight ++ + @Override + public BlockState getBlockState(BlockPos pos) { + return Blocks.VOID_AIR.defaultBlockState(); +diff --git a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java +index 2953e93965aa688be8fc1620580701ba0c9d907e..aa5dee839d4c0dbc3c2abee9b501ec250c575cb3 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java +@@ -47,6 +47,48 @@ public class ImposterProtoChunk extends ProtoChunk { + this.allowWrites = propagateToWrapped; + } + ++ // Paper start - rewrite light engine ++ @Override ++ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getBlockNibbles() { ++ return this.wrapped.getBlockNibbles(); ++ } ++ ++ @Override ++ public void setBlockNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) { ++ this.wrapped.setBlockNibbles(nibbles); ++ } ++ ++ @Override ++ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getSkyNibbles() { ++ return this.wrapped.getSkyNibbles(); ++ } ++ ++ @Override ++ public void setSkyNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) { ++ this.wrapped.setSkyNibbles(nibbles); ++ } ++ ++ @Override ++ public boolean[] getSkyEmptinessMap() { ++ return this.wrapped.getSkyEmptinessMap(); ++ } ++ ++ @Override ++ public void setSkyEmptinessMap(final boolean[] emptinessMap) { ++ this.wrapped.setSkyEmptinessMap(emptinessMap); ++ } ++ ++ @Override ++ public boolean[] getBlockEmptinessMap() { ++ return this.wrapped.getBlockEmptinessMap(); ++ } ++ ++ @Override ++ public void setBlockEmptinessMap(final boolean[] emptinessMap) { ++ this.wrapped.setBlockEmptinessMap(emptinessMap); ++ } ++ // Paper end - rewrite light engine ++ + @Nullable + @Override + public BlockEntity getBlockEntity(BlockPos pos) { +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +index 8de6ad8b131061b2dae440dff71e2e6e7af2de39..bac191f92ea3735df19c68d5568c2c7962c8680f 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -222,6 +222,12 @@ public class LevelChunk extends ChunkAccess { + + public LevelChunk(ServerLevel world, ProtoChunk protoChunk, @Nullable LevelChunk.PostLoadProcessor entityLoader) { + this(world, protoChunk.getPos(), protoChunk.getUpgradeData(), protoChunk.unpackBlockTicks(), protoChunk.unpackFluidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), entityLoader, protoChunk.getBlendingData()); ++ // Paper start - rewrite light engine ++ this.setBlockNibbles(protoChunk.getBlockNibbles()); ++ this.setSkyNibbles(protoChunk.getSkyNibbles()); ++ this.setSkyEmptinessMap(protoChunk.getSkyEmptinessMap()); ++ this.setBlockEmptinessMap(protoChunk.getBlockEmptinessMap()); ++ // Paper end - rewrite light engine + Iterator iterator = protoChunk.getBlockEntities().values().iterator(); + + while (iterator.hasNext()) { +@@ -248,7 +254,7 @@ public class LevelChunk extends ChunkAccess { + } + } + +- this.skyLightSources = protoChunk.skyLightSources; ++ // Paper - starlight - remove skyLightSources + this.setLightCorrect(protoChunk.isLightCorrect()); + this.unsaved = true; + this.needsDecoration = true; // CraftBukkit +@@ -437,7 +443,7 @@ public class LevelChunk extends ChunkAccess { + ProfilerFiller gameprofilerfiller = this.level.getProfiler(); + + gameprofilerfiller.push("updateSkyLightSources"); +- this.skyLightSources.update(this, j, i, l); ++ // Paper - starlight - remove skyLightSources + gameprofilerfiller.popPush("queueCheckLight"); + this.level.getChunkSource().getLightEngine().checkBlock(blockposition); + gameprofilerfiller.pop(); +diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java +index 2fa0097a9374a89177e4f1068d1bfed30b8ff122..fa9df6ebcd90d4e9e5836a37212b1f60665783b1 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java ++++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java +@@ -155,7 +155,7 @@ public class PalettedContainer implements PaletteResize, PalettedContainer + return this.get(this.strategy.getIndex(x, y, z)); + } + +- protected T get(int index) { ++ public T get(int index) { // Paper - public + PalettedContainer.Data data = this.data; + return data.palette.valueFor(data.storage.get(index)); + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java +index bcc70883d23d38c408130ffe778205e371ff4e8a..576ae0cb138b265c8a3995de7b5ebc827d50949d 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java +@@ -143,7 +143,7 @@ public class ProtoChunk extends ChunkAccess { + } + + if (LightEngine.hasDifferentLightProperties(this, pos, blockState, state)) { +- this.skyLightSources.update(this, m, j, o); ++ // Paper - starlight - remove skyLightSources + this.lightEngine.checkBlock(pos); + } + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java +index ae992aeb8b836e8c2e5bab338ae46cc31c317245..95318092f8281d98132d1d3ceb4a5c36cf32eb05 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java ++++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java +@@ -117,6 +117,18 @@ public class ChunkStatus { + private final ChunkType chunkType; + private final EnumSet heightmapsAfter; + ++ // Paper start - starlight ++ public static ChunkStatus getStatus(String name) { ++ try { ++ // We need this otherwise we return EMPTY for invalid names ++ ResourceLocation key = new ResourceLocation(name); ++ return BuiltInRegistries.CHUNK_STATUS.getOptional(key).orElse(null); ++ } catch (Exception ex) { ++ return null; // invalid name ++ } ++ } ++ // Paper end - starlight ++ + private static ChunkStatus register( + String id, + @Nullable ChunkStatus previous, +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +index 88f0aca2da0e14ed5ec0513944fa0ba28b73b5d1..01d6b8683a9fa30d05b03ebfef8ee2dca4e83a56 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +@@ -90,6 +90,14 @@ public class ChunkSerializer { + private static final int CURRENT_DATA_VERSION = net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion(); + private static final boolean JUST_CORRUPT_IT = Boolean.getBoolean("Paper.ignoreWorldDataVersion"); + // Paper end - Do not let the server load chunks from newer versions ++ // Paper start - replace light engine impl ++ private static final int STARLIGHT_LIGHT_VERSION = 9; ++ ++ private static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state"; ++ private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state"; ++ private static final String STARLIGHT_VERSION_TAG = "starlight.light_version"; ++ // Paper end - replace light engine impl ++ + public ChunkSerializer() {} + + // Paper start - guard against serializing mismatching coordinates +@@ -121,19 +129,26 @@ public class ChunkSerializer { + } + + UpgradeData chunkconverter = nbt.contains("UpgradeData", 10) ? new UpgradeData(nbt.getCompound("UpgradeData"), world) : UpgradeData.EMPTY; +- boolean flag = nbt.getBoolean("isLightOn"); ++ boolean flag = getStatus(nbt) != null && getStatus(nbt).isOrAfter(ChunkStatus.LIGHT) && nbt.get("isLightOn") != null && nbt.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION; // Paper + ListTag nbttaglist = nbt.getList("sections", 10); + int i = world.getSectionsCount(); + LevelChunkSection[] achunksection = new LevelChunkSection[i]; + boolean flag1 = world.dimensionType().hasSkyLight(); + ServerChunkCache chunkproviderserver = world.getChunkSource(); + LevelLightEngine levellightengine = chunkproviderserver.getLightEngine(); ++ // Paper start ++ ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] blockNibbles = ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(world); ++ ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] skyNibbles = ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(world); ++ final int minSection = io.papermc.paper.util.WorldUtil.getMinLightSection(world); ++ final int maxSection = io.papermc.paper.util.WorldUtil.getMaxLightSection(world); ++ boolean canReadSky = world.dimensionType().hasSkyLight(); ++ // Paper end + Registry iregistry = world.registryAccess().registryOrThrow(Registries.BIOME); + Codec>> codec = ChunkSerializer.makeBiomeCodecRW(iregistry); // CraftBukkit - read/write + boolean flag2 = false; + + for (int j = 0; j < nbttaglist.size(); ++j) { +- CompoundTag nbttagcompound1 = nbttaglist.getCompound(j); ++ CompoundTag nbttagcompound1 = nbttaglist.getCompound(j); CompoundTag sectionData = nbttagcompound1; // Paper + byte b0 = nbttagcompound1.getByte("Y"); + int k = world.getSectionIndexFromSectionY(b0); + +@@ -169,19 +184,39 @@ public class ChunkSerializer { + boolean flag3 = nbttagcompound1.contains("BlockLight", 7); + boolean flag4 = flag1 && nbttagcompound1.contains("SkyLight", 7); + +- if (flag3 || flag4) { +- if (!flag2) { +- levellightengine.retainData(chunkPos, true); +- flag2 = true; +- } +- ++ // Paper start - rewrite the light engine ++ if (flag) { ++ try { ++ int y = sectionData.getByte("Y"); ++ // Paper end - rewrite the light engine + if (flag3) { +- levellightengine.queueSectionData(LightLayer.BLOCK, SectionPos.of(chunkPos, b0), new DataLayer(nbttagcompound1.getByteArray("BlockLight"))); ++ // Paper start - rewrite the light engine ++ // this is where our diff is ++ blockNibbles[y - minSection] = new ca.spottedleaf.starlight.common.light.SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety ++ } else { ++ blockNibbles[y - minSection] = new ca.spottedleaf.starlight.common.light.SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG)); ++ // Paper end - rewrite the light engine + } + + if (flag4) { +- levellightengine.queueSectionData(LightLayer.SKY, SectionPos.of(chunkPos, b0), new DataLayer(nbttagcompound1.getByteArray("SkyLight"))); ++ // Paper start - rewrite the light engine ++ // we store under the same key so mod programs editing nbt ++ // can still read the data, hopefully. ++ // however, for compatibility we store chunks as unlit so vanilla ++ // is forced to re-light them if it encounters our data. It's too much of a burden ++ // to try and maintain compatibility with a broken and inferior skylight management system. ++ skyNibbles[y - minSection] = new ca.spottedleaf.starlight.common.light.SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety ++ } else if (flag1) { ++ skyNibbles[y - minSection] = new ca.spottedleaf.starlight.common.light.SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG)); ++ // Paper end - rewrite the light engine ++ } ++ ++ // Paper start - rewrite the light engine ++ } catch (Exception ex) { ++ LOGGER.warn("Failed to load light data for chunk " + chunkPos + " in world '" + world.getWorld().getName() + "', light will be regenerated", ex); ++ flag = false; + } ++ // Paper end - rewrite light engine + } + } + +@@ -211,6 +246,8 @@ public class ChunkSerializer { + }, chunkPos); + + object1 = new LevelChunk(world.getLevel(), chunkPos, chunkconverter, levelchunkticks, levelchunkticks1, l, achunksection, ChunkSerializer.postLoadChunk(world, nbt), blendingdata); ++ ((LevelChunk)object1).setBlockNibbles(blockNibbles); // Paper - replace light impl ++ ((LevelChunk)object1).setSkyNibbles(skyNibbles); // Paper - replace light impl + } else { + ProtoChunkTicks protochunkticklist = ProtoChunkTicks.load(nbt.getList("block_ticks", 10), (s) -> { + return BuiltInRegistries.BLOCK.getOptional(ResourceLocation.tryParse(s)); +@@ -219,6 +256,8 @@ public class ChunkSerializer { + return BuiltInRegistries.FLUID.getOptional(ResourceLocation.tryParse(s)); + }, chunkPos); + ProtoChunk protochunk = new ProtoChunk(chunkPos, chunkconverter, achunksection, protochunkticklist, protochunkticklist1, world, iregistry, blendingdata); ++ protochunk.setBlockNibbles(blockNibbles); // Paper - replace light impl ++ protochunk.setSkyNibbles(skyNibbles); // Paper - replace light impl + + object1 = protochunk; + protochunk.setInhabitedTime(l); +@@ -340,6 +379,12 @@ public class ChunkSerializer { + // CraftBukkit end + + public static CompoundTag write(ServerLevel world, ChunkAccess chunk) { ++ // Paper start - rewrite light impl ++ final int minSection = io.papermc.paper.util.WorldUtil.getMinLightSection(world); ++ final int maxSection = io.papermc.paper.util.WorldUtil.getMaxLightSection(world); ++ ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] blockNibbles = chunk.getBlockNibbles(); ++ ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] skyNibbles = chunk.getSkyNibbles(); ++ // Paper end - rewrite light impl + ChunkPos chunkcoordintpair = chunk.getPos(); + CompoundTag nbttagcompound = NbtUtils.addCurrentDataVersion(new CompoundTag()); + +@@ -389,11 +434,14 @@ public class ChunkSerializer { + for (int i = lightenginethreaded.getMinLightSection(); i < lightenginethreaded.getMaxLightSection(); ++i) { + int j = chunk.getSectionIndexFromSectionY(i); + boolean flag1 = j >= 0 && j < achunksection.length; +- DataLayer nibblearray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); +- DataLayer nibblearray1 = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); ++ // Paper - replace light engine + +- if (flag1 || nibblearray != null || nibblearray1 != null) { +- CompoundTag nbttagcompound1 = new CompoundTag(); ++ // Paper start - replace light engine ++ ca.spottedleaf.starlight.common.light.SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection].getSaveState(); ++ ca.spottedleaf.starlight.common.light.SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection].getSaveState(); ++ if (flag1 || blockNibble != null || skyNibble != null) { ++ // Paper end - replace light engine ++ CompoundTag nbttagcompound1 = new CompoundTag(); CompoundTag section = nbttagcompound1; // Paper + + if (flag1) { + LevelChunkSection chunksection = achunksection[j]; +@@ -402,13 +450,27 @@ public class ChunkSerializer { + nbttagcompound1.put("biomes", (Tag) codec.encodeStart(NbtOps.INSTANCE, chunksection.getBiomes()).getOrThrow()); + } + +- if (nibblearray != null && !nibblearray.isEmpty()) { +- nbttagcompound1.putByteArray("BlockLight", nibblearray.getData()); ++ // Paper start ++ // we store under the same key so mod programs editing nbt ++ // can still read the data, hopefully. ++ // however, for compatibility we store chunks as unlit so vanilla ++ // is forced to re-light them if it encounters our data. It's too much of a burden ++ // to try and maintain compatibility with a broken and inferior skylight management system. ++ ++ if (blockNibble != null) { ++ if (blockNibble.data != null) { ++ section.putByteArray("BlockLight", blockNibble.data); ++ } ++ section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state); + } + +- if (nibblearray1 != null && !nibblearray1.isEmpty()) { +- nbttagcompound1.putByteArray("SkyLight", nibblearray1.getData()); ++ if (skyNibble != null) { ++ if (skyNibble.data != null) { ++ section.putByteArray("SkyLight", skyNibble.data); ++ } ++ section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state); + } ++ // Paper end + + if (!nbttagcompound1.isEmpty()) { + nbttagcompound1.putByte("Y", (byte) i); +@@ -419,7 +481,8 @@ public class ChunkSerializer { + + nbttagcompound.put("sections", nbttaglist); + if (flag) { +- nbttagcompound.putBoolean("isLightOn", true); ++ nbttagcompound.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // Paper ++ nbttagcompound.putBoolean("isLightOn", false); // Paper - set to false but still store, this allows us to detect --eraseCache (as eraseCache _removes_) + } + + ListTag nbttaglist1 = new ListTag(); +@@ -493,6 +556,17 @@ public class ChunkSerializer { + })); + } + ++ // Paper start ++ public static @Nullable ChunkStatus getStatus(@Nullable CompoundTag compound) { ++ if (compound == null) { ++ return null; ++ } ++ ++ // Note: Copied from below ++ return ChunkStatus.getStatus(compound.getString("Status")); ++ } ++ // Paper end ++ + public static ChunkType getChunkTypeFromTag(@Nullable CompoundTag nbt) { + return nbt != null ? ChunkStatus.byName(nbt.getString("Status")).getChunkType() : ChunkType.PROTOCHUNK; + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 34f03dd227181a03fa90845067424a26382bab9b..4b6a04e47f5d4c071607516519098fab317dcf12 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -507,12 +507,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + } + } + +- for (final ChunkPos pos : chunksToRelight) { +- final ChunkAccess chunk = serverChunkCache.getChunk(pos.x, pos.z, false); +- if (chunk != null) { +- serverChunkCache.getLightEngine().lightChunk(chunk, false); +- } +- } ++ serverChunkCache.getLightEngine().relight(chunksToRelight, pos -> {}, relit -> {}); // Paper - Starlight + + return true; + // Paper end - implement regenerate chunk method diff --git a/patches/server/0984-disable-forced-empty-world-ticks.patch b/patches/server/0984-disable-forced-empty-world-ticks.patch deleted file mode 100644 index 6ef5295591..0000000000 --- a/patches/server/0984-disable-forced-empty-world-ticks.patch +++ /dev/null @@ -1,19 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Shane Freeder -Date: Tue, 21 Mar 2023 23:51:46 +0000 -Subject: [PATCH] disable forced empty world ticks - - -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 5eab8532bfe3b7082123a12b8a3e62d25a4463d3..7dfbb3dd6100180abd2ebab4d1e9e34928666f9e 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -533,7 +533,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - - this.handlingTick = false; - gameprofilerfiller.pop(); -- boolean flag1 = true || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players -+ boolean flag1 = !paperConfig().unsupportedSettings.disableWorldTickingWhenEmpty || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players // Paper - restore this - - if (flag1) { - this.resetEmptyTime(); diff --git a/patches/server/0985-Rewrite-chunk-system.patch b/patches/server/0985-Rewrite-chunk-system.patch new file mode 100644 index 0000000000..d1a25169df --- /dev/null +++ b/patches/server/0985-Rewrite-chunk-system.patch @@ -0,0 +1,21905 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Thu, 11 Mar 2021 02:32:30 -0800 +Subject: [PATCH] Rewrite chunk system + +Rebased patches: + +New player chunk loader system + +Make ChunkStatus.EMPTY not rely on the main thread for completion + +In order to do this, we need to push the POI consistency checks +to a later status. Since FULL is the only other status that +uses the main thread, it can go there. + +The consistency checks are only really for when a desync occurs, +and so that delaying the check only matters when the chunk data +has desync'd. As long as the desync is sorted before the +chunk is full loaded (i.e before setBlock can occur on +a chunk), it should not matter. + +This change is primarily due to behavioural changes +in the chunk task queue brought by region threading - +which is to split the queue into separate regions. As such, +it is required that in order for the sync load to complete +that the region owning the chunk drain and execute the task +while ticking. However, that is not always possible in +region threading. Thus, removing the main thread reliance allows +the chunk to progress without requiring a tick thread. +Specifically, this allows far sync loads (outside of a specific +regions bounds) to occur without issue - namely with structure +searching. + +Increase parallelism for neighbour writing chunk statuses + +Namely, everything after FEATURES. By creating a dependency +chain indicating what chunks are in use, we can safely +schedule completely independent tasks in parallel. This +will allow the chunk system to scale beyond 10 threads +per world. + +Properly cancel chunk load tasks that were not scheduled + +Since the chunk load task was not scheduled, the entity/poi load +task fields will not be set, but the task complete counter +will not be adjusted. Thus, the chunk load task will not complete. + +To resolve this, detect when the entity/poi tasks were not scheduled +and decrement the task complete counter in such cases. + +Mark POI/Entity load tasks as completed before releasing scheduling lock + +It must be marked as completed during that lock hold since the +waiters field is set to null. Thus, any other thread attempting +a cancellation will fail to remove from waiters. Also, any +other thread attempting to cancel may set the completed field +to true which would cause accept() to fail as well. + +Completion was always designed to happen while holding the +scheduling lock to prevent these race conditions. The code +was originally set up to complete while not holding the +scheduling lock to avoid invoking callbacks while holding the +lock, however the access to the completion field was not +considered. + +Resolve this by marking the callback as completed during the +lock, but invoking the accept() function after releasing +the lock. This will prevent any cancellation attempts to be +blocked, and allow the current thread to complete the callback +without any issues. + +Cache whether region files do not exist + +The repeated I/O of creating the directory for the regionfile +or for checking if the file exists can be heavy in +when pushing chunk generation extremely hard - as each chunk gen +request may effectively go through to the I/O thread. + +Use coordinate-based locking to increase chunk system parallelism + +A significant overhead in Folia comes from the chunk system's +locks, the ticket lock and the scheduling lock. The public +test server, which had ~330 players, had signficant performance +problems with these locks: ~80% of the time spent ticking +was _waiting_ for the locks to free. Given that it used +around 15 cores total at peak, this is a complete and utter loss +of potential. + +To address this issue, I have replaced the ticket lock and scheduling +lock with two ReentrantAreaLocks. The ReentrantAreaLock takes a +shift, which is used internally to group positions into sections. +This grouping is neccessary, as the possible radius of area that +needs to be acquired for any given lock usage is up to 64. As such, +the shift is critical to reduce the number of areas required to lock +for any lock operation. Currently, it is set to a shift of 6, which +is identical to the ticket level propagation shift (and, it must be +at least the ticket level propagation shift AND the region shift). + +The chunk system locking changes required a complete rewrite of the +chunk system tick, chunk system unload, and chunk system ticket level +propagation - as all of the previous logic only works with a single +global lock. + +This does introduce two other section shifts: the lock shift, and the +ticket shift. The lock shift is simply what shift the area locks use, +and the ticket shift represents the size of the ticket sections. +Currently, these values are just set to the region shift for simplicity. +However, they are not arbitrary: the lock shift must be at least the size +of the ticket shift and must be at least the size of the region shift. +The ticket shift must also be >= the ceil(log2(max ticket level source)). + +The chunk system's ticket propagator is now global state, instead of +region state. This cleans up the logic for ticket levels significantly, +and removes usage of the region lock in this area, but it also means +that the addition of a ticket no longer creates a region. To alleviate +the side effects of this change, the global tick thread now processes +ticket level updates for each world every tick to guarantee eventual +ticket level processing. The chunk system also provides a hook to +process ticket level changes in a given _section_, so that the +region queue can guarantee that after adding its reference counter +that the region section is created/exists/wont be destroyed. + +The ticket propagator operates by updating the sources in a single ticket +section, and propagating the updates to its 1 radius neighbours. This +allows the ticket updates to occur in parallel or selectively (see above). +Currently, the process ticket level update function operates by +polling from a concurrent queue of sections to update and simply +invoking the single section update logic. This allows the function +to operate completely in parallel, provided the queue is ordered right. +Additionally, this limits the area used in the ticket/scheduling lock +when processing updates, which should massively increase parallelism compared +to before. + +The chunk system ticket addition for expirable ticket types has been modified +to no longer track exact tick deadlines, as this relies on what region the +ticket is in. Instead, the chunk system tracks a map of +lock section -> (chunk coordinate -> expire ticket count) and every ticket +has been changed to have a removeDelay count that is decremented each tick. +Each region searches its own sections to find tickets to try to expire. + +Chunk system unloading has been modified to track unloads by lock section. +The ordering is determined by which section a chunk resides in. +The unload process now removes from unload sections and processes +the full unload stages (1, 2, 3) before moving to the next section, if possible. +This allows the unload logic to only hold one lock section at a time for +each lock, which is a massive parallelism increase. + +In stress testing, these changes lowered the locking overhead to only 5% +from ~70%, which completely fix the original problem as described. + +== AT == +public net.minecraft.server.level.ChunkHolder pos +public net.minecraft.server.level.ChunkMap overworldDataStorage +public-f net.minecraft.world.level.chunk.storage.RegionFileStorage +public net.minecraft.server.level.ChunkMap getPoiManager()Lnet/minecraft/world/entity/ai/village/poi/PoiManager; + +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java b/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4fd9a0cd8f1e6ae1a97e963dc7731a80bc6fac5b +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java +@@ -0,0 +1,395 @@ ++package ca.spottedleaf.concurrentutil.lock; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import it.unimi.dsi.fastutil.HashCommon; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.concurrent.ConcurrentHashMap; ++import java.util.concurrent.locks.LockSupport; ++ ++public final class ReentrantAreaLock { ++ ++ public final int coordinateShift; ++ ++ // aggressive load factor to reduce contention ++ private final ConcurrentHashMap nodes = new ConcurrentHashMap<>(128, 0.2f); ++ ++ public ReentrantAreaLock(final int coordinateShift) { ++ this.coordinateShift = coordinateShift; ++ } ++ ++ public boolean isHeldByCurrentThread(final int x, final int z) { ++ final Thread currThread = Thread.currentThread(); ++ final int shift = this.coordinateShift; ++ final int sectionX = x >> shift; ++ final int sectionZ = z >> shift; ++ ++ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ)); ++ final Node node = this.nodes.get(coordinate); ++ ++ return node != null && node.thread == currThread; ++ } ++ ++ public boolean isHeldByCurrentThread(final int centerX, final int centerZ, final int radius) { ++ return this.isHeldByCurrentThread(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius); ++ } ++ ++ public boolean isHeldByCurrentThread(final int fromX, final int fromZ, final int toX, final int toZ) { ++ if (fromX > toX || fromZ > toZ) { ++ throw new IllegalArgumentException(); ++ } ++ ++ final Thread currThread = Thread.currentThread(); ++ final int shift = this.coordinateShift; ++ final int fromSectionX = fromX >> shift; ++ final int fromSectionZ = fromZ >> shift; ++ final int toSectionX = toX >> shift; ++ final int toSectionZ = toZ >> shift; ++ ++ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) { ++ for (int currX = fromSectionX; currX <= toSectionX; ++currX) { ++ final Coordinate coordinate = new Coordinate(Coordinate.key(currX, currZ)); ++ ++ final Node node = this.nodes.get(coordinate); ++ ++ if (node == null || node.thread != currThread) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ public Node tryLock(final int x, final int z) { ++ return this.tryLock(x, z, x, z); ++ } ++ ++ public Node tryLock(final int centerX, final int centerZ, final int radius) { ++ return this.tryLock(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius); ++ } ++ ++ public Node tryLock(final int fromX, final int fromZ, final int toX, final int toZ) { ++ if (fromX > toX || fromZ > toZ) { ++ throw new IllegalArgumentException(); ++ } ++ ++ final Thread currThread = Thread.currentThread(); ++ final int shift = this.coordinateShift; ++ final int fromSectionX = fromX >> shift; ++ final int fromSectionZ = fromZ >> shift; ++ final int toSectionX = toX >> shift; ++ final int toSectionZ = toZ >> shift; ++ ++ final List areaAffected = new ArrayList<>(); ++ ++ final Node ret = new Node(this, areaAffected, currThread); ++ ++ boolean failed = false; ++ ++ // try to fast acquire area ++ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) { ++ for (int currX = fromSectionX; currX <= toSectionX; ++currX) { ++ final Coordinate coordinate = new Coordinate(Coordinate.key(currX, currZ)); ++ ++ final Node prev = this.nodes.putIfAbsent(coordinate, ret); ++ ++ if (prev == null) { ++ areaAffected.add(coordinate); ++ continue; ++ } ++ ++ if (prev.thread != currThread) { ++ failed = true; ++ break; ++ } ++ } ++ } ++ ++ if (!failed) { ++ return ret; ++ } ++ ++ // failed, undo logic ++ if (!areaAffected.isEmpty()) { ++ for (int i = 0, len = areaAffected.size(); i < len; ++i) { ++ final Coordinate key = areaAffected.get(i); ++ ++ if (this.nodes.remove(key) != ret) { ++ throw new IllegalStateException(); ++ } ++ } ++ ++ areaAffected.clear(); ++ ++ // since we inserted, we need to drain waiters ++ Thread unpark; ++ while ((unpark = ret.pollOrBlockAdds()) != null) { ++ LockSupport.unpark(unpark); ++ } ++ } ++ ++ return null; ++ } ++ ++ public Node lock(final int x, final int z) { ++ final Thread currThread = Thread.currentThread(); ++ final int shift = this.coordinateShift; ++ final int sectionX = x >> shift; ++ final int sectionZ = z >> shift; ++ ++ final List areaAffected = new ArrayList<>(1); ++ ++ final Node ret = new Node(this, areaAffected, currThread); ++ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ)); ++ ++ for (long failures = 0L;;) { ++ final Node park; ++ ++ // try to fast acquire area ++ { ++ final Node prev = this.nodes.putIfAbsent(coordinate, ret); ++ ++ if (prev == null) { ++ areaAffected.add(coordinate); ++ return ret; ++ } else if (prev.thread != currThread) { ++ park = prev; ++ } else { ++ // only one node we would want to acquire, and it's owned by this thread already ++ return ret; ++ } ++ } ++ ++ ++failures; ++ ++ if (failures > 128L && park.add(currThread)) { ++ LockSupport.park(); ++ } else { ++ // high contention, spin wait ++ if (failures < 128L) { ++ for (long i = 0; i < failures; ++i) { ++ Thread.onSpinWait(); ++ } ++ failures = failures << 1; ++ } else if (failures < 1_200L) { ++ LockSupport.parkNanos(1_000L); ++ failures = failures + 1L; ++ } else { // scale 0.1ms (100us) per failure ++ Thread.yield(); ++ LockSupport.parkNanos(100_000L * failures); ++ failures = failures + 1L; ++ } ++ } ++ } ++ } ++ ++ public Node lock(final int centerX, final int centerZ, final int radius) { ++ return this.lock(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius); ++ } ++ ++ public Node lock(final int fromX, final int fromZ, final int toX, final int toZ) { ++ if (fromX > toX || fromZ > toZ) { ++ throw new IllegalArgumentException(); ++ } ++ ++ final Thread currThread = Thread.currentThread(); ++ final int shift = this.coordinateShift; ++ final int fromSectionX = fromX >> shift; ++ final int fromSectionZ = fromZ >> shift; ++ final int toSectionX = toX >> shift; ++ final int toSectionZ = toZ >> shift; ++ ++ if (((fromSectionX ^ toSectionX) | (fromSectionZ ^ toSectionZ)) == 0) { ++ return this.lock(fromX, fromZ); ++ } ++ ++ final List areaAffected = new ArrayList<>(); ++ ++ final Node ret = new Node(this, areaAffected, currThread); ++ ++ for (long failures = 0L;;) { ++ Node park = null; ++ boolean addedToArea = false; ++ boolean alreadyOwned = false; ++ boolean allOwned = true; ++ ++ // try to fast acquire area ++ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) { ++ for (int currX = fromSectionX; currX <= toSectionX; ++currX) { ++ final Coordinate coordinate = new Coordinate(Coordinate.key(currX, currZ)); ++ ++ final Node prev = this.nodes.putIfAbsent(coordinate, ret); ++ ++ if (prev == null) { ++ addedToArea = true; ++ allOwned = false; ++ areaAffected.add(coordinate); ++ continue; ++ } ++ ++ if (prev.thread != currThread) { ++ park = prev; ++ alreadyOwned = true; ++ break; ++ } ++ } ++ } ++ ++ if (park == null) { ++ if (alreadyOwned && !allOwned) { ++ throw new IllegalStateException("Improper lock usage: Should never acquire intersecting areas"); ++ } ++ return ret; ++ } ++ ++ // failed, undo logic ++ if (addedToArea) { ++ for (int i = 0, len = areaAffected.size(); i < len; ++i) { ++ final Coordinate key = areaAffected.get(i); ++ ++ if (this.nodes.remove(key) != ret) { ++ throw new IllegalStateException(); ++ } ++ } ++ ++ areaAffected.clear(); ++ ++ // since we inserted, we need to drain waiters ++ Thread unpark; ++ while ((unpark = ret.pollOrBlockAdds()) != null) { ++ LockSupport.unpark(unpark); ++ } ++ } ++ ++ ++failures; ++ ++ if (failures > 128L && park.add(currThread)) { ++ LockSupport.park(park); ++ } else { ++ // high contention, spin wait ++ if (failures < 128L) { ++ for (long i = 0; i < failures; ++i) { ++ Thread.onSpinWait(); ++ } ++ failures = failures << 1; ++ } else if (failures < 1_200L) { ++ LockSupport.parkNanos(1_000L); ++ failures = failures + 1L; ++ } else { // scale 0.1ms (100us) per failure ++ Thread.yield(); ++ LockSupport.parkNanos(100_000L * failures); ++ failures = failures + 1L; ++ } ++ } ++ ++ if (addedToArea) { ++ // try again, so we need to allow adds so that other threads can properly block on us ++ ret.allowAdds(); ++ } ++ } ++ } ++ ++ public void unlock(final Node node) { ++ if (node.lock != this) { ++ throw new IllegalStateException("Unlock target lock mismatch"); ++ } ++ ++ final List areaAffected = node.areaAffected; ++ ++ if (areaAffected.isEmpty()) { ++ // here we are not in the node map, and so do not need to remove from the node map or unblock any waiters ++ return; ++ } ++ ++ // remove from node map; allowing other threads to lock ++ for (int i = 0, len = areaAffected.size(); i < len; ++i) { ++ final Coordinate coordinate = areaAffected.get(i); ++ if (this.nodes.remove(coordinate) != node) { ++ throw new IllegalStateException(); ++ } ++ } ++ ++ Thread unpark; ++ while ((unpark = node.pollOrBlockAdds()) != null) { ++ LockSupport.unpark(unpark); ++ } ++ } ++ ++ public static final class Node extends MultiThreadedQueue { ++ ++ private final ReentrantAreaLock lock; ++ private final List areaAffected; ++ private final Thread thread; ++ //private final Throwable WHO_CREATED_MY_ASS = new Throwable(); ++ ++ private Node(final ReentrantAreaLock lock, final List areaAffected, final Thread thread) { ++ this.lock = lock; ++ this.areaAffected = areaAffected; ++ this.thread = thread; ++ } ++ ++ @Override ++ public String toString() { ++ return "Node{" + ++ "areaAffected=" + this.areaAffected + ++ ", thread=" + this.thread + ++ '}'; ++ } ++ } ++ ++ private static final class Coordinate implements Comparable { ++ ++ public final long key; ++ ++ public Coordinate(final long key) { ++ this.key = key; ++ } ++ ++ public Coordinate(final int x, final int z) { ++ this.key = key(x, z); ++ } ++ ++ public static long key(final int x, final int z) { ++ return ((long)z << 32) | (x & 0xFFFFFFFFL); ++ } ++ ++ public static int x(final long key) { ++ return (int)key; ++ } ++ ++ public static int z(final long key) { ++ return (int)(key >>> 32); ++ } ++ ++ @Override ++ public int hashCode() { ++ return (int)HashCommon.mix(this.key); ++ } ++ ++ @Override ++ public boolean equals(final Object obj) { ++ if (this == obj) { ++ return true; ++ } ++ ++ if (!(obj instanceof Coordinate other)) { ++ return false; ++ } ++ ++ return this.key == other.key; ++ } ++ ++ // This class is intended for HashMap/ConcurrentHashMap usage, which do treeify bin nodes if the chain ++ // is too large. So we should implement compareTo to help. ++ @Override ++ public int compareTo(final Coordinate other) { ++ return Long.compare(this.key, other.key); ++ } ++ ++ @Override ++ public String toString() { ++ return "[" + x(this.key) + "," + z(this.key) + "]"; ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/concurrentutil/lock/SyncReentrantAreaLock.java b/src/main/java/ca/spottedleaf/concurrentutil/lock/SyncReentrantAreaLock.java +new file mode 100644 +index 0000000000000000000000000000000000000000..64b5803d002b2968841a5ddee987f98b72964e87 +--- /dev/null ++++ b/src/main/java/ca/spottedleaf/concurrentutil/lock/SyncReentrantAreaLock.java +@@ -0,0 +1,217 @@ ++package ca.spottedleaf.concurrentutil.lock; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.longs.LongArrayList; ++import java.util.concurrent.locks.LockSupport; ++ ++// not concurrent, unlike ReentrantAreaLock ++// no incorrect lock usage detection (acquiring intersecting areas) ++// this class is nothing more than a performance reference for ReentrantAreaLock ++public final class SyncReentrantAreaLock { ++ ++ private final int coordinateShift; ++ ++ // aggressive load factor to reduce contention ++ private final Long2ReferenceOpenHashMap nodes = new Long2ReferenceOpenHashMap<>(128, 0.2f); ++ ++ public SyncReentrantAreaLock(final int coordinateShift) { ++ this.coordinateShift = coordinateShift; ++ } ++ ++ private static long key(final int x, final int z) { ++ return ((long)z << 32) | (x & 0xFFFFFFFFL); ++ } ++ ++ public Node lock(final int x, final int z) { ++ final Thread currThread = Thread.currentThread(); ++ final int shift = this.coordinateShift; ++ final int sectionX = x >> shift; ++ final int sectionZ = z >> shift; ++ ++ final LongArrayList areaAffected = new LongArrayList(); ++ ++ final Node ret = new Node(this, areaAffected, currThread); ++ ++ final long coordinate = key(sectionX, sectionZ); ++ ++ for (long failures = 0L;;) { ++ final Node park; ++ ++ synchronized (this) { ++ // try to fast acquire area ++ final Node prev = this.nodes.putIfAbsent(coordinate, ret); ++ ++ if (prev == null) { ++ areaAffected.add(coordinate); ++ return ret; ++ } else if (prev.thread != currThread) { ++ park = prev; ++ } else { ++ // only one node we would want to acquire, and it's owned by this thread already ++ return ret; ++ } ++ } ++ ++ ++failures; ++ ++ if (failures > 128L && park.add(currThread)) { ++ LockSupport.park(); ++ } else { ++ // high contention, spin wait ++ if (failures < 128L) { ++ for (long i = 0; i < failures; ++i) { ++ Thread.onSpinWait(); ++ } ++ failures = failures << 1; ++ } else if (failures < 1_200L) { ++ LockSupport.parkNanos(1_000L); ++ failures = failures + 1L; ++ } else { // scale 0.1ms (100us) per failure ++ Thread.yield(); ++ LockSupport.parkNanos(100_000L * failures); ++ failures = failures + 1L; ++ } ++ } ++ } ++ } ++ ++ public Node lock(final int centerX, final int centerZ, final int radius) { ++ return this.lock(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius); ++ } ++ ++ public Node lock(final int fromX, final int fromZ, final int toX, final int toZ) { ++ if (fromX > toX || fromZ > toZ) { ++ throw new IllegalArgumentException(); ++ } ++ ++ final Thread currThread = Thread.currentThread(); ++ final int shift = this.coordinateShift; ++ final int fromSectionX = fromX >> shift; ++ final int fromSectionZ = fromZ >> shift; ++ final int toSectionX = toX >> shift; ++ final int toSectionZ = toZ >> shift; ++ ++ final LongArrayList areaAffected = new LongArrayList(); ++ ++ final Node ret = new Node(this, areaAffected, currThread); ++ ++ for (long failures = 0L;;) { ++ Node park = null; ++ boolean addedToArea = false; ++ ++ synchronized (this) { ++ // try to fast acquire area ++ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) { ++ for (int currX = fromSectionX; currX <= toSectionX; ++currX) { ++ final long coordinate = key(currX, currZ); ++ ++ final Node prev = this.nodes.putIfAbsent(coordinate, ret); ++ ++ if (prev == null) { ++ addedToArea = true; ++ areaAffected.add(coordinate); ++ continue; ++ } ++ ++ if (prev.thread != currThread) { ++ park = prev; ++ break; ++ } ++ } ++ } ++ ++ if (park == null) { ++ return ret; ++ } ++ ++ // failed, undo logic ++ if (!areaAffected.isEmpty()) { ++ for (int i = 0, len = areaAffected.size(); i < len; ++i) { ++ final long key = areaAffected.getLong(i); ++ ++ if (!this.nodes.remove(key, ret)) { ++ throw new IllegalStateException(); ++ } ++ } ++ } ++ } ++ ++ if (addedToArea) { ++ areaAffected.clear(); ++ // since we inserted, we need to drain waiters ++ Thread unpark; ++ while ((unpark = ret.pollOrBlockAdds()) != null) { ++ LockSupport.unpark(unpark); ++ } ++ } ++ ++ ++failures; ++ ++ if (failures > 128L && park.add(currThread)) { ++ LockSupport.park(); ++ } else { ++ // high contention, spin wait ++ if (failures < 128L) { ++ for (long i = 0; i < failures; ++i) { ++ Thread.onSpinWait(); ++ } ++ failures = failures << 1; ++ } else if (failures < 1_200L) { ++ LockSupport.parkNanos(1_000L); ++ failures = failures + 1L; ++ } else { // scale 0.1ms (100us) per failure ++ Thread.yield(); ++ LockSupport.parkNanos(100_000L * failures); ++ failures = failures + 1L; ++ } ++ } ++ ++ if (addedToArea) { ++ // try again, so we need to allow adds so that other threads can properly block on us ++ ret.allowAdds(); ++ } ++ } ++ } ++ ++ public void unlock(final Node node) { ++ if (node.lock != this) { ++ throw new IllegalStateException("Unlock target lock mismatch"); ++ } ++ ++ final LongArrayList areaAffected = node.areaAffected; ++ ++ if (areaAffected.isEmpty()) { ++ // here we are not in the node map, and so do not need to remove from the node map or unblock any waiters ++ return; ++ } ++ ++ // remove from node map; allowing other threads to lock ++ synchronized (this) { ++ for (int i = 0, len = areaAffected.size(); i < len; ++i) { ++ final long coordinate = areaAffected.getLong(i); ++ if (!this.nodes.remove(coordinate, node)) { ++ throw new IllegalStateException(); ++ } ++ } ++ } ++ ++ Thread unpark; ++ while ((unpark = node.pollOrBlockAdds()) != null) { ++ LockSupport.unpark(unpark); ++ } ++ } ++ ++ public static final class Node extends MultiThreadedQueue { ++ ++ private final SyncReentrantAreaLock lock; ++ private final LongArrayList areaAffected; ++ private final Thread thread; ++ ++ private Node(final SyncReentrantAreaLock lock, final LongArrayList areaAffected, final Thread thread) { ++ this.lock = lock; ++ this.areaAffected = areaAffected; ++ this.thread = thread; ++ } ++ } ++} +diff --git a/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java +index e0338db4d6fa359029ed5edeacc3646aa98701f5..c03dbb4a74d00d794be4139f0f7c4b5ff1b01d38 100644 +--- a/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java ++++ b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java +@@ -41,14 +41,14 @@ public final class StarLightInterface { + protected final ArrayDeque cachedSkyPropagators; + protected final ArrayDeque cachedBlockPropagators; + +- protected final LightQueue lightQueue = new LightQueue(this); ++ public final io.papermc.paper.chunk.system.light.LightQueue lightQueue; // Paper - replace light queue + + protected final LayerLightEventListener skyReader; + protected final LayerLightEventListener blockReader; + protected final boolean isClientSide; + +- protected final int minSection; +- protected final int maxSection; ++ public final int minSection; // Paper - public ++ public final int maxSection; // Paper - public + protected final int minLightSection; + protected final int maxLightSection; + +@@ -182,6 +182,7 @@ public final class StarLightInterface { + StarLightInterface.this.sectionChange(pos, notReady); + } + }; ++ this.lightQueue = new io.papermc.paper.chunk.system.light.LightQueue(this); // Paper - replace light queue + } + + public boolean hasSkyLight() { +@@ -333,7 +334,7 @@ public final class StarLightInterface { + return this.lightAccess; + } + +- protected final SkyStarLightEngine getSkyLightEngine() { ++ public final SkyStarLightEngine getSkyLightEngine() { // Paper - public + if (this.cachedSkyPropagators == null) { + return null; + } +@@ -348,7 +349,7 @@ public final class StarLightInterface { + return ret; + } + +- protected final void releaseSkyLightEngine(final SkyStarLightEngine engine) { ++ public final void releaseSkyLightEngine(final SkyStarLightEngine engine) { // Paper - public + if (this.cachedSkyPropagators == null) { + return; + } +@@ -357,7 +358,7 @@ public final class StarLightInterface { + } + } + +- protected final BlockStarLightEngine getBlockLightEngine() { ++ public final BlockStarLightEngine getBlockLightEngine() { // Paper - public + if (this.cachedBlockPropagators == null) { + return null; + } +@@ -372,7 +373,7 @@ public final class StarLightInterface { + return ret; + } + +- protected final void releaseBlockLightEngine(final BlockStarLightEngine engine) { ++ public final void releaseBlockLightEngine(final BlockStarLightEngine engine) { // Paper - public + if (this.cachedBlockPropagators == null) { + return; + } +@@ -381,7 +382,7 @@ public final class StarLightInterface { + } + } + +- public LightQueue.ChunkTasks blockChange(final BlockPos pos) { ++ public io.papermc.paper.chunk.system.light.LightQueue.ChunkTasks blockChange(final BlockPos pos) { // Paper - rewrite chunk system + if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world + return null; + } +@@ -389,7 +390,7 @@ public final class StarLightInterface { + return this.lightQueue.queueBlockChange(pos); + } + +- public LightQueue.ChunkTasks sectionChange(final SectionPos pos, final boolean newEmptyValue) { ++ public io.papermc.paper.chunk.system.light.LightQueue.ChunkTasks sectionChange(final SectionPos pos, final boolean newEmptyValue) { // Paper - rewrite chunk system + if (this.world == null) { // empty world + return null; + } +@@ -519,57 +520,15 @@ public final class StarLightInterface { + } + + public void scheduleChunkLight(final ChunkPos pos, final Runnable run) { +- this.lightQueue.queueChunkLighting(pos, run); ++ throw new UnsupportedOperationException("No longer implemented, use the new lightQueue field to queue tasks"); // Paper - replace light queue + } + + public void removeChunkTasks(final ChunkPos pos) { +- this.lightQueue.removeChunk(pos); ++ throw new UnsupportedOperationException("No longer implemented, use the new lightQueue field to queue tasks"); // Paper - replace light queue + } + + public void propagateChanges() { +- if (this.lightQueue.isEmpty()) { +- return; +- } +- +- final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); +- final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); +- +- try { +- LightQueue.ChunkTasks task; +- while ((task = this.lightQueue.removeFirstTask()) != null) { +- if (task.lightTasks != null) { +- for (final Runnable run : task.lightTasks) { +- run.run(); +- } +- } +- +- final long coordinate = task.chunkCoordinate; +- final int chunkX = CoordinateUtils.getChunkX(coordinate); +- final int chunkZ = CoordinateUtils.getChunkZ(coordinate); +- +- final Set positions = task.changedPositions; +- final Boolean[] sectionChanges = task.changedSectionSet; +- +- if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) { +- skyEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); +- } +- if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) { +- blockEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); +- } +- +- if (skyEngine != null && task.queuedEdgeChecksSky != null) { +- skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksSky); +- } +- if (blockEngine != null && task.queuedEdgeChecksBlock != null) { +- blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksBlock); +- } +- +- task.onComplete.complete(null); +- } +- } finally { +- this.releaseSkyLightEngine(skyEngine); +- this.releaseBlockLightEngine(blockEngine); +- } ++ throw new UnsupportedOperationException("No longer implemented, task draining is now performed by the light thread"); // Paper - replace light queue + } + + public static final class LightQueue { +diff --git a/src/main/java/co/aikar/timings/WorldTimingsHandler.java b/src/main/java/co/aikar/timings/WorldTimingsHandler.java +index 2f0d9b953802dee821cfde82d22b0567cce8ee91..22687667ec69a954261e55e59261286ac1b8b8cd 100644 +--- a/src/main/java/co/aikar/timings/WorldTimingsHandler.java ++++ b/src/main/java/co/aikar/timings/WorldTimingsHandler.java +@@ -59,6 +59,16 @@ public class WorldTimingsHandler { + + public final Timing miscMobSpawning; + ++ public final Timing poiUnload; ++ public final Timing chunkUnload; ++ public final Timing poiSaveDataSerialization; ++ public final Timing chunkSave; ++ public final Timing chunkSaveDataSerialization; ++ public final Timing chunkSaveIOWait; ++ public final Timing chunkUnloadPrepareSave; ++ public final Timing chunkUnloadPOISerialization; ++ public final Timing chunkUnloadDataSave; ++ + public WorldTimingsHandler(Level server) { + String name = ((PrimaryLevelData) server.getLevelData()).getLevelName() + " - "; + +@@ -112,6 +122,16 @@ public class WorldTimingsHandler { + + + miscMobSpawning = Timings.ofSafe(name + "Mob spawning - Misc"); ++ ++ poiUnload = Timings.ofSafe(name + "Chunk unload - POI"); ++ chunkUnload = Timings.ofSafe(name + "Chunk unload - Chunk"); ++ poiSaveDataSerialization = Timings.ofSafe(name + "Chunk save - POI Data serialization"); ++ chunkSave = Timings.ofSafe(name + "Chunk save - Chunk"); ++ chunkSaveDataSerialization = Timings.ofSafe(name + "Chunk save - Chunk Data serialization"); ++ chunkSaveIOWait = Timings.ofSafe(name + "Chunk save - Chunk IO Wait"); ++ chunkUnloadPrepareSave = Timings.ofSafe(name + "Chunk unload - Async Save Prepare"); ++ chunkUnloadPOISerialization = Timings.ofSafe(name + "Chunk unload - POI Data Serialization"); ++ chunkUnloadDataSave = Timings.ofSafe(name + "Chunk unload - Data Serialization"); + } + + public static Timing getTickList(ServerLevel worldserver, String timingsType) { +diff --git a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java +index cff2f04409fab9abca87ceec85a551e1d59f9e7d..e3f56908cc8a9c3f4580def50fcfdc61bd495a71 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java ++++ b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java +@@ -32,192 +32,41 @@ public final class ChunkSystem { + } + + public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) { +- level.chunkSource.mainThreadProcessor.execute(run); ++ level.chunkTaskScheduler.scheduleChunkTask(chunkX, chunkZ, run, priority); // Paper - rewrite chunk system + } + + public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen, + final ChunkStatus toStatus, final boolean addTicket, final PrioritisedExecutor.Priority priority, + final Consumer onComplete) { +- if (gen) { +- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); +- return; +- } +- scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> { +- if (chunk == null) { +- onComplete.accept(null); +- } else { +- if (chunk.getStatus().isOrAfter(toStatus)) { +- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); +- } else { +- onComplete.accept(null); +- } +- } +- }); ++ level.chunkTaskScheduler.scheduleChunkLoad(chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete); // Paper - rewrite chunk system + } + +- static final TicketType CHUNK_LOAD = TicketType.create("chunk_load", Long::compareTo); +- +- private static long chunkLoadCounter = 0L; ++ // Paper - rewrite chunk system + public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus, + final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer onComplete) { +- if (!Bukkit.isPrimaryThread()) { +- scheduleChunkTask(level, chunkX, chunkZ, () -> { +- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); +- }, priority); +- return; +- } +- +- final int minLevel = 33 + ChunkStatus.getDistance(toStatus); +- final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null; +- final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); +- +- if (addTicket) { +- level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); +- } +- level.chunkSource.runDistanceManagerUpdates(); +- +- final Consumer loadCallback = (final ChunkAccess chunk) -> { +- try { +- if (onComplete != null) { +- onComplete.accept(chunk); +- } +- } catch (final ThreadDeath death) { +- throw death; +- } catch (final Throwable thr) { +- LOGGER.error("Exception handling chunk load callback", thr); +- SneakyThrow.sneaky(thr); +- } finally { +- if (addTicket) { +- level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); +- level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); +- } +- } +- }; +- +- final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); +- +- if (holder == null || holder.getTicketLevel() > minLevel) { +- loadCallback.accept(null); +- return; +- } +- +- final CompletableFuture> loadFuture = holder.getOrScheduleFuture(toStatus, level.chunkSource.chunkMap); +- +- if (loadFuture.isDone()) { +- loadCallback.accept(loadFuture.join().left().orElse(null)); +- return; +- } +- +- loadFuture.whenCompleteAsync((final Either either, final Throwable thr) -> { +- if (thr != null) { +- loadCallback.accept(null); +- return; +- } +- loadCallback.accept(either.left().orElse(null)); +- }, (final Runnable r) -> { +- scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST); +- }); ++ level.chunkTaskScheduler.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); // Paper - rewrite chunk system + } + + public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ, + final FullChunkStatus toStatus, final boolean addTicket, + final PrioritisedExecutor.Priority priority, final Consumer onComplete) { +- // This method goes unused until the chunk system rewrite +- if (toStatus == FullChunkStatus.INACCESSIBLE) { +- throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status"); +- } +- +- if (!Bukkit.isPrimaryThread()) { +- scheduleChunkTask(level, chunkX, chunkZ, () -> { +- scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); +- }, priority); +- return; +- } +- +- final int minLevel = 33 - (toStatus.ordinal() - 1); +- final int radius = toStatus.ordinal() - 1; +- final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null; +- final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); +- +- if (addTicket) { +- level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); +- } +- level.chunkSource.runDistanceManagerUpdates(); +- +- final Consumer loadCallback = (final LevelChunk chunk) -> { +- try { +- if (onComplete != null) { +- onComplete.accept(chunk); +- } +- } catch (final ThreadDeath death) { +- throw death; +- } catch (final Throwable thr) { +- LOGGER.error("Exception handling chunk load callback", thr); +- SneakyThrow.sneaky(thr); +- } finally { +- if (addTicket) { +- level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); +- level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); +- } +- } +- }; +- +- final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); +- +- if (holder == null || holder.getTicketLevel() > minLevel) { +- loadCallback.accept(null); +- return; +- } +- +- final CompletableFuture> tickingState; +- switch (toStatus) { +- case FULL: { +- tickingState = holder.getFullChunkFuture(); +- break; +- } +- case BLOCK_TICKING: { +- tickingState = holder.getTickingChunkFuture(); +- break; +- } +- case ENTITY_TICKING: { +- tickingState = holder.getEntityTickingChunkFuture(); +- break; +- } +- default: { +- throw new IllegalStateException("Cannot reach here"); +- } +- } +- +- if (tickingState.isDone()) { +- loadCallback.accept(tickingState.join().left().orElse(null)); +- return; +- } +- +- tickingState.whenCompleteAsync((final Either either, final Throwable thr) -> { +- if (thr != null) { +- loadCallback.accept(null); +- return; +- } +- loadCallback.accept(either.left().orElse(null)); +- }, (final Runnable r) -> { +- scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST); +- }); ++ level.chunkTaskScheduler.scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); // Paper - rewrite chunk system + } + + public static List getVisibleChunkHolders(final ServerLevel level) { +- return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values()); ++ return level.chunkTaskScheduler.chunkHolderManager.getOldChunkHolders(); // Paper - rewrite chunk system + } + + public static List getUpdatingChunkHolders(final ServerLevel level) { +- return new ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values()); ++ return level.chunkTaskScheduler.chunkHolderManager.getOldChunkHolders(); // Paper - rewrite chunk system + } + + public static int getVisibleChunkHolderCount(final ServerLevel level) { +- return level.chunkSource.chunkMap.visibleChunkMap.size(); ++ return level.chunkTaskScheduler.chunkHolderManager.size(); // Paper - rewrite chunk system + } + + public static int getUpdatingChunkHolderCount(final ServerLevel level) { +- return level.chunkSource.chunkMap.updatingChunkMap.size(); ++ return level.chunkTaskScheduler.chunkHolderManager.size(); // Paper - rewrite chunk system + } + + public static boolean hasAnyChunkHolders(final ServerLevel level) { +@@ -244,26 +93,32 @@ public final class ChunkSystem { + + public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { + chunk.playerChunk = holder; ++ chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.FULL; + } + + public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) { +- ++ chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.INACCESSIBLE; + } + + public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) { + chunk.level.getChunkSource().tickingChunks.add(chunk); ++ chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.BLOCK_TICKING; ++ chunk.level.chunkSource.chunkMap.tickingGenerated.incrementAndGet(); + } + + public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { + chunk.level.getChunkSource().tickingChunks.remove(chunk); ++ chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.FULL; + } + + public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { + chunk.level.getChunkSource().entityTickingChunks.add(chunk); ++ chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.ENTITY_TICKING; + } + + public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { + chunk.level.getChunkSource().entityTickingChunks.remove(chunk); ++ chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.BLOCK_TICKING; + } + + public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) { +@@ -271,23 +126,15 @@ public final class ChunkSystem { + } + + public static int getSendViewDistance(final ServerPlayer player) { +- return getLoadViewDistance(player); ++ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPISendViewDistance(player); + } + + public static int getLoadViewDistance(final ServerPlayer player) { +- final ServerLevel level = player.serverLevel(); +- if (level == null) { +- return Bukkit.getViewDistance(); +- } +- return level.chunkSource.chunkMap.getPlayerViewDistance(player); ++ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getLoadViewDistance(player); + } + + public static int getTickViewDistance(final ServerPlayer player) { +- final ServerLevel level = player.serverLevel(); +- if (level == null) { +- return Bukkit.getSimulationDistance(); +- } +- return level.chunkSource.chunkMap.distanceManager.simulationDistance; ++ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPITickViewDistance(player); + } + + private ChunkSystem() { +diff --git a/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ee58c67cb2bd78159cce19ec75f13dc6168a0e7a +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java +@@ -0,0 +1,1375 @@ ++package io.papermc.paper.chunk.system; ++ ++import ca.spottedleaf.concurrentutil.collection.SRSWLinkedQueue; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager; ++import io.papermc.paper.configuration.GlobalConfiguration; ++import io.papermc.paper.util.CoordinateUtils; ++import io.papermc.paper.util.TickThread; ++import io.papermc.paper.util.player.SingleUserAreaMap; ++import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; ++import it.unimi.dsi.fastutil.longs.LongArrayList; ++import it.unimi.dsi.fastutil.longs.LongComparator; ++import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue; ++import it.unimi.dsi.fastutil.longs.LongIterator; ++import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; ++import it.unimi.dsi.fastutil.longs.LongOpenHashSet; ++import net.minecraft.network.protocol.Packet; ++import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket; ++import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket; ++import net.minecraft.network.protocol.game.ClientboundSetSimulationDistancePacket; ++import net.minecraft.server.level.ChunkTrackingView; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.server.network.PlayerChunkSender; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.GameRules; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.levelgen.BelowZeroRetrogen; ++import org.bukkit.craftbukkit.entity.CraftPlayer; ++import org.bukkit.entity.Player; ++import java.lang.invoke.VarHandle; ++import java.util.ArrayDeque; ++import java.util.Arrays; ++import java.util.Objects; ++import java.util.concurrent.TimeUnit; ++import java.util.concurrent.atomic.AtomicLong; ++ ++public class RegionizedPlayerChunkLoader { ++ ++ // expected that this list returns for a given radius, the set of chunks ordered ++ // by manhattan distance ++ private static final long[][] SEARCH_RADIUS_ITERATION_LIST = new long[64+2+1][]; ++ static { ++ for (int i = 0; i < SEARCH_RADIUS_ITERATION_LIST.length; ++i) { ++ // a BFS around -x, -z, +x, +z will give increasing manhatten distance ++ SEARCH_RADIUS_ITERATION_LIST[i] = generateBFSOrder(i); ++ } ++ } ++ ++ private static void expandQuadrants(final CustomLongArray input, final int size) { ++ final int len = input.size(); ++ final long[] array = input.elements(); ++ ++ int writeIndex = size - 1; ++ for (int i = len - 1; i >= 0; --i) { ++ final long key = array[i]; ++ final int chunkX = CoordinateUtils.getChunkX(key); ++ final int chunkZ = CoordinateUtils.getChunkZ(key); ++ ++ if ((chunkX | chunkZ) < 0 || (i != 0 && chunkX == 0 && chunkZ == 0)) { ++ throw new IllegalStateException(); ++ } ++ ++ // Q4 ++ if (chunkZ != 0) { ++ array[writeIndex--] = CoordinateUtils.getChunkKey(chunkX, -chunkZ); ++ } ++ // Q3 ++ if (chunkX != 0 && chunkZ != 0) { ++ array[writeIndex--] = CoordinateUtils.getChunkKey(-chunkX, -chunkZ); ++ } ++ // Q2 ++ if (chunkX != 0) { ++ array[writeIndex--] = CoordinateUtils.getChunkKey(-chunkX, chunkZ); ++ } ++ ++ array[writeIndex--] = key; ++ } ++ ++ input.forceSize(size); ++ ++ if (writeIndex != -1) { ++ throw new IllegalStateException(); ++ } ++ } ++ ++ private static long[] generateBFSOrder(final int radius) { ++ // by using only the first quadrant, we can reduce the total element size by 4 when spreading ++ final CustomLongArray[] byDistance = makeQ1BFS(radius); ++ ++ // to increase generation parallelism, we want to space the chunks out so that they are not nearby when generating ++ // this also means we are minimising locality ++ // but, we need to maintain sorted order by manhatten distance ++ ++ // per manhatten distance we transform the chunk list so that each element is maximally spaced out from each other ++ for (int i = 0, len = byDistance.length; i < len; ++i) { ++ final CustomLongArray points = byDistance[i]; ++ final int expectedSize = getDistanceSize(i, radius); ++ ++ final CustomLongArray spread = spread(points, expectedSize); ++ // add in Q2, Q3, Q4 ++ expandQuadrants(spread, expectedSize); ++ ++ byDistance[i] = spread; ++ } ++ ++ // now, rebuild the list so that it still maintains manhatten distance order ++ final CustomLongArray ret = new CustomLongArray((2 * radius + 1) * (2 * radius + 1)); ++ ++ for (final CustomLongArray dist : byDistance) { ++ ret.addAll(dist); ++ } ++ ++ return ret.elements(); ++ } ++ ++ public static final TicketType REGION_PLAYER_TICKET = TicketType.create("region_player_ticket", Long::compareTo); ++ ++ public static final int MIN_VIEW_DISTANCE = 2; ++ public static final int MAX_VIEW_DISTANCE = 32; ++ ++ public static final int TICK_TICKET_LEVEL = 31; ++ public static final int GENERATED_TICKET_LEVEL = 33 + ChunkStatus.getDistance(ChunkStatus.FULL); ++ public static final int LOADED_TICKET_LEVEL = 33 + ChunkStatus.getDistance(ChunkStatus.EMPTY); ++ ++ public static final record ViewDistances( ++ int tickViewDistance, ++ int loadViewDistance, ++ int sendViewDistance ++ ) { ++ public ViewDistances setTickViewDistance(final int distance) { ++ return new ViewDistances(distance, this.loadViewDistance, this.sendViewDistance); ++ } ++ ++ public ViewDistances setLoadViewDistance(final int distance) { ++ return new ViewDistances(this.tickViewDistance, distance, this.sendViewDistance); ++ } ++ ++ ++ public ViewDistances setSendViewDistance(final int distance) { ++ return new ViewDistances(this.tickViewDistance, this.loadViewDistance, distance); ++ } ++ } ++ ++ public static int getAPITickViewDistance(final Player player) { ++ return getAPITickViewDistance(((CraftPlayer)player).getHandle()); ++ } ++ ++ public static int getAPITickViewDistance(final ServerPlayer player) { ++ final ServerLevel level = (ServerLevel)player.level(); ++ final PlayerChunkLoaderData data = player.chunkLoader; ++ if (data == null) { ++ return level.playerChunkLoader.getAPITickDistance(); ++ } ++ return data.lastTickDistance; ++ } ++ ++ public static int getAPIViewDistance(final Player player) { ++ return getAPIViewDistance(((CraftPlayer)player).getHandle()); ++ } ++ ++ public static int getAPIViewDistance(final ServerPlayer player) { ++ final ServerLevel level = (ServerLevel)player.level(); ++ final PlayerChunkLoaderData data = player.chunkLoader; ++ if (data == null) { ++ return level.playerChunkLoader.getAPIViewDistance(); ++ } ++ // view distance = load distance + 1 ++ return data.lastLoadDistance - 1; ++ } ++ ++ public static int getLoadViewDistance(final ServerPlayer player) { ++ final ServerLevel level = (ServerLevel)player.level(); ++ final PlayerChunkLoaderData data = player.chunkLoader; ++ if (data == null) { ++ return level.playerChunkLoader.getAPIViewDistance(); ++ } ++ // view distance = load distance + 1 ++ return data.lastLoadDistance - 1; ++ } ++ ++ public static int getAPISendViewDistance(final Player player) { ++ return getAPISendViewDistance(((CraftPlayer)player).getHandle()); ++ } ++ ++ public static int getAPISendViewDistance(final ServerPlayer player) { ++ final ServerLevel level = (ServerLevel)player.level(); ++ final PlayerChunkLoaderData data = player.chunkLoader; ++ if (data == null) { ++ return level.playerChunkLoader.getAPISendViewDistance(); ++ } ++ return data.lastSendDistance; ++ } ++ ++ private final ServerLevel world; ++ ++ public RegionizedPlayerChunkLoader(final ServerLevel world) { ++ this.world = world; ++ } ++ ++ public void addPlayer(final ServerPlayer player) { ++ TickThread.ensureTickThread(player, "Cannot add player to player chunk loader async"); ++ if (!player.isRealPlayer) { ++ return; ++ } ++ ++ if (player.chunkLoader != null) { ++ throw new IllegalStateException("Player is already added to player chunk loader"); ++ } ++ ++ final PlayerChunkLoaderData loader = new PlayerChunkLoaderData(this.world, player); ++ ++ player.chunkLoader = loader; ++ loader.add(); ++ } ++ ++ public void updatePlayer(final ServerPlayer player) { ++ final PlayerChunkLoaderData loader = player.chunkLoader; ++ if (loader != null) { ++ loader.update(); ++ } ++ } ++ ++ public void removePlayer(final ServerPlayer player) { ++ TickThread.ensureTickThread(player, "Cannot remove player from player chunk loader async"); ++ if (!player.isRealPlayer) { ++ return; ++ } ++ ++ final PlayerChunkLoaderData loader = player.chunkLoader; ++ ++ if (loader == null) { ++ return; ++ } ++ ++ loader.remove(); ++ player.chunkLoader = null; ++ } ++ ++ public void setSendDistance(final int distance) { ++ this.world.setSendViewDistance(distance); ++ } ++ ++ public void setLoadDistance(final int distance) { ++ this.world.setLoadViewDistance(distance); ++ } ++ ++ public void setTickDistance(final int distance) { ++ this.world.setTickViewDistance(distance); ++ } ++ ++ // Note: follow the player chunk loader so everything stays consistent... ++ public int getAPITickDistance() { ++ final ViewDistances distances = this.world.getViewDistances(); ++ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(-1, distances.tickViewDistance); ++ return tickViewDistance; ++ } ++ ++ public int getAPIViewDistance() { ++ final ViewDistances distances = this.world.getViewDistances(); ++ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(-1, distances.tickViewDistance); ++ final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance); ++ ++ // loadDistance = api view distance + 1 ++ return loadDistance - 1; ++ } ++ ++ public int getAPISendViewDistance() { ++ final ViewDistances distances = this.world.getViewDistances(); ++ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(-1, distances.tickViewDistance); ++ final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance); ++ final int sendViewDistance = PlayerChunkLoaderData.getSendViewDistance( ++ loadDistance, -1, -1, distances.sendViewDistance ++ ); ++ ++ return sendViewDistance; ++ } ++ ++ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ, final boolean borderOnly) { ++ return borderOnly ? this.isChunkSentBorderOnly(player, chunkX, chunkZ) : this.isChunkSent(player, chunkX, chunkZ); ++ } ++ ++ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ) { ++ final PlayerChunkLoaderData loader = player.chunkLoader; ++ if (loader == null) { ++ return false; ++ } ++ ++ return loader.sentChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ ++ public boolean isChunkSentBorderOnly(final ServerPlayer player, final int chunkX, final int chunkZ) { ++ final PlayerChunkLoaderData loader = player.chunkLoader; ++ if (loader == null) { ++ return false; ++ } ++ ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ if (!loader.sentChunks.contains(CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ))) { ++ return true; ++ } ++ } ++ } ++ ++ return false; ++ } ++ ++ public void tick() { ++ TickThread.ensureTickThread("Cannot tick player chunk loader async"); ++ long currTime = System.nanoTime(); ++ for (final ServerPlayer player : new java.util.ArrayList<>(this.world.players())) { ++ final PlayerChunkLoaderData loader = player.chunkLoader; ++ if (loader == null || loader.world != this.world) { ++ // not our problem anymore ++ continue; ++ } ++ loader.update(); // can't invoke plugin logic ++ loader.updateQueues(currTime); ++ } ++ } ++ ++ public static final class PlayerChunkLoaderData { ++ ++ private static final AtomicLong ID_GENERATOR = new AtomicLong(); ++ private final long id = ID_GENERATOR.incrementAndGet(); ++ private final Long idBoxed = Long.valueOf(this.id); ++ ++ private static final long MAX_RATE = 10_000L; ++ ++ private final ServerPlayer player; ++ private final ServerLevel world; ++ ++ private int lastChunkX = Integer.MIN_VALUE; ++ private int lastChunkZ = Integer.MIN_VALUE; ++ ++ private int lastSendDistance = Integer.MIN_VALUE; ++ private int lastLoadDistance = Integer.MIN_VALUE; ++ private int lastTickDistance = Integer.MIN_VALUE; ++ ++ private int lastSentChunkCenterX = Integer.MIN_VALUE; ++ private int lastSentChunkCenterZ = Integer.MIN_VALUE; ++ ++ private int lastSentChunkRadius = Integer.MIN_VALUE; ++ private int lastSentSimulationDistance = Integer.MIN_VALUE; ++ ++ private boolean canGenerateChunks = true; ++ ++ private final ArrayDeque> delayedTicketOps = new ArrayDeque<>(); ++ private final LongOpenHashSet sentChunks = new LongOpenHashSet(); ++ ++ private static final byte CHUNK_TICKET_STAGE_NONE = 0; ++ private static final byte CHUNK_TICKET_STAGE_LOADING = 1; ++ private static final byte CHUNK_TICKET_STAGE_LOADED = 2; ++ private static final byte CHUNK_TICKET_STAGE_GENERATING = 3; ++ private static final byte CHUNK_TICKET_STAGE_GENERATED = 4; ++ private static final byte CHUNK_TICKET_STAGE_TICK = 5; ++ private static final int[] TICKET_STAGE_TO_LEVEL = new int[] { ++ ChunkHolderManager.MAX_TICKET_LEVEL + 1, ++ LOADED_TICKET_LEVEL, ++ LOADED_TICKET_LEVEL, ++ GENERATED_TICKET_LEVEL, ++ GENERATED_TICKET_LEVEL, ++ TICK_TICKET_LEVEL ++ }; ++ private final Long2ByteOpenHashMap chunkTicketStage = new Long2ByteOpenHashMap(); ++ { ++ this.chunkTicketStage.defaultReturnValue(CHUNK_TICKET_STAGE_NONE); ++ } ++ ++ // rate limiting ++ private final AllocatingRateLimiter chunkSendLimiter = new AllocatingRateLimiter(); ++ private final AllocatingRateLimiter chunkLoadTicketLimiter = new AllocatingRateLimiter(); ++ private final AllocatingRateLimiter chunkGenerateTicketLimiter = new AllocatingRateLimiter(); ++ ++ // queues ++ private final LongComparator CLOSEST_MANHATTAN_DIST = (final long c1, final long c2) -> { ++ final int c1x = CoordinateUtils.getChunkX(c1); ++ final int c1z = CoordinateUtils.getChunkZ(c1); ++ ++ final int c2x = CoordinateUtils.getChunkX(c2); ++ final int c2z = CoordinateUtils.getChunkZ(c2); ++ ++ final int centerX = PlayerChunkLoaderData.this.lastChunkX; ++ final int centerZ = PlayerChunkLoaderData.this.lastChunkZ; ++ ++ return Integer.compare( ++ Math.abs(c1x - centerX) + Math.abs(c1z - centerZ), ++ Math.abs(c2x - centerX) + Math.abs(c2z - centerZ) ++ ); ++ }; ++ private final LongHeapPriorityQueue sendQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); ++ private final LongHeapPriorityQueue tickingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); ++ private final LongHeapPriorityQueue generatingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); ++ private final LongHeapPriorityQueue genQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); ++ private final LongHeapPriorityQueue loadingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); ++ private final LongHeapPriorityQueue loadQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); ++ ++ private volatile boolean removed; ++ ++ public PlayerChunkLoaderData(final ServerLevel world, final ServerPlayer player) { ++ this.world = world; ++ this.player = player; ++ } ++ ++ private void flushDelayedTicketOps() { ++ if (this.delayedTicketOps.isEmpty()) { ++ return; ++ } ++ this.world.chunkTaskScheduler.chunkHolderManager.performTicketUpdates(this.delayedTicketOps); ++ this.delayedTicketOps.clear(); ++ } ++ ++ private void pushDelayedTicketOp(final ChunkHolderManager.TicketOperation op) { ++ this.delayedTicketOps.addLast(op); ++ } ++ ++ private void sendChunk(final int chunkX, final int chunkZ) { ++ if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { ++ PlayerChunkSender.sendChunk(this.player.connection, this.world, this.world.getChunkIfLoaded(chunkX, chunkZ)); ++ return; ++ } ++ throw new IllegalStateException(); ++ } ++ ++ private void sendUnloadChunk(final int chunkX, final int chunkZ) { ++ if (!this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { ++ return; ++ } ++ this.sendUnloadChunkRaw(chunkX, chunkZ); ++ } ++ ++ private void sendUnloadChunkRaw(final int chunkX, final int chunkZ) { ++ PlayerChunkSender.dropChunkStatic(this.player, new ChunkPos(chunkX, chunkZ)); ++ } ++ ++ private final SingleUserAreaMap broadcastMap = new SingleUserAreaMap<>(this) { ++ @Override ++ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { ++ // do nothing, we only care about remove ++ } ++ ++ @Override ++ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { ++ parameter.sendUnloadChunk(chunkX, chunkZ); ++ } ++ }; ++ private final SingleUserAreaMap loadTicketCleanup = new SingleUserAreaMap<>(this) { ++ @Override ++ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { ++ // do nothing, we only care about remove ++ } ++ ++ @Override ++ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { ++ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ final byte ticketStage = parameter.chunkTicketStage.remove(chunk); ++ final int level = TICKET_STAGE_TO_LEVEL[ticketStage]; ++ if (level > ChunkHolderManager.MAX_TICKET_LEVEL) { ++ return; ++ } ++ ++ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove( ++ chunk, ++ TicketType.UNKNOWN, level, new ChunkPos(chunkX, chunkZ), ++ REGION_PLAYER_TICKET, level, parameter.idBoxed ++ )); ++ } ++ }; ++ private final SingleUserAreaMap tickMap = new SingleUserAreaMap<>(this) { ++ @Override ++ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { ++ // do nothing, we will detect ticking chunks when we try to load them ++ } ++ ++ @Override ++ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { ++ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ // note: by the time this is called, the tick cleanup should have ran - so, if the chunk is at ++ // the tick stage it was deemed in range for loading. Thus, we need to move it to generated ++ if (!parameter.chunkTicketStage.replace(chunk, CHUNK_TICKET_STAGE_TICK, CHUNK_TICKET_STAGE_GENERATED)) { ++ return; ++ } ++ ++ // Since we are possibly downgrading the ticket level, we add an unknown ticket so that ++ // the level is kept until tick(). ++ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove( ++ chunk, ++ TicketType.UNKNOWN, TICK_TICKET_LEVEL, new ChunkPos(chunkX, chunkZ), ++ REGION_PLAYER_TICKET, TICK_TICKET_LEVEL, parameter.idBoxed ++ )); ++ // keep chunk at new generated level ++ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addOp( ++ chunk, ++ REGION_PLAYER_TICKET, GENERATED_TICKET_LEVEL, parameter.idBoxed ++ )); ++ } ++ }; ++ ++ private static boolean wantChunkLoaded(final int centerX, final int centerZ, final int chunkX, final int chunkZ, ++ final int sendRadius) { ++ // expect sendRadius to be = 1 + target viewable radius ++ return ChunkTrackingView.isWithinDistance(centerX, centerZ, sendRadius, chunkX, chunkZ, true); ++ } ++ ++ private static int getClientViewDistance(final ServerPlayer player) { ++ final Integer vd = player.requestedViewDistance(); ++ return vd == null ? -1 : Math.max(0, vd.intValue()); ++ } ++ ++ private static int getTickDistance(final int playerTickViewDistance, final int worldTickViewDistance) { ++ return playerTickViewDistance < 0 ? worldTickViewDistance : playerTickViewDistance; ++ } ++ ++ private static int getLoadViewDistance(final int tickViewDistance, final int playerLoadViewDistance, ++ final int worldLoadViewDistance) { ++ return Math.max(tickViewDistance + 1, playerLoadViewDistance < 0 ? worldLoadViewDistance : playerLoadViewDistance); ++ } ++ ++ private static int getSendViewDistance(final int loadViewDistance, final int clientViewDistance, ++ final int playerSendViewDistance, final int worldSendViewDistance) { ++ return Math.min( ++ loadViewDistance - 1, ++ playerSendViewDistance < 0 ? (!GlobalConfiguration.get().chunkLoadingAdvanced.autoConfigSendDistance || clientViewDistance < 0 ? (worldSendViewDistance < 0 ? (loadViewDistance - 1) : worldSendViewDistance) : clientViewDistance + 1) : playerSendViewDistance ++ ); ++ } ++ ++ private Packet updateClientChunkRadius(final int radius) { ++ this.lastSentChunkRadius = radius; ++ return new ClientboundSetChunkCacheRadiusPacket(radius); ++ } ++ ++ private Packet updateClientSimulationDistance(final int distance) { ++ this.lastSentSimulationDistance = distance; ++ return new ClientboundSetSimulationDistancePacket(distance); ++ } ++ ++ private Packet updateClientChunkCenter(final int chunkX, final int chunkZ) { ++ this.lastSentChunkCenterX = chunkX; ++ this.lastSentChunkCenterZ = chunkZ; ++ return new ClientboundSetChunkCacheCenterPacket(chunkX, chunkZ); ++ } ++ ++ private boolean canPlayerGenerateChunks() { ++ return !this.player.isSpectator() || this.world.getGameRules().getBoolean(GameRules.RULE_SPECTATORSGENERATECHUNKS); ++ } ++ ++ private double getMaxChunkLoadRate() { ++ final double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkLoadRate; ++ ++ return configRate < 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); ++ } ++ ++ private double getMaxChunkGenRate() { ++ final double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkGenerateRate; ++ ++ return configRate < 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); ++ } ++ ++ private double getMaxChunkSendRate() { ++ final double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkSendRate; ++ ++ return configRate < 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); ++ } ++ ++ private long getMaxChunkLoads() { ++ final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L); ++ long configLimit = GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkLoads; ++ if (configLimit == 0L) { ++ // by default, only allow 1/5th of the chunks in the view distance to be concurrently active ++ configLimit = Math.max(5L, radiusChunks / 5L); ++ } else if (configLimit < 0L) { ++ configLimit = Integer.MAX_VALUE; ++ } // else: use the value configured ++ configLimit = configLimit - this.loadingQueue.size(); ++ ++ return configLimit; ++ } ++ ++ private long getMaxChunkGenerates() { ++ final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L); ++ long configLimit = GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkGenerates; ++ if (configLimit == 0L) { ++ // by default, only allow 1/5th of the chunks in the view distance to be concurrently active ++ configLimit = Math.max(5L, radiusChunks / 5L); ++ } else if (configLimit < 0L) { ++ configLimit = Integer.MAX_VALUE; ++ } // else: use the value configured ++ configLimit = configLimit - this.generatingQueue.size(); ++ ++ return configLimit; ++ } ++ ++ private boolean wantChunkSent(final int chunkX, final int chunkZ) { ++ final int dx = this.lastChunkX - chunkX; ++ final int dz = this.lastChunkZ - chunkZ; ++ return (Math.max(Math.abs(dx), Math.abs(dz)) <= (this.lastSendDistance + 1)) && wantChunkLoaded( ++ this.lastChunkX, this.lastChunkZ, chunkX, chunkZ, this.lastSendDistance ++ ); ++ } ++ ++ private boolean wantChunkTicked(final int chunkX, final int chunkZ) { ++ final int dx = this.lastChunkX - chunkX; ++ final int dz = this.lastChunkZ - chunkZ; ++ return Math.max(Math.abs(dx), Math.abs(dz)) <= this.lastTickDistance; ++ } ++ ++ void updateQueues(final long time) { ++ TickThread.ensureTickThread(this.player, "Cannot tick player chunk loader async"); ++ if (this.removed) { ++ throw new IllegalStateException("Ticking removed player chunk loader"); ++ } ++ // update rate limits ++ final double loadRate = this.getMaxChunkLoadRate(); ++ final double genRate = this.getMaxChunkGenRate(); ++ final double sendRate = this.getMaxChunkSendRate(); ++ ++ this.chunkLoadTicketLimiter.tickAllocation(time, loadRate, loadRate); ++ this.chunkGenerateTicketLimiter.tickAllocation(time, genRate, genRate); ++ this.chunkSendLimiter.tickAllocation(time, sendRate, sendRate); ++ ++ // try to progress chunk loads ++ while (!this.loadingQueue.isEmpty()) { ++ final long pendingLoadChunk = this.loadingQueue.firstLong(); ++ final int pendingChunkX = CoordinateUtils.getChunkX(pendingLoadChunk); ++ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingLoadChunk); ++ final ChunkAccess pending = this.world.chunkSource.getChunkAtImmediately(pendingChunkX, pendingChunkZ); ++ if (pending == null) { ++ // nothing to do here ++ break; ++ } ++ // chunk has loaded, so we can take it out of the queue ++ this.loadingQueue.dequeueLong(); ++ ++ // try to move to generate queue ++ final byte prev = this.chunkTicketStage.put(pendingLoadChunk, CHUNK_TICKET_STAGE_LOADED); ++ if (prev != CHUNK_TICKET_STAGE_LOADING) { ++ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADING + ", not " + prev); ++ } ++ ++ if (this.canGenerateChunks || this.isLoadedChunkGeneratable(pending)) { ++ this.genQueue.enqueue(pendingLoadChunk); ++ } // else: don't want to generate, so just leave it loaded ++ } ++ ++ // try to push more chunk loads ++ final long maxLoads = Math.max(0L, Math.min(MAX_RATE, Math.min(this.loadQueue.size(), this.getMaxChunkLoads()))); ++ final int maxLoadsThisTick = (int)this.chunkLoadTicketLimiter.takeAllocation(time, loadRate, maxLoads); ++ if (maxLoadsThisTick > 0) { ++ final LongArrayList chunks = new LongArrayList(maxLoadsThisTick); ++ for (int i = 0; i < maxLoadsThisTick; ++i) { ++ final long chunk = this.loadQueue.dequeueLong(); ++ final byte prev = this.chunkTicketStage.put(chunk, CHUNK_TICKET_STAGE_LOADING); ++ if (prev != CHUNK_TICKET_STAGE_NONE) { ++ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_NONE + ", not " + prev); ++ } ++ this.pushDelayedTicketOp( ++ ChunkHolderManager.TicketOperation.addOp( ++ chunk, ++ REGION_PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed ++ ) ++ ); ++ chunks.add(chunk); ++ this.loadingQueue.enqueue(chunk); ++ } ++ ++ // here we need to flush tickets, as scheduleChunkLoad requires tickets to be propagated with addTicket = false ++ this.flushDelayedTicketOps(); ++ // we only need to call scheduleChunkLoad because the loaded ticket level is not enough to start the chunk ++ // load - only generate ticket levels start anything, but they start generation... ++ // propagate levels ++ // Note: this CAN call plugin logic, so it is VITAL that our bookkeeping logic is completely done by the time this is invoked ++ this.world.chunkTaskScheduler.chunkHolderManager.processTicketUpdates(); ++ ++ if (this.removed) { ++ // process ticket updates may invoke plugin logic, which may remove this player ++ return; ++ } ++ ++ for (int i = 0; i < maxLoadsThisTick; ++i) { ++ final long queuedLoadChunk = chunks.getLong(i); ++ final int queuedChunkX = CoordinateUtils.getChunkX(queuedLoadChunk); ++ final int queuedChunkZ = CoordinateUtils.getChunkZ(queuedLoadChunk); ++ this.world.chunkTaskScheduler.scheduleChunkLoad( ++ queuedChunkX, queuedChunkZ, ChunkStatus.EMPTY, false, PrioritisedExecutor.Priority.NORMAL, null ++ ); ++ if (this.removed) { ++ return; ++ } ++ } ++ } ++ ++ // try to progress chunk generations ++ while (!this.generatingQueue.isEmpty()) { ++ final long pendingGenChunk = this.generatingQueue.firstLong(); ++ final int pendingChunkX = CoordinateUtils.getChunkX(pendingGenChunk); ++ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingGenChunk); ++ final LevelChunk pending = this.world.chunkSource.getChunkAtIfLoadedMainThreadNoCache(pendingChunkX, pendingChunkZ); ++ if (pending == null) { ++ // nothing to do here ++ break; ++ } ++ ++ // chunk has generated, so we can take it out of queue ++ this.generatingQueue.dequeueLong(); ++ ++ final byte prev = this.chunkTicketStage.put(pendingGenChunk, CHUNK_TICKET_STAGE_GENERATED); ++ if (prev != CHUNK_TICKET_STAGE_GENERATING) { ++ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATING + ", not " + prev); ++ } ++ ++ // try to move to send queue ++ if (this.wantChunkSent(pendingChunkX, pendingChunkZ)) { ++ this.sendQueue.enqueue(pendingGenChunk); ++ } ++ // try to move to tick queue ++ if (this.wantChunkTicked(pendingChunkX, pendingChunkZ)) { ++ this.tickingQueue.enqueue(pendingGenChunk); ++ } ++ } ++ ++ // try to push more chunk generations ++ final long maxGens = Math.max(0L, Math.min(MAX_RATE, Math.min(this.genQueue.size(), this.getMaxChunkGenerates()))); ++ final int maxGensThisTick = (int)this.chunkGenerateTicketLimiter.takeAllocation(time, genRate, maxGens); ++ int ratedGensThisTick = 0; ++ while (!this.genQueue.isEmpty()) { ++ final long chunkKey = this.genQueue.firstLong(); ++ final int chunkX = CoordinateUtils.getChunkX(chunkKey); ++ final int chunkZ = CoordinateUtils.getChunkZ(chunkKey); ++ final ChunkAccess chunk = this.world.chunkSource.getChunkAtImmediately(chunkX, chunkZ); ++ if (chunk.getStatus() != ChunkStatus.FULL) { ++ // only rate limit actual generations ++ if ((ratedGensThisTick + 1) > maxGensThisTick) { ++ break; ++ } ++ ++ratedGensThisTick; ++ } ++ ++ this.genQueue.dequeueLong(); ++ ++ final byte prev = this.chunkTicketStage.put(chunkKey, CHUNK_TICKET_STAGE_GENERATING); ++ if (prev != CHUNK_TICKET_STAGE_LOADED) { ++ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADED + ", not " + prev); ++ } ++ this.pushDelayedTicketOp( ++ ChunkHolderManager.TicketOperation.addAndRemove( ++ chunkKey, ++ REGION_PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed, ++ REGION_PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed ++ ) ++ ); ++ this.generatingQueue.enqueue(chunkKey); ++ } ++ ++ // try to pull ticking chunks ++ tick_check_outer: ++ while (!this.tickingQueue.isEmpty()) { ++ final long pendingTicking = this.tickingQueue.firstLong(); ++ final int pendingChunkX = CoordinateUtils.getChunkX(pendingTicking); ++ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingTicking); ++ ++ final int tickingReq = 2; ++ for (int dz = -tickingReq; dz <= tickingReq; ++dz) { ++ for (int dx = -tickingReq; dx <= tickingReq; ++dx) { ++ if ((dx | dz) == 0) { ++ continue; ++ } ++ final long neighbour = CoordinateUtils.getChunkKey(dx + pendingChunkX, dz + pendingChunkZ); ++ final byte stage = this.chunkTicketStage.get(neighbour); ++ if (stage != CHUNK_TICKET_STAGE_GENERATED && stage != CHUNK_TICKET_STAGE_TICK) { ++ break tick_check_outer; ++ } ++ } ++ } ++ // only gets here if all neighbours were marked as generated or ticking themselves ++ this.tickingQueue.dequeueLong(); ++ this.pushDelayedTicketOp( ++ ChunkHolderManager.TicketOperation.addAndRemove( ++ pendingTicking, ++ REGION_PLAYER_TICKET, TICK_TICKET_LEVEL, this.idBoxed, ++ REGION_PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed ++ ) ++ ); ++ // there is no queue to add after ticking ++ final byte prev = this.chunkTicketStage.put(pendingTicking, CHUNK_TICKET_STAGE_TICK); ++ if (prev != CHUNK_TICKET_STAGE_GENERATED) { ++ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATED + ", not " + prev); ++ } ++ } ++ ++ // try to pull sending chunks ++ final long maxSends = Math.max(0L, Math.min(MAX_RATE, Integer.MAX_VALUE)); // no logic to track concurrent sends ++ final int maxSendsThisTick = Math.min((int)this.chunkSendLimiter.takeAllocation(time, sendRate, maxSends), this.sendQueue.size()); ++ // we do not return sends that we took from the allocation back because we want to limit the max send rate, not target it ++ for (int i = 0; i < maxSendsThisTick; ++i) { ++ final long pendingSend = this.sendQueue.firstLong(); ++ final int pendingSendX = CoordinateUtils.getChunkX(pendingSend); ++ final int pendingSendZ = CoordinateUtils.getChunkZ(pendingSend); ++ final LevelChunk chunk = this.world.chunkSource.getChunkAtIfLoadedMainThreadNoCache(pendingSendX, pendingSendZ); ++ if (!chunk.areNeighboursLoaded(1) || !TickThread.isTickThreadFor(this.world, pendingSendX, pendingSendZ)) { ++ // nothing to do ++ // the target chunk may not be owned by this region, but this should be resolved in the future ++ break; ++ } ++ if (!chunk.isPostProcessingDone) { ++ // not yet post-processed, need to do this so that tile entities can properly be sent to clients ++ chunk.postProcessGeneration(); ++ // check if there was any recursive action ++ if (this.removed || this.sendQueue.isEmpty() || this.sendQueue.firstLong() != pendingSend) { ++ return; ++ } // else: good to dequeue and send, fall through ++ } ++ this.sendQueue.dequeueLong(); ++ ++ this.sendChunk(pendingSendX, pendingSendZ); ++ if (this.removed) { ++ // sendChunk may invoke plugin logic ++ return; ++ } ++ } ++ ++ this.flushDelayedTicketOps(); ++ // we assume propagate ticket levels happens after this call ++ } ++ ++ void add() { ++ TickThread.ensureTickThread(this.player, "Cannot add player asynchronously"); ++ if (this.removed) { ++ throw new IllegalStateException("Adding removed player chunk loader"); ++ } ++ final ViewDistances playerDistances = this.player.getViewDistances(); ++ final ViewDistances worldDistances = this.world.getViewDistances(); ++ final int chunkX = this.player.chunkPosition().x; ++ final int chunkZ = this.player.chunkPosition().z; ++ ++ final int tickViewDistance = getTickDistance(playerDistances.tickViewDistance, worldDistances.tickViewDistance); ++ // load view cannot be less-than tick view + 1 ++ final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance); ++ // send view cannot be greater-than load view ++ final int clientViewDistance = getClientViewDistance(this.player); ++ final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance); ++ ++ // send view distances ++ this.player.connection.send(this.updateClientChunkRadius(sendViewDistance)); ++ this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance)); ++ ++ // add to distance maps ++ this.broadcastMap.add(chunkX, chunkZ, sendViewDistance + 1); ++ this.loadTicketCleanup.add(chunkX, chunkZ, loadViewDistance + 1); ++ this.tickMap.add(chunkX, chunkZ, tickViewDistance); ++ ++ // update chunk center ++ this.player.connection.send(this.updateClientChunkCenter(chunkX, chunkZ)); ++ ++ // now we can update ++ this.update(); ++ } ++ ++ private boolean isLoadedChunkGeneratable(final int chunkX, final int chunkZ) { ++ return this.isLoadedChunkGeneratable(this.world.chunkSource.getChunkAtImmediately(chunkX, chunkZ)); ++ } ++ ++ private boolean isLoadedChunkGeneratable(final ChunkAccess chunkAccess) { ++ final BelowZeroRetrogen belowZeroRetrogen; ++ // see PortalForcer#findPortalAround ++ return chunkAccess != null && ( ++ chunkAccess.getStatus() == ChunkStatus.FULL || ++ ((belowZeroRetrogen = chunkAccess.getBelowZeroRetrogen()) != null && belowZeroRetrogen.targetStatus().isOrAfter(ChunkStatus.SPAWN)) ++ ); ++ } ++ ++ void update() { ++ TickThread.ensureTickThread(this.player, "Cannot update player asynchronously"); ++ if (this.removed) { ++ throw new IllegalStateException("Updating removed player chunk loader"); ++ } ++ final ViewDistances playerDistances = this.player.getViewDistances(); ++ final ViewDistances worldDistances = this.world.getViewDistances(); ++ ++ final int tickViewDistance = getTickDistance(playerDistances.tickViewDistance, worldDistances.tickViewDistance); ++ // load view cannot be less-than tick view + 1 ++ final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance); ++ // send view cannot be greater-than load view ++ final int clientViewDistance = getClientViewDistance(this.player); ++ final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance); ++ ++ final ChunkPos playerPos = this.player.chunkPosition(); ++ final boolean canGenerateChunks = this.canPlayerGenerateChunks(); ++ final int currentChunkX = playerPos.x; ++ final int currentChunkZ = playerPos.z; ++ ++ final int prevChunkX = this.lastChunkX; ++ final int prevChunkZ = this.lastChunkZ; ++ ++ if ( ++ // has view distance stayed the same? ++ sendViewDistance == this.lastSendDistance ++ && loadViewDistance == this.lastLoadDistance ++ && tickViewDistance == this.lastTickDistance ++ ++ // has our chunk stayed the same? ++ && prevChunkX == currentChunkX ++ && prevChunkZ == currentChunkZ ++ ++ // can we still generate chunks? ++ && this.canGenerateChunks == canGenerateChunks ++ ) { ++ // nothing we care about changed, so we're not re-calculating ++ return; ++ } ++ ++ // update distance maps ++ this.broadcastMap.update(currentChunkX, currentChunkZ, sendViewDistance + 1); ++ this.loadTicketCleanup.update(currentChunkX, currentChunkZ, loadViewDistance + 1); ++ this.tickMap.update(currentChunkX, currentChunkZ, tickViewDistance); ++ if (sendViewDistance > loadViewDistance || tickViewDistance > loadViewDistance) { ++ throw new IllegalStateException(); ++ } ++ ++ // update VDs for client ++ // this should be after the distance map updates, as they will send unload packets ++ if (this.lastSentChunkRadius != sendViewDistance) { ++ this.player.connection.send(this.updateClientChunkRadius(sendViewDistance)); ++ } ++ if (this.lastSentSimulationDistance != tickViewDistance) { ++ this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance)); ++ } ++ ++ this.sendQueue.clear(); ++ this.tickingQueue.clear(); ++ this.generatingQueue.clear(); ++ this.genQueue.clear(); ++ this.loadingQueue.clear(); ++ this.loadQueue.clear(); ++ ++ this.lastChunkX = currentChunkX; ++ this.lastChunkZ = currentChunkZ; ++ this.lastSendDistance = sendViewDistance; ++ this.lastLoadDistance = loadViewDistance; ++ this.lastTickDistance = tickViewDistance; ++ this.canGenerateChunks = canGenerateChunks; ++ ++ // +1 since we need to load chunks +1 around the load view distance... ++ final long[] toIterate = SEARCH_RADIUS_ITERATION_LIST[loadViewDistance + 1]; ++ // the iteration order is by increasing manhattan distance - so, we do NOT need to ++ // sort anything in the queue! ++ for (final long deltaChunk : toIterate) { ++ final int dx = CoordinateUtils.getChunkX(deltaChunk); ++ final int dz = CoordinateUtils.getChunkZ(deltaChunk); ++ final int chunkX = dx + currentChunkX; ++ final int chunkZ = dz + currentChunkZ; ++ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz)); ++ final int manhattanDistance = Math.abs(dx) + Math.abs(dz); ++ ++ // since chunk sending is not by radius alone, we need an extra check here to account for ++ // everything <= sendDistance ++ // Note: Vanilla may want to send chunks outside the send view distance, so we do need ++ // the dist <= view check ++ final boolean sendChunk = (squareDistance <= (sendViewDistance + 1)) ++ && wantChunkLoaded(currentChunkX, currentChunkZ, chunkX, chunkZ, sendViewDistance); ++ final boolean sentChunk = sendChunk ? this.sentChunks.contains(chunk) : this.sentChunks.remove(chunk); ++ ++ if (!sendChunk && sentChunk) { ++ // have sent the chunk, but don't want it anymore ++ // unload it now ++ this.sendUnloadChunkRaw(chunkX, chunkZ); ++ } ++ ++ final byte stage = this.chunkTicketStage.get(chunk); ++ switch (stage) { ++ case CHUNK_TICKET_STAGE_NONE: { ++ // we want the chunk to be at least loaded ++ this.loadQueue.enqueue(chunk); ++ break; ++ } ++ case CHUNK_TICKET_STAGE_LOADING: { ++ this.loadingQueue.enqueue(chunk); ++ break; ++ } ++ case CHUNK_TICKET_STAGE_LOADED: { ++ if (canGenerateChunks || this.isLoadedChunkGeneratable(chunkX, chunkZ)) { ++ this.genQueue.enqueue(chunk); ++ } ++ break; ++ } ++ case CHUNK_TICKET_STAGE_GENERATING: { ++ this.generatingQueue.enqueue(chunk); ++ break; ++ } ++ case CHUNK_TICKET_STAGE_GENERATED: { ++ if (sendChunk && !sentChunk) { ++ this.sendQueue.enqueue(chunk); ++ } ++ if (squareDistance <= tickViewDistance) { ++ this.tickingQueue.enqueue(chunk); ++ } ++ break; ++ } ++ case CHUNK_TICKET_STAGE_TICK: { ++ if (sendChunk && !sentChunk) { ++ this.sendQueue.enqueue(chunk); ++ } ++ break; ++ } ++ default: { ++ throw new IllegalStateException("Unknown stage: " + stage); ++ } ++ } ++ } ++ ++ // update the chunk center ++ // this must be done last so that the client does not ignore any of our unload chunk packets above ++ if (this.lastSentChunkCenterX != currentChunkX || this.lastSentChunkCenterZ != currentChunkZ) { ++ this.player.connection.send(this.updateClientChunkCenter(currentChunkX, currentChunkZ)); ++ } ++ ++ this.flushDelayedTicketOps(); ++ } ++ ++ void remove() { ++ TickThread.ensureTickThread(this.player, "Cannot add player asynchronously"); ++ if (this.removed) { ++ throw new IllegalStateException("Removing removed player chunk loader"); ++ } ++ this.removed = true; ++ // sends the chunk unload packets ++ this.broadcastMap.remove(); ++ // cleans up loading/generating tickets ++ this.loadTicketCleanup.remove(); ++ // cleans up ticking tickets ++ this.tickMap.remove(); ++ ++ // purge queues ++ this.sendQueue.clear(); ++ this.tickingQueue.clear(); ++ this.generatingQueue.clear(); ++ this.genQueue.clear(); ++ this.loadingQueue.clear(); ++ this.loadQueue.clear(); ++ ++ // flush ticket changes ++ this.flushDelayedTicketOps(); ++ ++ // now all tickets should be removed, which is all of our external state ++ } ++ } ++ ++ // TODO rebase into util patch ++ private static final class AllocatingRateLimiter { ++ ++ // max difference granularity in ns ++ private static final long MAX_GRANULARITY = TimeUnit.SECONDS.toNanos(1L); ++ ++ private double allocation; ++ private long lastAllocationUpdate; ++ private double takeCarry; ++ private long lastTakeUpdate; ++ ++ // rate in units/s, and time in ns ++ public void tickAllocation(final long time, final double rate, final double maxAllocation) { ++ final long diff = Math.min(MAX_GRANULARITY, time - this.lastAllocationUpdate); ++ this.lastAllocationUpdate = time; ++ ++ this.allocation = Math.min(maxAllocation - this.takeCarry, this.allocation + rate * (diff*1.0E-9D)); ++ } ++ ++ // rate in units/s, and time in ns ++ public long takeAllocation(final long time, final double rate, final long maxTake) { ++ if (maxTake < 1L) { ++ return 0L; ++ } ++ ++ double ret = this.takeCarry; ++ final long diff = Math.min(MAX_GRANULARITY, time - this.lastTakeUpdate); ++ this.lastTakeUpdate = time; ++ ++ // note: abs(takeCarry) <= 1.0 ++ final double take = Math.min(Math.min((double)maxTake - this.takeCarry, this.allocation), rate * (diff*1.0E-9)); ++ ++ ret += take; ++ this.allocation -= take; ++ ++ final long retInteger = (long)Math.floor(ret); ++ this.takeCarry = ret - (double)retInteger; ++ ++ return retInteger; ++ } ++ } ++ ++ static final class CountedSRSWLinkedQueue { ++ ++ private final SRSWLinkedQueue queue = new SRSWLinkedQueue<>(); ++ private volatile long countAdded; ++ private volatile long countRemoved; ++ ++ private static final VarHandle COUNT_ADDED_HANDLE = ConcurrentUtil.getVarHandle(CountedSRSWLinkedQueue.class, "countAdded", long.class); ++ private static final VarHandle COUNT_REMOVED_HANDLE = ConcurrentUtil.getVarHandle(CountedSRSWLinkedQueue.class, "countRemoved", long.class); ++ ++ private long getCountAddedPlain() { ++ return (long)COUNT_ADDED_HANDLE.get(this); ++ } ++ ++ private long getCountAddedAcquire() { ++ return (long)COUNT_ADDED_HANDLE.getAcquire(this); ++ } ++ ++ private void setCountAddedRelease(final long to) { ++ COUNT_ADDED_HANDLE.setRelease(this, to); ++ } ++ ++ private long getCountRemovedPlain() { ++ return (long)COUNT_REMOVED_HANDLE.get(this); ++ } ++ ++ private long getCountRemovedAcquire() { ++ return (long)COUNT_REMOVED_HANDLE.getAcquire(this); ++ } ++ ++ private void setCountRemovedRelease(final long to) { ++ COUNT_REMOVED_HANDLE.setRelease(this, to); ++ } ++ ++ public void add(final E element) { ++ this.setCountAddedRelease(this.getCountAddedPlain() + 1L); ++ this.queue.addLast(element); ++ } ++ ++ public E poll() { ++ final E ret = this.queue.poll(); ++ if (ret != null) { ++ this.setCountRemovedRelease(this.getCountRemovedPlain() + 1L); ++ } ++ ++ return ret; ++ } ++ ++ public long size() { ++ final long removed = this.getCountRemovedAcquire(); ++ final long added = this.getCountAddedAcquire(); ++ ++ return added - removed; ++ } ++ } ++ ++ private static class CustomLongArray extends LongArrayList { ++ ++ public CustomLongArray() { ++ super(); ++ } ++ ++ public CustomLongArray(final int expected) { ++ super(expected); ++ } ++ ++ public boolean addAll(final CustomLongArray list) { ++ this.addElements(this.size, list.a, 0, list.size); ++ return list.size != 0; ++ } ++ ++ public void addUnchecked(final long value) { ++ this.a[this.size++] = value; ++ } ++ ++ public void forceSize(final int to) { ++ this.size = to; ++ } ++ ++ @Override ++ public int hashCode() { ++ long h = 1L; ++ ++ Objects.checkFromToIndex(0, this.size, this.a.length); ++ ++ for (int i = 0; i < this.size; ++i) { ++ h = it.unimi.dsi.fastutil.HashCommon.mix(h + this.a[i]); ++ } ++ ++ return (int)h; ++ } ++ ++ @Override ++ public boolean equals(final Object o) { ++ if (o == this) { ++ return true; ++ } ++ ++ if (!(o instanceof CustomLongArray other)) { ++ return false; ++ } ++ ++ return this.size == other.size && Arrays.equals(this.a, 0, this.size, other.a, 0, this.size); ++ } ++ } ++ ++ private static int getDistanceSize(final int radius, final int max) { ++ if (radius == 0) { ++ return 1; ++ } ++ final int diff = radius - max; ++ if (diff <= 0) { ++ return 4*radius; ++ } ++ return 4*(max - Math.max(0, diff - 1)); ++ } ++ ++ private static int getQ1DistanceSize(final int radius, final int max) { ++ if (radius == 0) { ++ return 1; ++ } ++ final int diff = radius - max; ++ if (diff <= 0) { ++ return radius+1; ++ } ++ return max - diff + 1; ++ } ++ ++ private static final class BasicFIFOLQueue { ++ ++ private final long[] values; ++ private int head, tail; ++ ++ public BasicFIFOLQueue(final int cap) { ++ if (cap <= 1) { ++ throw new IllegalArgumentException(); ++ } ++ this.values = new long[cap]; ++ } ++ ++ public boolean isEmpty() { ++ return this.head == this.tail; ++ } ++ ++ public long removeFirst() { ++ final long ret = this.values[this.head]; ++ ++ if (this.head == this.tail) { ++ throw new IllegalStateException(); ++ } ++ ++ ++this.head; ++ if (this.head == this.values.length) { ++ this.head = 0; ++ } ++ ++ return ret; ++ } ++ ++ public void addLast(final long value) { ++ this.values[this.tail++] = value; ++ ++ if (this.tail == this.head) { ++ throw new IllegalStateException(); ++ } ++ ++ if (this.tail == this.values.length) { ++ this.tail = 0; ++ } ++ } ++ } ++ ++ private static CustomLongArray[] makeQ1BFS(final int radius) { ++ final CustomLongArray[] ret = new CustomLongArray[2 * radius + 1]; ++ final BasicFIFOLQueue queue = new BasicFIFOLQueue(Math.max(1, 4 * radius) + 1); ++ final LongOpenHashSet seen = new LongOpenHashSet((radius + 1) * (radius + 1)); ++ ++ seen.add(CoordinateUtils.getChunkKey(0, 0)); ++ queue.addLast(CoordinateUtils.getChunkKey(0, 0)); ++ while (!queue.isEmpty()) { ++ final long chunk = queue.removeFirst(); ++ final int chunkX = CoordinateUtils.getChunkX(chunk); ++ final int chunkZ = CoordinateUtils.getChunkZ(chunk); ++ ++ final int index = Math.abs(chunkX) + Math.abs(chunkZ); ++ final CustomLongArray list = ret[index]; ++ if (list != null) { ++ list.addUnchecked(chunk); ++ } else { ++ (ret[index] = new CustomLongArray(getQ1DistanceSize(index, radius))).addUnchecked(chunk); ++ } ++ ++ for (int i = 0; i < 4; ++i) { ++ // 0 -> -1, 0 ++ // 1 -> 0, -1 ++ // 2 -> 1, 0 ++ // 3 -> 0, 1 ++ ++ final int signInv = -(i >>> 1); // 2/3 -> -(1), 0/1 -> -(0) ++ // note: -n = (~n) + 1 ++ // (n ^ signInv) - signInv = signInv == 0 ? ((n ^ 0) - 0 = n) : ((n ^ -1) - (-1) = ~n + 1) ++ ++ final int axis = i & 1; // 0/2 -> 0, 1/3 -> 1 ++ final int dx = ((axis - 1) ^ signInv) - signInv; // 0 -> -1, 1 -> 0 ++ final int dz = (-axis ^ signInv) - signInv; // 0 -> 0, 1 -> -1 ++ ++ final int neighbourX = chunkX + dx; ++ final int neighbourZ = chunkZ + dz; ++ final long neighbour = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); ++ ++ if ((neighbourX | neighbourZ) < 0 || Math.max(Math.abs(neighbourX), Math.abs(neighbourZ)) > radius) { ++ // don't enqueue out of range ++ continue; ++ } ++ ++ if (!seen.add(neighbour)) { ++ continue; ++ } ++ ++ queue.addLast(neighbour); ++ } ++ } ++ ++ return ret; ++ } ++ ++ // doesn't appear worth optimising this function now, even though it's 70% of the call ++ private static CustomLongArray spread(final CustomLongArray input, final int size) { ++ final LongLinkedOpenHashSet notAdded = new LongLinkedOpenHashSet(input); ++ final CustomLongArray added = new CustomLongArray(size); ++ ++ while (!notAdded.isEmpty()) { ++ if (added.isEmpty()) { ++ added.addUnchecked(notAdded.removeLastLong()); ++ continue; ++ } ++ ++ long maxChunk = -1L; ++ int maxDist = 0; ++ ++ // select the chunk from the not yet added set that has the largest minimum distance from ++ // the current set of added chunks ++ ++ for (final LongIterator iterator = notAdded.iterator(); iterator.hasNext();) { ++ final long chunkKey = iterator.nextLong(); ++ final int chunkX = CoordinateUtils.getChunkX(chunkKey); ++ final int chunkZ = CoordinateUtils.getChunkZ(chunkKey); ++ ++ int minDist = Integer.MAX_VALUE; ++ ++ final int len = added.size(); ++ final long[] addedArr = added.elements(); ++ Objects.checkFromToIndex(0, len, addedArr.length); ++ for (int i = 0; i < len; ++i) { ++ final long addedKey = addedArr[i]; ++ final int addedX = CoordinateUtils.getChunkX(addedKey); ++ final int addedZ = CoordinateUtils.getChunkZ(addedKey); ++ ++ // here we use square distance because chunk generation uses neighbours in a square radius ++ final int dist = Math.max(Math.abs(addedX - chunkX), Math.abs(addedZ - chunkZ)); ++ ++ minDist = Math.min(dist, minDist); ++ } ++ ++ if (minDist > maxDist) { ++ maxDist = minDist; ++ maxChunk = chunkKey; ++ } ++ } ++ ++ // move the selected chunk from the not added set to the added set ++ ++ if (!notAdded.remove(maxChunk)) { ++ throw new IllegalStateException(); ++ } ++ ++ added.addUnchecked(maxChunk); ++ } ++ ++ return added; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java b/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java +new file mode 100644 +index 0000000000000000000000000000000000000000..15ee41452992714108efe53b708b5a4e1da7c1ff +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java +@@ -0,0 +1,902 @@ ++package io.papermc.paper.chunk.system.entity; ++ ++import com.destroystokyo.paper.util.maplist.EntityList; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.util.CoordinateUtils; ++import io.papermc.paper.util.TickThread; ++import io.papermc.paper.util.WorldUtil; ++import io.papermc.paper.world.ChunkEntitySlices; ++import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; ++import net.minecraft.core.BlockPos; ++import io.papermc.paper.chunk.system.ChunkSystem; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.util.AbortableIterationConsumer; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.entity.EntityInLevelCallback; ++import net.minecraft.world.level.entity.EntityTypeTest; ++import net.minecraft.world.level.entity.LevelCallback; ++import net.minecraft.server.level.FullChunkStatus; ++import net.minecraft.world.level.entity.LevelEntityGetter; ++import net.minecraft.world.level.entity.Visibility; ++import net.minecraft.world.phys.AABB; ++import net.minecraft.world.phys.Vec3; ++import org.jetbrains.annotations.NotNull; ++import org.jetbrains.annotations.Nullable; ++import org.slf4j.Logger; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Iterator; ++import java.util.List; ++import java.util.NoSuchElementException; ++import java.util.UUID; ++import java.util.concurrent.locks.StampedLock; ++import java.util.function.Consumer; ++import java.util.function.Predicate; ++ ++public final class EntityLookup implements LevelEntityGetter { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ protected static final int REGION_SHIFT = 5; ++ protected static final int REGION_MASK = (1 << REGION_SHIFT) - 1; ++ protected static final int REGION_SIZE = 1 << REGION_SHIFT; ++ ++ public final ServerLevel world; ++ ++ private final StampedLock stateLock = new StampedLock(); ++ protected final Long2ObjectOpenHashMap regions = new Long2ObjectOpenHashMap<>(128, 0.5f); ++ ++ private final int minSection; // inclusive ++ private final int maxSection; // inclusive ++ private final LevelCallback worldCallback; ++ ++ private final StampedLock entityByLock = new StampedLock(); ++ private final Int2ReferenceOpenHashMap entityById = new Int2ReferenceOpenHashMap<>(); ++ private final Object2ReferenceOpenHashMap entityByUUID = new Object2ReferenceOpenHashMap<>(); ++ private final EntityList accessibleEntities = new EntityList(); ++ ++ public EntityLookup(final ServerLevel world, final LevelCallback worldCallback) { ++ this.world = world; ++ this.minSection = WorldUtil.getMinSection(world); ++ this.maxSection = WorldUtil.getMaxSection(world); ++ this.worldCallback = worldCallback; ++ } ++ ++ private static Entity maskNonAccessible(final Entity entity) { ++ if (entity == null) { ++ return null; ++ } ++ final Visibility visibility = EntityLookup.getEntityStatus(entity); ++ return visibility.isAccessible() ? entity : null; ++ } ++ ++ @Nullable ++ @Override ++ public Entity get(final int id) { ++ final long attempt = this.entityByLock.tryOptimisticRead(); ++ if (attempt != 0L) { ++ try { ++ final Entity ret = this.entityById.get(id); ++ ++ if (this.entityByLock.validate(attempt)) { ++ return maskNonAccessible(ret); ++ } ++ } catch (final Error error) { ++ throw error; ++ } catch (final Throwable thr) { ++ // ignore ++ } ++ } ++ ++ this.entityByLock.readLock(); ++ try { ++ return maskNonAccessible(this.entityById.get(id)); ++ } finally { ++ this.entityByLock.tryUnlockRead(); ++ } ++ } ++ ++ @Nullable ++ @Override ++ public Entity get(final UUID id) { ++ final long attempt = this.entityByLock.tryOptimisticRead(); ++ if (attempt != 0L) { ++ try { ++ final Entity ret = this.entityByUUID.get(id); ++ ++ if (this.entityByLock.validate(attempt)) { ++ return maskNonAccessible(ret); ++ } ++ } catch (final Error error) { ++ throw error; ++ } catch (final Throwable thr) { ++ // ignore ++ } ++ } ++ ++ this.entityByLock.readLock(); ++ try { ++ return maskNonAccessible(this.entityByUUID.get(id)); ++ } finally { ++ this.entityByLock.tryUnlockRead(); ++ } ++ } ++ ++ public boolean hasEntity(final UUID uuid) { ++ return this.get(uuid) != null; ++ } ++ ++ public String getDebugInfo() { ++ return "count_id:" + this.entityById.size() + ",count_uuid:" + this.entityByUUID.size() + ",region_count:" + this.regions.size(); ++ } ++ ++ static final class ArrayIterable implements Iterable { ++ ++ private final T[] array; ++ private final int off; ++ private final int length; ++ ++ public ArrayIterable(final T[] array, final int off, final int length) { ++ this.array = array; ++ this.off = off; ++ this.length = length; ++ if (length > array.length) { ++ throw new IllegalArgumentException("Length must be no greater-than the array length"); ++ } ++ } ++ ++ @NotNull ++ @Override ++ public Iterator iterator() { ++ return new ArrayIterator<>(this.array, this.off, this.length); ++ } ++ ++ static final class ArrayIterator implements Iterator { ++ ++ private final T[] array; ++ private int off; ++ private final int length; ++ ++ public ArrayIterator(final T[] array, final int off, final int length) { ++ this.array = array; ++ this.off = off; ++ this.length = length; ++ } ++ ++ @Override ++ public boolean hasNext() { ++ return this.off < this.length; ++ } ++ ++ @Override ++ public T next() { ++ if (this.off >= this.length) { ++ throw new NoSuchElementException(); ++ } ++ return this.array[this.off++]; ++ } ++ ++ @Override ++ public void remove() { ++ throw new UnsupportedOperationException(); ++ } ++ } ++ } ++ ++ @Override ++ public Iterable getAll() { ++ return new ArrayIterable<>(this.accessibleEntities.getRawData(), 0, this.accessibleEntities.size()); ++ } ++ ++ public Entity[] getAllCopy() { ++ return Arrays.copyOf(this.accessibleEntities.getRawData(), this.accessibleEntities.size(), Entity[].class); ++ } ++ ++ @Override ++ public void get(final EntityTypeTest filter, final AbortableIterationConsumer action) { ++ final Int2ReferenceOpenHashMap entityCopy; ++ ++ this.entityByLock.readLock(); ++ try { ++ entityCopy = this.entityById.clone(); ++ } finally { ++ this.entityByLock.tryUnlockRead(); ++ } ++ for (final Entity entity : entityCopy.values()) { ++ final Visibility visibility = EntityLookup.getEntityStatus(entity); ++ if (!visibility.isAccessible()) { ++ continue; ++ } ++ final U casted = filter.tryCast(entity); ++ if (casted != null && action.accept(casted).shouldAbort()) { ++ break; ++ } ++ } ++ } ++ ++ @Override ++ public void get(final AABB box, final Consumer action) { ++ List entities = new ArrayList<>(); ++ this.getEntitiesWithoutDragonParts(null, box, entities, null); ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ action.accept(entities.get(i)); ++ } ++ } ++ ++ @Override ++ public void get(final EntityTypeTest filter, final AABB box, final AbortableIterationConsumer action) { ++ List entities = new ArrayList<>(); ++ this.getEntitiesWithoutDragonParts(null, box, entities, null); ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ final U casted = filter.tryCast(entities.get(i)); ++ if (casted != null && action.accept(casted).shouldAbort()) { ++ break; ++ } ++ } ++ } ++ ++ public void entityStatusChange(final Entity entity, final ChunkEntitySlices slices, final Visibility oldVisibility, final Visibility newVisibility, final boolean moved, ++ final boolean created, final boolean destroyed) { ++ TickThread.ensureTickThread(entity, "Entity status change must only happen on the main thread"); ++ ++ if (entity.updatingSectionStatus) { ++ // recursive status update ++ LOGGER.error("Cannot recursively update entity chunk status for entity " + entity, new Throwable()); ++ return; ++ } ++ ++ final boolean entityStatusUpdateBefore = slices == null ? false : slices.startPreventingStatusUpdates(); ++ ++ if (entityStatusUpdateBefore) { ++ LOGGER.error("Cannot update chunk status for entity " + entity + " since entity chunk (" + slices.chunkX + "," + slices.chunkZ + ") is receiving update", new Throwable()); ++ return; ++ } ++ ++ try { ++ final Boolean ticketBlockBefore = this.world.chunkTaskScheduler.chunkHolderManager.blockTicketUpdates(); ++ try { ++ entity.updatingSectionStatus = true; ++ try { ++ if (created) { ++ EntityLookup.this.worldCallback.onCreated(entity); ++ } ++ ++ if (oldVisibility == newVisibility) { ++ if (moved && newVisibility.isAccessible()) { ++ EntityLookup.this.worldCallback.onSectionChange(entity); ++ } ++ return; ++ } ++ ++ if (newVisibility.ordinal() > oldVisibility.ordinal()) { ++ // status upgrade ++ if (!oldVisibility.isAccessible() && newVisibility.isAccessible()) { ++ this.accessibleEntities.add(entity); ++ EntityLookup.this.worldCallback.onTrackingStart(entity); ++ } ++ ++ if (!oldVisibility.isTicking() && newVisibility.isTicking()) { ++ EntityLookup.this.worldCallback.onTickingStart(entity); ++ } ++ } else { ++ // status downgrade ++ if (oldVisibility.isTicking() && !newVisibility.isTicking()) { ++ EntityLookup.this.worldCallback.onTickingEnd(entity); ++ } ++ ++ if (oldVisibility.isAccessible() && !newVisibility.isAccessible()) { ++ this.accessibleEntities.remove(entity); ++ EntityLookup.this.worldCallback.onTrackingEnd(entity); ++ } ++ } ++ ++ if (moved && newVisibility.isAccessible()) { ++ EntityLookup.this.worldCallback.onSectionChange(entity); ++ } ++ ++ if (destroyed) { ++ EntityLookup.this.worldCallback.onDestroyed(entity); ++ } ++ } finally { ++ entity.updatingSectionStatus = false; ++ } ++ } finally { ++ this.world.chunkTaskScheduler.chunkHolderManager.unblockTicketUpdates(ticketBlockBefore); ++ } ++ } finally { ++ if (slices != null) { ++ slices.stopPreventingStatusUpdates(false); ++ } ++ } ++ } ++ ++ public void chunkStatusChange(final int x, final int z, final FullChunkStatus newStatus) { ++ this.getChunk(x, z).updateStatus(newStatus, this); ++ } ++ ++ public void addLegacyChunkEntities(final List entities, final ChunkPos forChunk) { ++ this.addEntityChunk(entities, forChunk, true); ++ } ++ ++ public void addEntityChunkEntities(final List entities, final ChunkPos forChunk) { ++ this.addEntityChunk(entities, forChunk, true); ++ } ++ ++ public void addWorldGenChunkEntities(final List entities, final ChunkPos forChunk) { ++ this.addEntityChunk(entities, forChunk, false); ++ } ++ ++ private void addRecursivelySafe(final Entity root, final boolean fromDisk) { ++ if (!this.addEntity(root, fromDisk)) { ++ // possible we are a passenger, and so should dismount from any valid entity in the world ++ root.stopRiding(true); ++ return; ++ } ++ for (final Entity passenger : root.getPassengers()) { ++ this.addRecursivelySafe(passenger, fromDisk); ++ } ++ } ++ ++ private void addEntityChunk(final List entities, final ChunkPos forChunk, final boolean fromDisk) { ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ final Entity entity = entities.get(i); ++ if (entity.isPassenger()) { ++ continue; ++ } ++ ++ if (!entity.chunkPosition().equals(forChunk)) { ++ LOGGER.warn("Root entity " + entity + " is outside of serialized chunk " + forChunk); ++ // can't set removed here, as we may not own the chunk position ++ // skip the entity ++ continue; ++ } ++ ++ final Vec3 rootPosition = entity.position(); ++ ++ // always adjust positions before adding passengers in case plugins access the entity, and so that ++ // they are added to the right entity chunk ++ for (final Entity passenger : entity.getIndirectPassengers()) { ++ if (!passenger.chunkPosition().equals(forChunk)) { ++ passenger.setPosRaw(rootPosition.x, rootPosition.y, rootPosition.z, true); ++ } ++ } ++ ++ this.addRecursivelySafe(entity, fromDisk); ++ } ++ } ++ ++ public boolean addNewEntity(final Entity entity) { ++ return this.addEntity(entity, false); ++ } ++ ++ public static Visibility getEntityStatus(final Entity entity) { ++ if (entity.isAlwaysTicking()) { ++ return Visibility.TICKING; ++ } ++ final FullChunkStatus entityStatus = entity.chunkStatus; ++ return Visibility.fromFullChunkStatus(entityStatus == null ? FullChunkStatus.INACCESSIBLE : entityStatus); ++ } ++ ++ private boolean addEntity(final Entity entity, final boolean fromDisk) { ++ final BlockPos pos = entity.blockPosition(); ++ final int sectionX = pos.getX() >> 4; ++ final int sectionY = Mth.clamp(pos.getY() >> 4, this.minSection, this.maxSection); ++ final int sectionZ = pos.getZ() >> 4; ++ TickThread.ensureTickThread(this.world, sectionX, sectionZ, "Cannot add entity off-main thread"); ++ ++ if (entity.isRemoved()) { ++ LOGGER.warn("Refusing to add removed entity: " + entity); ++ return false; ++ } ++ ++ if (entity.updatingSectionStatus) { ++ LOGGER.warn("Entity " + entity + " is currently prevented from being added/removed to world since it is processing section status updates", new Throwable()); ++ return false; ++ } ++ ++ if (fromDisk) { ++ ChunkSystem.onEntityPreAdd(this.world, entity); ++ if (entity.isRemoved()) { ++ // removed from checkDupeUUID call ++ return false; ++ } ++ } ++ ++ this.entityByLock.writeLock(); ++ try { ++ if (this.entityById.containsKey(entity.getId())) { ++ LOGGER.warn("Entity id already exists: " + entity.getId() + ", mapped to " + this.entityById.get(entity.getId()) + ", can't add " + entity); ++ return false; ++ } ++ if (this.entityByUUID.containsKey(entity.getUUID())) { ++ LOGGER.warn("Entity uuid already exists: " + entity.getUUID() + ", mapped to " + this.entityByUUID.get(entity.getUUID()) + ", can't add " + entity); ++ return false; ++ } ++ this.entityById.put(entity.getId(), entity); ++ this.entityByUUID.put(entity.getUUID(), entity); ++ } finally { ++ this.entityByLock.tryUnlockWrite(); ++ } ++ ++ entity.sectionX = sectionX; ++ entity.sectionY = sectionY; ++ entity.sectionZ = sectionZ; ++ final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ); ++ if (!slices.addEntity(entity, sectionY)) { ++ LOGGER.warn("Entity " + entity + " added to world '" + this.world.getWorld().getName() + "', but was already contained in entity chunk (" + sectionX + "," + sectionZ + ")"); ++ } ++ ++ entity.setLevelCallback(new EntityCallback(entity)); ++ ++ this.entityStatusChange(entity, slices, Visibility.HIDDEN, getEntityStatus(entity), false, !fromDisk, false); ++ ++ return true; ++ } ++ ++ public boolean canRemoveEntity(final Entity entity) { ++ if (entity.updatingSectionStatus) { ++ return false; ++ } ++ ++ final int sectionX = entity.sectionX; ++ final int sectionZ = entity.sectionZ; ++ final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ); ++ return slices == null || !slices.isPreventingStatusUpdates(); ++ } ++ ++ private void removeEntity(final Entity entity) { ++ final int sectionX = entity.sectionX; ++ final int sectionY = entity.sectionY; ++ final int sectionZ = entity.sectionZ; ++ TickThread.ensureTickThread(this.world, sectionX, sectionZ, "Cannot remove entity off-main"); ++ if (!entity.isRemoved()) { ++ throw new IllegalStateException("Only call Entity#setRemoved to remove an entity"); ++ } ++ final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ); ++ // all entities should be in a chunk ++ if (slices == null) { ++ LOGGER.warn("Cannot remove entity " + entity + " from null entity slices (" + sectionX + "," + sectionZ + ")"); ++ } else { ++ if (slices.isPreventingStatusUpdates()) { ++ throw new IllegalStateException("Attempting to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ") that is receiving status updates"); ++ } ++ if (!slices.removeEntity(entity, sectionY)) { ++ LOGGER.warn("Failed to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ")"); ++ } ++ } ++ entity.sectionX = entity.sectionY = entity.sectionZ = Integer.MIN_VALUE; ++ ++ this.entityByLock.writeLock(); ++ try { ++ if (!this.entityById.remove(entity.getId(), entity)) { ++ LOGGER.warn("Failed to remove entity " + entity + " by id, current entity mapped: " + this.entityById.get(entity.getId())); ++ } ++ if (!this.entityByUUID.remove(entity.getUUID(), entity)) { ++ LOGGER.warn("Failed to remove entity " + entity + " by uuid, current entity mapped: " + this.entityByUUID.get(entity.getUUID())); ++ } ++ } finally { ++ this.entityByLock.tryUnlockWrite(); ++ } ++ } ++ ++ private ChunkEntitySlices moveEntity(final Entity entity) { ++ // ensure we own the entity ++ TickThread.ensureTickThread(entity, "Cannot move entity off-main"); ++ ++ final BlockPos newPos = entity.blockPosition(); ++ final int newSectionX = newPos.getX() >> 4; ++ final int newSectionY = Mth.clamp(newPos.getY() >> 4, this.minSection, this.maxSection); ++ final int newSectionZ = newPos.getZ() >> 4; ++ ++ if (newSectionX == entity.sectionX && newSectionY == entity.sectionY && newSectionZ == entity.sectionZ) { ++ return null; ++ } ++ ++ // ensure the new section is owned by this tick thread ++ TickThread.ensureTickThread(this.world, newSectionX, newSectionZ, "Cannot move entity off-main"); ++ ++ // ensure the old section is owned by this tick thread ++ TickThread.ensureTickThread(this.world, entity.sectionX, entity.sectionZ, "Cannot move entity off-main"); ++ ++ final ChunkEntitySlices old = this.getChunk(entity.sectionX, entity.sectionZ); ++ final ChunkEntitySlices slices = this.getOrCreateChunk(newSectionX, newSectionZ); ++ ++ if (!old.removeEntity(entity, entity.sectionY)) { ++ LOGGER.warn("Could not remove entity " + entity + " from its old chunk section (" + entity.sectionX + "," + entity.sectionY + "," + entity.sectionZ + ") since it was not contained in the section"); ++ } ++ ++ if (!slices.addEntity(entity, newSectionY)) { ++ LOGGER.warn("Could not add entity " + entity + " to its new chunk section (" + newSectionX + "," + newSectionY + "," + newSectionZ + ") as it is already contained in the section"); ++ } ++ ++ entity.sectionX = newSectionX; ++ entity.sectionY = newSectionY; ++ entity.sectionZ = newSectionZ; ++ ++ return slices; ++ } ++ ++ public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List into, final Predicate predicate) { ++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; ++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; ++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; ++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; ++ ++ final int minRegionX = minChunkX >> REGION_SHIFT; ++ final int minRegionZ = minChunkZ >> REGION_SHIFT; ++ final int maxRegionX = maxChunkX >> REGION_SHIFT; ++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; ++ ++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { ++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; ++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; ++ ++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { ++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); ++ ++ if (region == null) { ++ continue; ++ } ++ ++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; ++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; ++ ++ for (int currZ = minZ; currZ <= maxZ; ++currZ) { ++ for (int currX = minX; currX <= maxX; ++currX) { ++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); ++ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { ++ continue; ++ } ++ ++ chunk.getEntitiesWithoutDragonParts(except, box, into, predicate); ++ } ++ } ++ } ++ } ++ } ++ ++ public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { ++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; ++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; ++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; ++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; ++ ++ final int minRegionX = minChunkX >> REGION_SHIFT; ++ final int minRegionZ = minChunkZ >> REGION_SHIFT; ++ final int maxRegionX = maxChunkX >> REGION_SHIFT; ++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; ++ ++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { ++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; ++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; ++ ++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { ++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); ++ ++ if (region == null) { ++ continue; ++ } ++ ++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; ++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; ++ ++ for (int currZ = minZ; currZ <= maxZ; ++currZ) { ++ for (int currX = minX; currX <= maxX; ++currX) { ++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); ++ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { ++ continue; ++ } ++ ++ chunk.getEntities(except, box, into, predicate); ++ } ++ } ++ } ++ } ++ } ++ ++ public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { ++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; ++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; ++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; ++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; ++ ++ final int minRegionX = minChunkX >> REGION_SHIFT; ++ final int minRegionZ = minChunkZ >> REGION_SHIFT; ++ final int maxRegionX = maxChunkX >> REGION_SHIFT; ++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; ++ ++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { ++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; ++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; ++ ++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { ++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); ++ ++ if (region == null) { ++ continue; ++ } ++ ++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; ++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; ++ ++ for (int currZ = minZ; currZ <= maxZ; ++currZ) { ++ for (int currX = minX; currX <= maxX; ++currX) { ++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); ++ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { ++ continue; ++ } ++ ++ chunk.getHardCollidingEntities(except, box, into, predicate); ++ } ++ } ++ } ++ } ++ } ++ ++ public void getEntities(final EntityType type, final AABB box, final List into, ++ final Predicate predicate) { ++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; ++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; ++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; ++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; ++ ++ final int minRegionX = minChunkX >> REGION_SHIFT; ++ final int minRegionZ = minChunkZ >> REGION_SHIFT; ++ final int maxRegionX = maxChunkX >> REGION_SHIFT; ++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; ++ ++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { ++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; ++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; ++ ++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { ++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); ++ ++ if (region == null) { ++ continue; ++ } ++ ++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; ++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; ++ ++ for (int currZ = minZ; currZ <= maxZ; ++currZ) { ++ for (int currX = minX; currX <= maxX; ++currX) { ++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); ++ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { ++ continue; ++ } ++ ++ chunk.getEntities(type, box, (List)into, (Predicate)predicate); ++ } ++ } ++ } ++ } ++ } ++ ++ public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, ++ final Predicate predicate) { ++ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; ++ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; ++ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; ++ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; ++ ++ final int minRegionX = minChunkX >> REGION_SHIFT; ++ final int minRegionZ = minChunkZ >> REGION_SHIFT; ++ final int maxRegionX = maxChunkX >> REGION_SHIFT; ++ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; ++ ++ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { ++ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; ++ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; ++ ++ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { ++ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); ++ ++ if (region == null) { ++ continue; ++ } ++ ++ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; ++ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; ++ ++ for (int currZ = minZ; currZ <= maxZ; ++currZ) { ++ for (int currX = minX; currX <= maxX; ++currX) { ++ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); ++ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { ++ continue; ++ } ++ ++ chunk.getEntities(clazz, except, box, into, predicate); ++ } ++ } ++ } ++ } ++ } ++ ++ public void entitySectionLoad(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) { ++ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot load in entity section off-main"); ++ synchronized (this) { ++ final ChunkEntitySlices curr = this.getChunk(chunkX, chunkZ); ++ if (curr != null) { ++ this.removeChunk(chunkX, chunkZ); ++ ++ curr.mergeInto(slices); ++ ++ this.addChunk(chunkX, chunkZ, slices); ++ } else { ++ this.addChunk(chunkX, chunkZ, slices); ++ } ++ } ++ } ++ ++ public void entitySectionUnload(final int chunkX, final int chunkZ) { ++ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot unload entity section off-main"); ++ this.removeChunk(chunkX, chunkZ); ++ } ++ ++ public ChunkEntitySlices getChunk(final int chunkX, final int chunkZ) { ++ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); ++ if (region == null) { ++ return null; ++ } ++ ++ return region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT)); ++ } ++ ++ public ChunkEntitySlices getOrCreateChunk(final int chunkX, final int chunkZ) { ++ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); ++ ChunkEntitySlices ret; ++ if (region == null || (ret = region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT))) == null) { ++ // loadInEntityChunk will call addChunk for us ++ return this.world.chunkTaskScheduler.chunkHolderManager.getOrCreateEntityChunk(chunkX, chunkZ, true); ++ } ++ ++ return ret; ++ } ++ ++ public ChunkSlicesRegion getRegion(final int regionX, final int regionZ) { ++ final long key = CoordinateUtils.getChunkKey(regionX, regionZ); ++ final long attempt = this.stateLock.tryOptimisticRead(); ++ if (attempt != 0L) { ++ try { ++ final ChunkSlicesRegion ret = this.regions.get(key); ++ ++ if (this.stateLock.validate(attempt)) { ++ return ret; ++ } ++ } catch (final Error error) { ++ throw error; ++ } catch (final Throwable thr) { ++ // ignore ++ } ++ } ++ ++ this.stateLock.readLock(); ++ try { ++ return this.regions.get(key); ++ } finally { ++ this.stateLock.tryUnlockRead(); ++ } ++ } ++ ++ private synchronized void removeChunk(final int chunkX, final int chunkZ) { ++ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); ++ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); ++ ++ final ChunkSlicesRegion region = this.regions.get(key); ++ final int remaining = region.remove(relIndex); ++ ++ if (remaining == 0) { ++ this.stateLock.writeLock(); ++ try { ++ this.regions.remove(key); ++ } finally { ++ this.stateLock.tryUnlockWrite(); ++ } ++ } ++ } ++ ++ public synchronized void addChunk(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) { ++ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); ++ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); ++ ++ ChunkSlicesRegion region = this.regions.get(key); ++ if (region != null) { ++ region.add(relIndex, slices); ++ } else { ++ region = new ChunkSlicesRegion(); ++ region.add(relIndex, slices); ++ this.stateLock.writeLock(); ++ try { ++ this.regions.put(key, region); ++ } finally { ++ this.stateLock.tryUnlockWrite(); ++ } ++ } ++ } ++ ++ public static final class ChunkSlicesRegion { ++ ++ protected final ChunkEntitySlices[] slices = new ChunkEntitySlices[REGION_SIZE * REGION_SIZE]; ++ protected int sliceCount; ++ ++ public ChunkEntitySlices get(final int index) { ++ return this.slices[index]; ++ } ++ ++ public int remove(final int index) { ++ final ChunkEntitySlices slices = this.slices[index]; ++ if (slices == null) { ++ throw new IllegalStateException(); ++ } ++ ++ this.slices[index] = null; ++ ++ return --this.sliceCount; ++ } ++ ++ public void add(final int index, final ChunkEntitySlices slices) { ++ final ChunkEntitySlices curr = this.slices[index]; ++ if (curr != null) { ++ throw new IllegalStateException(); ++ } ++ ++ this.slices[index] = slices; ++ ++ ++this.sliceCount; ++ } ++ } ++ ++ private final class EntityCallback implements EntityInLevelCallback { ++ ++ public final Entity entity; ++ ++ public EntityCallback(final Entity entity) { ++ this.entity = entity; ++ } ++ ++ @Override ++ public void onMove() { ++ final Entity entity = this.entity; ++ final Visibility oldVisibility = getEntityStatus(entity); ++ final ChunkEntitySlices newSlices = EntityLookup.this.moveEntity(this.entity); ++ if (newSlices == null) { ++ // no new section, so didn't change sections ++ return; ++ } ++ final Visibility newVisibility = getEntityStatus(entity); ++ ++ EntityLookup.this.entityStatusChange(entity, newSlices, oldVisibility, newVisibility, true, false, false); ++ } ++ ++ @Override ++ public void onRemove(final Entity.RemovalReason reason) { ++ final Entity entity = this.entity; ++ TickThread.ensureTickThread(entity, "Cannot remove entity off-main"); // Paper - rewrite chunk system ++ final Visibility tickingState = EntityLookup.getEntityStatus(entity); ++ ++ EntityLookup.this.removeEntity(entity); ++ ++ EntityLookup.this.entityStatusChange(entity, null, tickingState, Visibility.HIDDEN, false, false, reason.shouldDestroy()); ++ ++ this.entity.setLevelCallback(NoOpCallback.INSTANCE); ++ } ++ } ++ ++ private static final class NoOpCallback implements EntityInLevelCallback { ++ ++ public static final NoOpCallback INSTANCE = new NoOpCallback(); ++ ++ @Override ++ public void onMove() {} ++ ++ @Override ++ public void onRemove(final Entity.RemovalReason reason) {} ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/io/RegionFileIOThread.java b/src/main/java/io/papermc/paper/chunk/system/io/RegionFileIOThread.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2934f0cf0ef09c84739312b00186c2ef0019a165 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/io/RegionFileIOThread.java +@@ -0,0 +1,1343 @@ ++package io.papermc.paper.chunk.system.io; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.executor.Cancellable; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedQueueExecutorThread; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.util.CoordinateUtils; ++import io.papermc.paper.util.TickThread; ++import it.unimi.dsi.fastutil.HashCommon; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.storage.RegionFile; ++import net.minecraft.world.level.chunk.storage.RegionFileStorage; ++import org.slf4j.Logger; ++import java.io.IOException; ++import java.lang.invoke.VarHandle; ++import java.util.concurrent.CompletableFuture; ++import java.util.concurrent.CompletionException; ++import java.util.concurrent.ConcurrentHashMap; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.function.BiConsumer; ++import java.util.function.BiFunction; ++import java.util.function.Consumer; ++import java.util.function.Function; ++ ++/** ++ * Prioritised RegionFile I/O executor, responsible for all RegionFile access. ++ *

++ * All functions provided are MT-Safe, however certain ordering constraints are recommended: ++ *

    ++ *
  • ++ * Chunk saves may not occur for unloaded chunks. ++ *
  • ++ *
  • ++ * Tasks must be scheduled on the chunk scheduler thread. ++ *
  • ++ *
++ * By following these constraints, no chunk data loss should occur with the exception of underlying I/O problems. ++ */ ++public final class RegionFileIOThread extends PrioritisedQueueExecutorThread { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ /** ++ * The kinds of region files controlled by the region file thread. Add more when needed, and ensure ++ * getControllerFor is updated. ++ */ ++ public static enum RegionFileType { ++ CHUNK_DATA, ++ POI_DATA, ++ ENTITY_DATA; ++ } ++ ++ protected static final RegionFileType[] CACHED_REGIONFILE_TYPES = RegionFileType.values(); ++ ++ private ChunkDataController getControllerFor(final ServerLevel world, final RegionFileType type) { ++ return switch (type) { ++ case CHUNK_DATA -> world.chunkDataControllerNew; ++ case POI_DATA -> world.poiDataControllerNew; ++ case ENTITY_DATA -> world.entityDataControllerNew; ++ default -> throw new IllegalStateException("Unknown controller type " + type); ++ }; ++ } ++ ++ /** ++ * Collects regionfile data for a certain chunk. ++ */ ++ public static final class RegionFileData { ++ ++ private final boolean[] hasResult = new boolean[CACHED_REGIONFILE_TYPES.length]; ++ private final CompoundTag[] data = new CompoundTag[CACHED_REGIONFILE_TYPES.length]; ++ private final Throwable[] throwables = new Throwable[CACHED_REGIONFILE_TYPES.length]; ++ ++ /** ++ * Sets the result associated with the specified regionfile type. Note that ++ * results can only be set once per regionfile type. ++ * ++ * @param type The regionfile type. ++ * @param data The result to set. ++ */ ++ public void setData(final RegionFileType type, final CompoundTag data) { ++ final int index = type.ordinal(); ++ ++ if (this.hasResult[index]) { ++ throw new IllegalArgumentException("Result already exists for type " + type); ++ } ++ this.hasResult[index] = true; ++ this.data[index] = data; ++ } ++ ++ /** ++ * Sets the result associated with the specified regionfile type. Note that ++ * results can only be set once per regionfile type. ++ * ++ * @param type The regionfile type. ++ * @param throwable The result to set. ++ */ ++ public void setThrowable(final RegionFileType type, final Throwable throwable) { ++ final int index = type.ordinal(); ++ ++ if (this.hasResult[index]) { ++ throw new IllegalArgumentException("Result already exists for type " + type); ++ } ++ this.hasResult[index] = true; ++ this.throwables[index] = throwable; ++ } ++ ++ /** ++ * Returns whether there is a result for the specified regionfile type. ++ * ++ * @param type Specified regionfile type. ++ * ++ * @return Whether a result exists for {@code type}. ++ */ ++ public boolean hasResult(final RegionFileType type) { ++ return this.hasResult[type.ordinal()]; ++ } ++ ++ /** ++ * Returns the data result for the regionfile type. ++ * ++ * @param type Specified regionfile type. ++ * ++ * @throws IllegalArgumentException If the result has not been set for {@code type}. ++ * @return The data result for the specified type. If the result is a {@code Throwable}, ++ * then returns {@code null}. ++ */ ++ public CompoundTag getData(final RegionFileType type) { ++ final int index = type.ordinal(); ++ ++ if (!this.hasResult[index]) { ++ throw new IllegalArgumentException("Result does not exist for type " + type); ++ } ++ ++ return this.data[index]; ++ } ++ ++ /** ++ * Returns the throwable result for the regionfile type. ++ * ++ * @param type Specified regionfile type. ++ * ++ * @throws IllegalArgumentException If the result has not been set for {@code type}. ++ * @return The throwable result for the specified type. If the result is an {@code CompoundTag}, ++ * then returns {@code null}. ++ */ ++ public Throwable getThrowable(final RegionFileType type) { ++ final int index = type.ordinal(); ++ ++ if (!this.hasResult[index]) { ++ throw new IllegalArgumentException("Result does not exist for type " + type); ++ } ++ ++ return this.throwables[index]; ++ } ++ } ++ ++ private static final Object INIT_LOCK = new Object(); ++ ++ static RegionFileIOThread[] threads; ++ ++ /* needs to be consistent given a set of parameters */ ++ static RegionFileIOThread selectThread(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { ++ if (threads == null) { ++ throw new IllegalStateException("Threads not initialised"); ++ } ++ ++ final int regionX = chunkX >> 5; ++ final int regionZ = chunkZ >> 5; ++ final int typeOffset = type.ordinal(); ++ ++ return threads[(System.identityHashCode(world) + regionX + regionZ + typeOffset) % threads.length]; ++ } ++ ++ /** ++ * Shuts down the I/O executor(s). Watis for all tasks to complete if specified. ++ * Tasks queued during this call might not be accepted, and tasks queued after will not be accepted. ++ * ++ * @param wait Whether to wait until all tasks have completed. ++ */ ++ public static void close(final boolean wait) { ++ for (int i = 0, len = threads.length; i < len; ++i) { ++ threads[i].close(false, true); ++ } ++ if (wait) { ++ RegionFileIOThread.flush(); ++ } ++ } ++ ++ public static long[] getExecutedTasks() { ++ final long[] ret = new long[threads.length]; ++ for (int i = 0, len = threads.length; i < len; ++i) { ++ ret[i] = threads[i].getTotalTasksExecuted(); ++ } ++ ++ return ret; ++ } ++ ++ public static long[] getTasksScheduled() { ++ final long[] ret = new long[threads.length]; ++ for (int i = 0, len = threads.length; i < len; ++i) { ++ ret[i] = threads[i].getTotalTasksScheduled(); ++ } ++ return ret; ++ } ++ ++ public static void flush() { ++ for (int i = 0, len = threads.length; i < len; ++i) { ++ threads[i].waitUntilAllExecuted(); ++ } ++ } ++ ++ public static void partialFlush(final int totalTasksRemaining) { ++ long failures = 1L; // start out at 0.25ms ++ ++ for (;;) { ++ final long[] executed = getExecutedTasks(); ++ final long[] scheduled = getTasksScheduled(); ++ ++ long sum = 0; ++ for (int i = 0; i < executed.length; ++i) { ++ sum += scheduled[i] - executed[i]; ++ } ++ ++ if (sum <= totalTasksRemaining) { ++ break; ++ } ++ ++ failures = ConcurrentUtil.linearLongBackoff(failures, 250_000L, 5_000_000L); // 500us, 5ms ++ } ++ } ++ ++ /** ++ * Inits the executor with the specified number of threads. ++ * ++ * @param threads Specified number of threads. ++ */ ++ public static void init(final int threads) { ++ synchronized (INIT_LOCK) { ++ if (RegionFileIOThread.threads != null) { ++ throw new IllegalStateException("Already initialised threads"); ++ } ++ ++ RegionFileIOThread.threads = new RegionFileIOThread[threads]; ++ ++ for (int i = 0; i < threads; ++i) { ++ RegionFileIOThread.threads[i] = new RegionFileIOThread(i); ++ RegionFileIOThread.threads[i].start(); ++ } ++ } ++ } ++ ++ private RegionFileIOThread(final int threadNumber) { ++ super(new PrioritisedThreadedTaskQueue(), (int)(1.0e6)); // 1.0ms spinwait time ++ this.setName("RegionFile I/O Thread #" + threadNumber); ++ this.setPriority(Thread.NORM_PRIORITY - 2); // we keep priority close to normal because threads can wait on us ++ this.setUncaughtExceptionHandler((final Thread thread, final Throwable thr) -> { ++ LOGGER.error("Uncaught exception thrown from I/O thread, report this! Thread: " + thread.getName(), thr); ++ }); ++ } ++ ++ /** ++ * Returns whether the current thread is a regionfile I/O executor. ++ * @return Whether the current thread is a regionfile I/O executor. ++ */ ++ public static boolean isRegionFileThread() { ++ return Thread.currentThread() instanceof RegionFileIOThread; ++ } ++ ++ /** ++ * Returns the priority associated with blocking I/O based on the current thread. The goal is to avoid ++ * dumb plugins from taking away priority from threads we consider crucial. ++ * @return The priroity to use with blocking I/O on the current thread. ++ */ ++ public static PrioritisedExecutor.Priority getIOBlockingPriorityForCurrentThread() { ++ if (TickThread.isTickThread()) { ++ return PrioritisedExecutor.Priority.BLOCKING; ++ } ++ return PrioritisedExecutor.Priority.HIGHEST; ++ } ++ ++ /** ++ * Returns the current {@code CompoundTag} pending for write for the specified chunk and regionfile type. ++ * Note that this does not copy the result, so do not modify the result returned. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * ++ * @return The compound tag associated for the specified chunk. {@code null} if no write was pending, or if {@code null} is the write pending. ++ */ ++ public static CompoundTag getPendingWrite(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ return thread.getPendingWriteInternal(world, chunkX, chunkZ, type); ++ } ++ ++ CompoundTag getPendingWriteInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { ++ final ChunkDataController taskController = this.getControllerFor(world, type); ++ final ChunkDataTask task = taskController.tasks.get(Long.valueOf(CoordinateUtils.getChunkKey(chunkX, chunkZ))); ++ ++ if (task == null) { ++ return null; ++ } ++ ++ final CompoundTag ret = task.inProgressWrite; ++ ++ return ret == ChunkDataTask.NOTHING_TO_WRITE ? null : ret; ++ } ++ ++ /** ++ * Returns the priority for the specified regionfile type for the specified chunk. ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * @return The priority for the chunk ++ */ ++ public static PrioritisedExecutor.Priority getPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ return thread.getPriorityInternal(world, chunkX, chunkZ, type); ++ } ++ ++ PrioritisedExecutor.Priority getPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { ++ final ChunkDataController taskController = this.getControllerFor(world, type); ++ final ChunkDataTask task = taskController.tasks.get(Long.valueOf(CoordinateUtils.getChunkKey(chunkX, chunkZ))); ++ ++ if (task == null) { ++ return PrioritisedExecutor.Priority.COMPLETING; ++ } ++ ++ return task.prioritisedTask.getPriority(); ++ } ++ ++ /** ++ * Sets the priority for all regionfile types for the specified chunk. Note that great care should ++ * be taken using this method, as there can be multiple tasks tied to the same chunk that want different ++ * priorities. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param priority New priority. ++ * ++ * @see #raisePriority(ServerLevel, int, int, Priority) ++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, ++ final PrioritisedExecutor.Priority priority) { ++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { ++ RegionFileIOThread.setPriority(world, chunkX, chunkZ, type, priority); ++ } ++ } ++ ++ /** ++ * Sets the priority for the specified regionfile type for the specified chunk. Note that great care should ++ * be taken using this method, as there can be multiple tasks tied to the same chunk that want different ++ * priorities. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * @param priority New priority. ++ * ++ * @see #raisePriority(ServerLevel, int, int, Priority) ++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final PrioritisedExecutor.Priority priority) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ thread.setPriorityInternal(world, chunkX, chunkZ, type, priority); ++ } ++ ++ void setPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final PrioritisedExecutor.Priority priority) { ++ final ChunkDataController taskController = this.getControllerFor(world, type); ++ final ChunkDataTask task = taskController.tasks.get(Long.valueOf(CoordinateUtils.getChunkKey(chunkX, chunkZ))); ++ ++ if (task != null) { ++ task.prioritisedTask.setPriority(priority); ++ } ++ } ++ ++ /** ++ * Raises the priority for all regionfile types for the specified chunk. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param priority New priority. ++ * ++ * @see #setPriority(ServerLevel, int, int, Priority) ++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, ++ final PrioritisedExecutor.Priority priority) { ++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { ++ RegionFileIOThread.raisePriority(world, chunkX, chunkZ, type, priority); ++ } ++ } ++ ++ /** ++ * Raises the priority for the specified regionfile type for the specified chunk. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * @param priority New priority. ++ * ++ * @see #setPriority(ServerLevel, int, int, Priority) ++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, Priority) ++ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final PrioritisedExecutor.Priority priority) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ thread.raisePriorityInternal(world, chunkX, chunkZ, type, priority); ++ } ++ ++ void raisePriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final PrioritisedExecutor.Priority priority) { ++ final ChunkDataController taskController = this.getControllerFor(world, type); ++ final ChunkDataTask task = taskController.tasks.get(Long.valueOf(CoordinateUtils.getChunkKey(chunkX, chunkZ))); ++ ++ if (task != null) { ++ task.prioritisedTask.raisePriority(priority); ++ } ++ } ++ ++ /** ++ * Lowers the priority for all regionfile types for the specified chunk. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param priority New priority. ++ * ++ * @see #raisePriority(ServerLevel, int, int, Priority) ++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #setPriority(ServerLevel, int, int, Priority) ++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, ++ final PrioritisedExecutor.Priority priority) { ++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { ++ RegionFileIOThread.lowerPriority(world, chunkX, chunkZ, type, priority); ++ } ++ } ++ ++ /** ++ * Lowers the priority for the specified regionfile type for the specified chunk. ++ * ++ * @param world Specified world. ++ * @param chunkX Specified chunk x. ++ * @param chunkZ Specified chunk z. ++ * @param type Specified regionfile type. ++ * @param priority New priority. ++ * ++ * @see #raisePriority(ServerLevel, int, int, Priority) ++ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) ++ * @see #setPriority(ServerLevel, int, int, Priority) ++ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) ++ */ ++ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final PrioritisedExecutor.Priority priority) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ thread.lowerPriorityInternal(world, chunkX, chunkZ, type, priority); ++ } ++ ++ void lowerPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final PrioritisedExecutor.Priority priority) { ++ final ChunkDataController taskController = this.getControllerFor(world, type); ++ final ChunkDataTask task = taskController.tasks.get(Long.valueOf(CoordinateUtils.getChunkKey(chunkX, chunkZ))); ++ ++ if (task != null) { ++ task.prioritisedTask.lowerPriority(priority); ++ } ++ } ++ ++ /** ++ * Schedules the chunk data to be written asynchronously. ++ *

++ * Impl notes: ++ *

    ++ *
  • ++ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means ++ * saves must be scheduled before a chunk is unloaded. ++ *
  • ++ *
  • ++ * Writes may be called concurrently, although only the "later" write will go through. ++ *
  • ++ *
++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param data Chunk's data ++ * @param type The regionfile type to write to. ++ * ++ * @throws IllegalStateException If the file io thread has shutdown. ++ */ ++ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, ++ final RegionFileType type) { ++ RegionFileIOThread.scheduleSave(world, chunkX, chunkZ, data, type, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ /** ++ * Schedules the chunk data to be written asynchronously. ++ *

++ * Impl notes: ++ *

    ++ *
  • ++ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means ++ * saves must be scheduled before a chunk is unloaded. ++ *
  • ++ *
  • ++ * Writes may be called concurrently, although only the "later" write will go through. ++ *
  • ++ *
++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param data Chunk's data ++ * @param type The regionfile type to write to. ++ * @param priority The minimum priority to schedule at. ++ * ++ * @throws IllegalStateException If the file io thread has shutdown. ++ */ ++ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, ++ final RegionFileType type, final PrioritisedExecutor.Priority priority) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ thread.scheduleSaveInternal(world, chunkX, chunkZ, data, type, priority); ++ } ++ ++ void scheduleSaveInternal(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, ++ final RegionFileType type, final PrioritisedExecutor.Priority priority) { ++ final ChunkDataController taskController = this.getControllerFor(world, type); ++ ++ final boolean[] created = new boolean[1]; ++ final ChunkCoordinate key = new ChunkCoordinate(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ final ChunkDataTask task = taskController.tasks.compute(key, (final ChunkCoordinate keyInMap, final ChunkDataTask taskRunning) -> { ++ if (taskRunning == null || taskRunning.failedWrite) { ++ // no task is scheduled or the previous write failed - meaning we need to overwrite it ++ ++ // create task ++ final ChunkDataTask newTask = new ChunkDataTask(world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority); ++ newTask.inProgressWrite = data; ++ created[0] = true; ++ ++ return newTask; ++ } ++ ++ taskRunning.inProgressWrite = data; ++ ++ return taskRunning; ++ }); ++ ++ if (created[0]) { ++ task.prioritisedTask.queue(); ++ } else { ++ task.prioritisedTask.raisePriority(priority); ++ } ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call ++ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)} ++ * for single load. ++ *

++ * Impl notes: ++ *

    ++ *
  • ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ *
  • ++ *
++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) ++ */ ++ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Consumer onComplete, final boolean intendingToBlock) { ++ return RegionFileIOThread.loadAllChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call ++ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)} ++ * for single load. ++ *

++ * Impl notes: ++ *

    ++ *
  • ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ *
  • ++ *
++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * @param priority The minimum priority to load the data at. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) ++ */ ++ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Consumer onComplete, final boolean intendingToBlock, ++ final PrioritisedExecutor.Priority priority) { ++ return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, priority, CACHED_REGIONFILE_TYPES); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and ++ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)} ++ * for single load. ++ *

++ * Impl notes: ++ *

    ++ *
  • ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ *
  • ++ *
++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * @param types The regionfile type(s) to load. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) ++ */ ++ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Consumer onComplete, final boolean intendingToBlock, ++ final RegionFileType... types) { ++ return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, PrioritisedExecutor.Priority.NORMAL, types); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and ++ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)} ++ * for single load. ++ *

++ * Impl notes: ++ *

    ++ *
  • ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ *
  • ++ *
++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * @param types The regionfile type(s) to load. ++ * @param priority The minimum priority to load the data at. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) ++ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) ++ */ ++ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Consumer onComplete, final boolean intendingToBlock, ++ final PrioritisedExecutor.Priority priority, final RegionFileType... types) { ++ if (types == null) { ++ throw new NullPointerException("Types cannot be null"); ++ } ++ if (types.length == 0) { ++ throw new IllegalArgumentException("Types cannot be empty"); ++ } ++ ++ final RegionFileData ret = new RegionFileData(); ++ ++ final Cancellable[] reads = new CancellableRead[types.length]; ++ final AtomicInteger completions = new AtomicInteger(); ++ final int expectedCompletions = types.length; ++ ++ for (int i = 0; i < expectedCompletions; ++i) { ++ final RegionFileType type = types[i]; ++ reads[i] = RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, ++ (final CompoundTag data, final Throwable throwable) -> { ++ if (throwable != null) { ++ ret.setThrowable(type, throwable); ++ } else { ++ ret.setData(type, data); ++ } ++ ++ if (completions.incrementAndGet() == expectedCompletions) { ++ onComplete.accept(ret); ++ } ++ }, intendingToBlock, priority); ++ } ++ ++ return new CancellableReads(reads); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call ++ * {@code onComplete}. ++ *

++ * Impl notes: ++ *

    ++ *
  • ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ *
  • ++ *
++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) ++ */ ++ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ, ++ final RegionFileType type, final BiConsumer onComplete, ++ final boolean intendingToBlock) { ++ return RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, onComplete, intendingToBlock, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ /** ++ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call ++ * {@code onComplete}. ++ *

++ * Impl notes: ++ *

    ++ *
  • ++ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may ++ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of ++ * data is undefined behaviour, and can cause deadlock. ++ *
  • ++ *
++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param onComplete Consumer to execute once this task has completed ++ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost ++ * of this call. ++ * @param priority Minimum priority to load the data at. ++ * ++ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. ++ * ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) ++ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) ++ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) ++ */ ++ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ, ++ final RegionFileType type, final BiConsumer onComplete, ++ final boolean intendingToBlock, final PrioritisedExecutor.Priority priority) { ++ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); ++ return thread.loadDataAsyncInternal(world, chunkX, chunkZ, type, onComplete, intendingToBlock, priority); ++ } ++ ++ private static Boolean doesRegionFileExist(final int chunkX, final int chunkZ, final boolean intendingToBlock, ++ final ChunkDataController taskController) { ++ final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); ++ if (intendingToBlock) { ++ return taskController.computeForRegionFile(chunkX, chunkZ, true, (final RegionFile file) -> { ++ if (file == null) { // null if no regionfile exists ++ return Boolean.FALSE; ++ } ++ ++ return file.hasChunk(chunkPos) ? Boolean.TRUE : Boolean.FALSE; ++ }); ++ } else { ++ // first check if the region file for sure does not exist ++ if (taskController.doesRegionFileNotExist(chunkX, chunkZ)) { ++ return Boolean.FALSE; ++ } // else: it either exists or is not known, fall back to checking the loaded region file ++ ++ return taskController.computeForRegionFileIfLoaded(chunkX, chunkZ, (final RegionFile file) -> { ++ if (file == null) { // null if not loaded ++ // not sure at this point, let the I/O thread figure it out ++ return Boolean.TRUE; ++ } ++ ++ return file.hasChunk(chunkPos) ? Boolean.TRUE : Boolean.FALSE; ++ }); ++ } ++ } ++ ++ Cancellable loadDataAsyncInternal(final ServerLevel world, final int chunkX, final int chunkZ, ++ final RegionFileType type, final BiConsumer onComplete, ++ final boolean intendingToBlock, final PrioritisedExecutor.Priority priority) { ++ final ChunkDataController taskController = this.getControllerFor(world, type); ++ ++ final ImmediateCallbackCompletion callbackInfo = new ImmediateCallbackCompletion(); ++ ++ final ChunkCoordinate key = new ChunkCoordinate(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ final BiFunction compute = (final ChunkCoordinate keyInMap, final ChunkDataTask running) -> { ++ if (running == null) { ++ // not scheduled ++ ++ if (callbackInfo.regionFileCalculation == null) { ++ // caller will compute this outside of compute(), to avoid holding the bin lock ++ callbackInfo.needsRegionFileTest = true; ++ return null; ++ } ++ ++ if (callbackInfo.regionFileCalculation == Boolean.FALSE) { ++ // not on disk ++ callbackInfo.data = null; ++ callbackInfo.throwable = null; ++ callbackInfo.completeNow = true; ++ return null; ++ } ++ ++ // set up task ++ final ChunkDataTask newTask = new ChunkDataTask( ++ world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority ++ ); ++ newTask.inProgressRead = new RegionFileIOThread.InProgressRead(); ++ newTask.inProgressRead.waiters.add(onComplete); ++ ++ callbackInfo.tasksNeedsScheduling = true; ++ return newTask; ++ } ++ ++ final CompoundTag pendingWrite = running.inProgressWrite; ++ ++ if (pendingWrite == ChunkDataTask.NOTHING_TO_WRITE) { ++ // need to add to waiters here, because the regionfile thread will use compute() to lock and check for cancellations ++ if (!running.inProgressRead.addToWaiters(onComplete)) { ++ callbackInfo.data = running.inProgressRead.value; ++ callbackInfo.throwable = running.inProgressRead.throwable; ++ callbackInfo.completeNow = true; ++ } ++ return running; ++ } ++ // using the result sync here - don't bump priority ++ ++ // at this stage we have to use the in progress write's data to avoid an order issue ++ callbackInfo.data = pendingWrite; ++ callbackInfo.throwable = null; ++ callbackInfo.completeNow = true; ++ return running; ++ }; ++ ++ ChunkDataTask curr = taskController.tasks.get(key); ++ if (curr == null) { ++ callbackInfo.regionFileCalculation = doesRegionFileExist(chunkX, chunkZ, intendingToBlock, taskController); ++ } ++ ChunkDataTask ret = taskController.tasks.compute(key, compute); ++ if (callbackInfo.needsRegionFileTest) { ++ // curr isn't null but when we went into compute() it was ++ callbackInfo.regionFileCalculation = doesRegionFileExist(chunkX, chunkZ, intendingToBlock, taskController); ++ // now it should be fine ++ ret = taskController.tasks.compute(key, compute); ++ } ++ ++ // needs to be scheduled ++ if (callbackInfo.tasksNeedsScheduling) { ++ ret.prioritisedTask.queue(); ++ } else if (callbackInfo.completeNow) { ++ try { ++ onComplete.accept(callbackInfo.data, callbackInfo.throwable); ++ } catch (final ThreadDeath thr) { ++ throw thr; ++ } catch (final Throwable thr) { ++ LOGGER.error("Callback " + ConcurrentUtil.genericToString(onComplete) + " synchronously failed to handle chunk data for task " + ret.toString(), thr); ++ } ++ } else { ++ // we're waiting on a task we didn't schedule, so raise its priority to what we want ++ ret.prioritisedTask.raisePriority(priority); ++ } ++ ++ return new CancellableRead(onComplete, ret); ++ } ++ ++ /** ++ * Schedules a load task to be executed asynchronously, and blocks on that task. ++ * ++ * @param world Chunk's world ++ * @param chunkX Chunk's x coordinate ++ * @param chunkZ Chunk's z coordinate ++ * @param type Regionfile type ++ * @param priority Minimum priority to load the data at. ++ * ++ * @return The chunk data for the chunk. Note that a {@code null} result means the chunk or regionfile does not exist on disk. ++ * ++ * @throws IOException If the load fails for any reason ++ */ ++ public static CompoundTag loadData(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, ++ final PrioritisedExecutor.Priority priority) throws IOException { ++ final CompletableFuture ret = new CompletableFuture<>(); ++ ++ RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, (final CompoundTag compound, final Throwable thr) -> { ++ if (thr != null) { ++ ret.completeExceptionally(thr); ++ } else { ++ ret.complete(compound); ++ } ++ }, true, priority); ++ ++ try { ++ return ret.join(); ++ } catch (final CompletionException ex) { ++ throw new IOException(ex); ++ } ++ } ++ ++ private static final class ImmediateCallbackCompletion { ++ ++ public CompoundTag data; ++ public Throwable throwable; ++ public boolean completeNow; ++ public boolean tasksNeedsScheduling; ++ public boolean needsRegionFileTest; ++ public Boolean regionFileCalculation; ++ ++ } ++ ++ static final class CancellableRead implements Cancellable { ++ ++ private BiConsumer callback; ++ private RegionFileIOThread.ChunkDataTask task; ++ ++ CancellableRead(final BiConsumer callback, final RegionFileIOThread.ChunkDataTask task) { ++ this.callback = callback; ++ this.task = task; ++ } ++ ++ @Override ++ public boolean cancel() { ++ final BiConsumer callback = this.callback; ++ final RegionFileIOThread.ChunkDataTask task = this.task; ++ ++ if (callback == null || task == null) { ++ return false; ++ } ++ ++ this.callback = null; ++ this.task = null; ++ ++ final RegionFileIOThread.InProgressRead read = task.inProgressRead; ++ ++ // read can be null if no read was scheduled (i.e no regionfile existed or chunk in regionfile didn't) ++ return (read != null && read.waiters.remove(callback)); ++ } ++ } ++ ++ static final class CancellableReads implements Cancellable { ++ ++ private Cancellable[] reads; ++ ++ protected static final VarHandle READS_HANDLE = ConcurrentUtil.getVarHandle(CancellableReads.class, "reads", Cancellable[].class); ++ ++ CancellableReads(final Cancellable[] reads) { ++ this.reads = reads; ++ } ++ ++ @Override ++ public boolean cancel() { ++ final Cancellable[] reads = (Cancellable[])READS_HANDLE.getAndSet((CancellableReads)this, (Cancellable[])null); ++ ++ if (reads == null) { ++ return false; ++ } ++ ++ boolean ret = false; ++ ++ for (final Cancellable read : reads) { ++ ret |= read.cancel(); ++ } ++ ++ return ret; ++ } ++ } ++ ++ static final class InProgressRead { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ CompoundTag value; ++ Throwable throwable; ++ final MultiThreadedQueue> waiters = new MultiThreadedQueue<>(); ++ ++ // rets false if already completed (callback not invoked), true if callback was added ++ boolean addToWaiters(final BiConsumer callback) { ++ return this.waiters.add(callback); ++ } ++ ++ void complete(final RegionFileIOThread.ChunkDataTask task, final CompoundTag value, final Throwable throwable) { ++ this.value = value; ++ this.throwable = throwable; ++ ++ BiConsumer consumer; ++ while ((consumer = this.waiters.pollOrBlockAdds()) != null) { ++ try { ++ consumer.accept(value, throwable); ++ } catch (final ThreadDeath thr) { ++ throw thr; ++ } catch (final Throwable thr) { ++ LOGGER.error("Callback " + ConcurrentUtil.genericToString(consumer) + " failed to handle chunk data for task " + task.toString(), thr); ++ } ++ } ++ } ++ } ++ ++ /** ++ * Class exists to replace {@link Long} usages as keys inside non-fastutil hashtables. The hash for some Long {@code x} ++ * is defined as {@code (x >>> 32) ^ x}. Chunk keys as long values are defined as {@code ((chunkX & 0xFFFFFFFFL) | (chunkZ << 32))}, ++ * which means the hashcode as a Long value will be {@code chunkX ^ chunkZ}. Given that most chunks are created within a radius arounds players, ++ * this will lead to many hash collisions. So, this class uses a better hashing algorithm so that usage of ++ * non-fastutil collections is not degraded. ++ */ ++ public static final class ChunkCoordinate implements Comparable { ++ ++ public final long key; ++ ++ public ChunkCoordinate(final long key) { ++ this.key = key; ++ } ++ ++ @Override ++ public int hashCode() { ++ return (int)HashCommon.mix(this.key); ++ } ++ ++ @Override ++ public boolean equals(final Object obj) { ++ if (this == obj) { ++ return true; ++ } ++ ++ if (!(obj instanceof ChunkCoordinate)) { ++ return false; ++ } ++ ++ final ChunkCoordinate other = (ChunkCoordinate)obj; ++ ++ return this.key == other.key; ++ } ++ ++ // This class is intended for HashMap/ConcurrentHashMap usage, which do treeify bin nodes if the chain ++ // is too large. So we should implement compareTo to help. ++ @Override ++ public int compareTo(final RegionFileIOThread.ChunkCoordinate other) { ++ return Long.compare(this.key, other.key); ++ } ++ ++ @Override ++ public String toString() { ++ return new ChunkPos(this.key).toString(); ++ } ++ } ++ ++ public static abstract class ChunkDataController { ++ ++ // ConcurrentHashMap synchronizes per chain, so reduce the chance of task's hashes colliding. ++ protected final ConcurrentHashMap tasks = new ConcurrentHashMap<>(8192, 0.10f); ++ ++ public final RegionFileType type; ++ ++ public ChunkDataController(final RegionFileType type) { ++ this.type = type; ++ } ++ ++ public abstract RegionFileStorage getCache(); ++ ++ public abstract void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException; ++ ++ public abstract CompoundTag readData(final int chunkX, final int chunkZ) throws IOException; ++ ++ public boolean hasTasks() { ++ return !this.tasks.isEmpty(); ++ } ++ ++ public boolean doesRegionFileNotExist(final int chunkX, final int chunkZ) { ++ return this.getCache().doesRegionFileNotExistNoIO(new ChunkPos(chunkX, chunkZ)); ++ } ++ ++ public T computeForRegionFile(final int chunkX, final int chunkZ, final boolean existingOnly, final Function function) { ++ final RegionFileStorage cache = this.getCache(); ++ final RegionFile regionFile; ++ synchronized (cache) { ++ try { ++ regionFile = cache.getRegionFile(new ChunkPos(chunkX, chunkZ), existingOnly, true); ++ } catch (final IOException ex) { ++ throw new RuntimeException(ex); ++ } ++ } ++ ++ try { ++ return function.apply(regionFile); ++ } finally { ++ if (regionFile != null) { ++ regionFile.fileLock.unlock(); ++ } ++ } ++ } ++ ++ public T computeForRegionFileIfLoaded(final int chunkX, final int chunkZ, final Function function) { ++ final RegionFileStorage cache = this.getCache(); ++ final RegionFile regionFile; ++ ++ synchronized (cache) { ++ regionFile = cache.getRegionFileIfLoaded(new ChunkPos(chunkX, chunkZ)); ++ if (regionFile != null) { ++ regionFile.fileLock.lock(); ++ } ++ } ++ ++ try { ++ return function.apply(regionFile); ++ } finally { ++ if (regionFile != null) { ++ regionFile.fileLock.unlock(); ++ } ++ } ++ } ++ } ++ ++ static final class ChunkDataTask implements Runnable { ++ ++ protected static final CompoundTag NOTHING_TO_WRITE = new CompoundTag(); ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ RegionFileIOThread.InProgressRead inProgressRead; ++ volatile CompoundTag inProgressWrite = NOTHING_TO_WRITE; // only needs to be acquire/release ++ ++ boolean failedWrite; ++ ++ final ServerLevel world; ++ final int chunkX; ++ final int chunkZ; ++ final RegionFileIOThread.ChunkDataController taskController; ++ ++ final PrioritisedExecutor.PrioritisedTask prioritisedTask; ++ ++ /* ++ * IO thread will perform reads before writes for a given chunk x and z ++ * ++ * How reads/writes are scheduled: ++ * ++ * If read is scheduled while scheduling write, take no special action and just schedule write ++ * If read is scheduled while scheduling read and no write is scheduled, chain the read task ++ * ++ * ++ * If write is scheduled while scheduling read, use the pending write data and ret immediately (so no read is scheduled) ++ * If write is scheduled while scheduling write (ignore read in progress), overwrite the write in progress data ++ * ++ * This allows the reads and writes to act as if they occur synchronously to the thread scheduling them, however ++ * it fails to properly propagate write failures thanks to writes overwriting each other ++ */ ++ ++ public ChunkDataTask(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileIOThread.ChunkDataController taskController, ++ final PrioritisedExecutor executor, final PrioritisedExecutor.Priority priority) { ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.taskController = taskController; ++ this.prioritisedTask = executor.createTask(this, priority); ++ } ++ ++ @Override ++ public String toString() { ++ return "Task for world: '" + this.world.getWorld().getName() + "' at (" + this.chunkX + "," + this.chunkZ + ++ ") type: " + this.taskController.type.name() + ", hash: " + this.hashCode(); ++ } ++ ++ @Override ++ public void run() { ++ final RegionFileIOThread.InProgressRead read = this.inProgressRead; ++ final ChunkCoordinate chunkKey = new ChunkCoordinate(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ)); ++ ++ if (read != null) { ++ final boolean[] canRead = new boolean[] { true }; ++ ++ if (read.waiters.isEmpty()) { ++ // cancelled read? go to task controller to confirm ++ final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final ChunkCoordinate keyInMap, final ChunkDataTask valueInMap) -> { ++ if (valueInMap == null) { ++ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!"); ++ } ++ if (valueInMap != ChunkDataTask.this) { ++ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); ++ } ++ ++ if (!read.waiters.isEmpty()) { // as per usual IntelliJ is unable to figure out that there are concurrent accesses. ++ return valueInMap; ++ } else { ++ canRead[0] = false; ++ } ++ ++ return valueInMap.inProgressWrite == NOTHING_TO_WRITE ? null : valueInMap; ++ }); ++ ++ if (inMap == null) { ++ // read is cancelled - and no write pending, so we're done ++ return; ++ } ++ // if there is a write in progress, we don't actually have to worry about waiters gaining new entries - ++ // the readers will just use the in progress write, so the value in canRead is good to use without ++ // further synchronisation. ++ } ++ ++ if (canRead[0]) { ++ CompoundTag compound = null; ++ Throwable throwable = null; ++ ++ try { ++ compound = this.taskController.readData(this.chunkX, this.chunkZ); ++ } catch (final ThreadDeath thr) { ++ throw thr; ++ } catch (final Throwable thr) { ++ throwable = thr; ++ LOGGER.error("Failed to read chunk data for task: " + this.toString(), thr); ++ } ++ read.complete(this, compound, throwable); ++ } ++ } ++ ++ CompoundTag write = this.inProgressWrite; ++ ++ if (write == NOTHING_TO_WRITE) { ++ final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final ChunkCoordinate keyInMap, final ChunkDataTask valueInMap) -> { ++ if (valueInMap == null) { ++ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!"); ++ } ++ if (valueInMap != ChunkDataTask.this) { ++ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); ++ } ++ return valueInMap.inProgressWrite == NOTHING_TO_WRITE ? null : valueInMap; ++ }); ++ ++ if (inMap == null) { ++ return; // set the task value to null, indicating we're done ++ } // else: inProgressWrite changed, so now we have something to write ++ } ++ ++ for (;;) { ++ write = this.inProgressWrite; ++ final CompoundTag dataWritten = write; ++ ++ boolean failedWrite = false; ++ ++ try { ++ this.taskController.writeData(this.chunkX, this.chunkZ, write); ++ } catch (final ThreadDeath thr) { ++ throw thr; ++ } catch (final Throwable thr) { ++ if (thr instanceof RegionFileStorage.RegionFileSizeException) { ++ final int maxSize = RegionFile.MAX_CHUNK_SIZE / (1024 * 1024); ++ LOGGER.error("Chunk at (" + this.chunkX + "," + this.chunkZ + ") in '" + this.world.getWorld().getName() + "' exceeds max size of " + maxSize + "MiB, it has been deleted from disk."); ++ } else { ++ failedWrite = thr instanceof IOException; ++ LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr); ++ } ++ } ++ ++ final boolean finalFailWrite = failedWrite; ++ final boolean[] done = new boolean[] { false }; ++ ++ this.taskController.tasks.compute(chunkKey, (final ChunkCoordinate keyInMap, final ChunkDataTask valueInMap) -> { ++ if (valueInMap == null) { ++ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!"); ++ } ++ if (valueInMap != ChunkDataTask.this) { ++ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); ++ } ++ if (valueInMap.inProgressWrite == dataWritten) { ++ valueInMap.failedWrite = finalFailWrite; ++ done[0] = true; ++ // keep the data in map if we failed the write so we can try to prevent data loss ++ return finalFailWrite ? valueInMap : null; ++ } ++ // different data than expected, means we need to retry write ++ return valueInMap; ++ }); ++ ++ if (done[0]) { ++ return; ++ } ++ ++ // fetch & write new data ++ continue; ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/light/LightQueue.java b/src/main/java/io/papermc/paper/chunk/system/light/LightQueue.java +new file mode 100644 +index 0000000000000000000000000000000000000000..69e9944358951bd69ff5e8b3482da1a5e4476209 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/light/LightQueue.java +@@ -0,0 +1,283 @@ ++package io.papermc.paper.chunk.system.light; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.starlight.common.light.BlockStarLightEngine; ++import ca.spottedleaf.starlight.common.light.SkyStarLightEngine; ++import ca.spottedleaf.starlight.common.light.StarLightInterface; ++import io.papermc.paper.util.CoordinateUtils; ++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.shorts.ShortCollection; ++import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.SectionPos; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import java.util.ArrayList; ++import java.util.HashSet; ++import java.util.List; ++import java.util.Set; ++import java.util.concurrent.CompletableFuture; ++import java.util.function.BooleanSupplier; ++ ++public final class LightQueue { ++ ++ protected final Long2ObjectOpenHashMap chunkTasks = new Long2ObjectOpenHashMap<>(); ++ protected final StarLightInterface manager; ++ protected final ServerLevel world; ++ ++ public LightQueue(final StarLightInterface manager) { ++ this.manager = manager; ++ this.world = ((ServerLevel)manager.getWorld()); ++ } ++ ++ public void lowerPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ final ChunkTasks task; ++ synchronized (this) { ++ task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ if (task != null) { ++ task.lowerPriority(priority); ++ } ++ } ++ ++ public void setPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ final ChunkTasks task; ++ synchronized (this) { ++ task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ if (task != null) { ++ task.setPriority(priority); ++ } ++ } ++ ++ public void raisePriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ final ChunkTasks task; ++ synchronized (this) { ++ task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ if (task != null) { ++ task.raisePriority(priority); ++ } ++ } ++ ++ public PrioritisedExecutor.Priority getPriority(final int chunkX, final int chunkZ) { ++ final ChunkTasks task; ++ synchronized (this) { ++ task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ if (task != null) { ++ return task.getPriority(); ++ } ++ ++ return PrioritisedExecutor.Priority.COMPLETING; ++ } ++ ++ public boolean isEmpty() { ++ synchronized (this) { ++ return this.chunkTasks.isEmpty(); ++ } ++ } ++ ++ public ChunkTasks queueBlockChange(final BlockPos pos) { ++ final ChunkTasks tasks; ++ synchronized (this) { ++ tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { ++ return new ChunkTasks(keyInMap, LightQueue.this.manager, LightQueue.this); ++ }); ++ tasks.changedPositions.add(pos.immutable()); ++ } ++ ++ tasks.schedule(); ++ ++ return tasks; ++ } ++ ++ public ChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) { ++ final ChunkTasks tasks; ++ synchronized (this) { ++ tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { ++ return new ChunkTasks(keyInMap, LightQueue.this.manager, LightQueue.this); ++ }); ++ ++ if (tasks.changedSectionSet == null) { ++ tasks.changedSectionSet = new Boolean[this.manager.maxSection - this.manager.minSection + 1]; ++ } ++ tasks.changedSectionSet[pos.getY() - this.manager.minSection] = Boolean.valueOf(newEmptyValue); ++ } ++ ++ tasks.schedule(); ++ ++ return tasks; ++ } ++ ++ public ChunkTasks queueChunkLightTask(final ChunkPos pos, final BooleanSupplier lightTask, final PrioritisedExecutor.Priority priority) { ++ final ChunkTasks tasks; ++ synchronized (this) { ++ tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { ++ return new ChunkTasks(keyInMap, LightQueue.this.manager, LightQueue.this, priority); ++ }); ++ if (tasks.lightTasks == null) { ++ tasks.lightTasks = new ArrayList<>(); ++ } ++ tasks.lightTasks.add(lightTask); ++ } ++ ++ tasks.schedule(); ++ ++ return tasks; ++ } ++ ++ public ChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) { ++ final ChunkTasks tasks; ++ synchronized (this) { ++ tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { ++ return new ChunkTasks(keyInMap, LightQueue.this.manager, LightQueue.this); ++ }); ++ ++ ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksSky; ++ if (queuedEdges == null) { ++ queuedEdges = tasks.queuedEdgeChecksSky = new ShortOpenHashSet(); ++ } ++ queuedEdges.addAll(sections); ++ } ++ ++ tasks.schedule(); ++ ++ return tasks; ++ } ++ ++ public ChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) { ++ final ChunkTasks tasks; ++ ++ synchronized (this) { ++ tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { ++ return new ChunkTasks(keyInMap, LightQueue.this.manager, LightQueue.this); ++ }); ++ ++ ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksBlock; ++ if (queuedEdges == null) { ++ queuedEdges = tasks.queuedEdgeChecksBlock = new ShortOpenHashSet(); ++ } ++ queuedEdges.addAll(sections); ++ } ++ ++ tasks.schedule(); ++ ++ return tasks; ++ } ++ ++ public void removeChunk(final ChunkPos pos) { ++ final ChunkTasks tasks; ++ synchronized (this) { ++ tasks = this.chunkTasks.remove(CoordinateUtils.getChunkKey(pos)); ++ } ++ if (tasks != null && tasks.cancel()) { ++ tasks.onComplete.complete(null); ++ } ++ } ++ ++ public static final class ChunkTasks implements Runnable { ++ ++ public final CompletableFuture onComplete = new CompletableFuture<>(); ++ public boolean isTicketAdded; ++ public final long chunkCoordinate; ++ ++ private final StarLightInterface lightEngine; ++ private final LightQueue queue; ++ private final PrioritisedExecutor.PrioritisedTask task; ++ private final Set changedPositions = new HashSet<>(); ++ private Boolean[] changedSectionSet; ++ private ShortOpenHashSet queuedEdgeChecksSky; ++ private ShortOpenHashSet queuedEdgeChecksBlock; ++ private List lightTasks; ++ ++ public ChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final LightQueue queue) { ++ this(chunkCoordinate, lightEngine, queue, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ public ChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final LightQueue queue, ++ final PrioritisedExecutor.Priority priority) { ++ this.chunkCoordinate = chunkCoordinate; ++ this.lightEngine = lightEngine; ++ this.queue = queue; ++ this.task = queue.world.chunkTaskScheduler.radiusAwareScheduler.createTask( ++ CoordinateUtils.getChunkX(chunkCoordinate), CoordinateUtils.getChunkZ(chunkCoordinate), ++ ChunkStatus.LIGHT.writeRadius, this, priority ++ ); ++ } ++ ++ public void schedule() { ++ this.task.queue(); ++ } ++ ++ public boolean cancel() { ++ return this.task.cancel(); ++ } ++ ++ public PrioritisedExecutor.Priority getPriority() { ++ return this.task.getPriority(); ++ } ++ ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ this.task.lowerPriority(priority); ++ } ++ ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ this.task.setPriority(priority); ++ } ++ ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ this.task.raisePriority(priority); ++ } ++ ++ @Override ++ public void run() { ++ synchronized (this.queue) { ++ this.queue.chunkTasks.remove(this.chunkCoordinate); ++ } ++ ++ boolean litChunk = false; ++ if (this.lightTasks != null) { ++ for (final BooleanSupplier run : this.lightTasks) { ++ if (run.getAsBoolean()) { ++ litChunk = true; ++ break; ++ } ++ } ++ } ++ ++ final SkyStarLightEngine skyEngine = this.lightEngine.getSkyLightEngine(); ++ final BlockStarLightEngine blockEngine = this.lightEngine.getBlockLightEngine(); ++ try { ++ final long coordinate = this.chunkCoordinate; ++ final int chunkX = CoordinateUtils.getChunkX(coordinate); ++ final int chunkZ = CoordinateUtils.getChunkZ(coordinate); ++ ++ final Set positions = this.changedPositions; ++ final Boolean[] sectionChanges = this.changedSectionSet; ++ ++ if (!litChunk) { ++ if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) { ++ skyEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges); ++ } ++ if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) { ++ blockEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges); ++ } ++ ++ if (skyEngine != null && this.queuedEdgeChecksSky != null) { ++ skyEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksSky); ++ } ++ if (blockEngine != null && this.queuedEdgeChecksBlock != null) { ++ blockEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksBlock); ++ } ++ } ++ ++ this.onComplete.complete(null); ++ } finally { ++ this.lightEngine.releaseSkyLightEngine(skyEngine); ++ this.lightEngine.releaseBlockLightEngine(blockEngine); ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/poi/PoiChunk.java b/src/main/java/io/papermc/paper/chunk/system/poi/PoiChunk.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d72041aa814ff179e6e29a45dcd359a91d426d47 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/poi/PoiChunk.java +@@ -0,0 +1,213 @@ ++package io.papermc.paper.chunk.system.poi; ++ ++import com.mojang.logging.LogUtils; ++import com.mojang.serialization.Codec; ++import com.mojang.serialization.DataResult; ++import io.papermc.paper.util.CoordinateUtils; ++import io.papermc.paper.util.TickThread; ++import io.papermc.paper.util.WorldUtil; ++import net.minecraft.SharedConstants; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.nbt.NbtOps; ++import net.minecraft.nbt.Tag; ++import net.minecraft.resources.RegistryOps; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.entity.ai.village.poi.PoiManager; ++import net.minecraft.world.entity.ai.village.poi.PoiSection; ++import org.slf4j.Logger; ++ ++import java.util.Optional; ++ ++public final class PoiChunk { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ public final ServerLevel world; ++ public final int chunkX; ++ public final int chunkZ; ++ public final int minSection; ++ public final int maxSection; ++ ++ protected final PoiSection[] sections; ++ ++ private boolean isDirty; ++ private boolean loaded; ++ ++ public PoiChunk(final ServerLevel world, final int chunkX, final int chunkZ, final int minSection, final int maxSection) { ++ this(world, chunkX, chunkZ, minSection, maxSection, new PoiSection[maxSection - minSection + 1]); ++ } ++ ++ public PoiChunk(final ServerLevel world, final int chunkX, final int chunkZ, final int minSection, final int maxSection, final PoiSection[] sections) { ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.minSection = minSection; ++ this.maxSection = maxSection; ++ this.sections = sections; ++ if (this.sections.length != (maxSection - minSection + 1)) { ++ throw new IllegalStateException("Incorrect length used, expected " + (maxSection - minSection + 1) + ", got " + this.sections.length); ++ } ++ } ++ ++ public void load() { ++ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Loading in poi chunk off-main"); ++ if (this.loaded) { ++ return; ++ } ++ this.loaded = true; ++ this.world.chunkSource.getPoiManager().loadInPoiChunk(this); ++ } ++ ++ public boolean isLoaded() { ++ return this.loaded; ++ } ++ ++ public boolean isEmpty() { ++ for (final PoiSection section : this.sections) { ++ if (section != null && !section.isEmpty()) { ++ return false; ++ } ++ } ++ ++ return true; ++ } ++ ++ public PoiSection getOrCreateSection(final int chunkY) { ++ if (chunkY >= this.minSection && chunkY <= this.maxSection) { ++ final int idx = chunkY - this.minSection; ++ final PoiSection ret = this.sections[idx]; ++ if (ret != null) { ++ return ret; ++ } ++ ++ final PoiManager poiManager = this.world.getPoiManager(); ++ final long key = CoordinateUtils.getChunkSectionKey(this.chunkX, chunkY, this.chunkZ); ++ ++ return this.sections[idx] = new PoiSection(() -> { ++ poiManager.setDirty(key); ++ }); ++ } ++ throw new IllegalArgumentException("chunkY is out of bounds, chunkY: " + chunkY + " outside [" + this.minSection + "," + this.maxSection + "]"); ++ } ++ ++ public PoiSection getSection(final int chunkY) { ++ if (chunkY >= this.minSection && chunkY <= this.maxSection) { ++ return this.sections[chunkY - this.minSection]; ++ } ++ return null; ++ } ++ ++ public Optional getSectionForVanilla(final int chunkY) { ++ if (chunkY >= this.minSection && chunkY <= this.maxSection) { ++ final PoiSection ret = this.sections[chunkY - this.minSection]; ++ return ret == null ? Optional.empty() : ret.noAllocateOptional; ++ } ++ return Optional.empty(); ++ } ++ ++ public boolean isDirty() { ++ return this.isDirty; ++ } ++ ++ public void setDirty(final boolean dirty) { ++ this.isDirty = dirty; ++ } ++ ++ // returns null if empty ++ public CompoundTag save() { ++ final RegistryOps registryOps = RegistryOps.create(NbtOps.INSTANCE, world.getPoiManager().registryAccess); ++ ++ final CompoundTag ret = new CompoundTag(); ++ final CompoundTag sections = new CompoundTag(); ++ ret.put("Sections", sections); ++ ++ ret.putInt("DataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion()); ++ ++ final ServerLevel world = this.world; ++ final PoiManager poiManager = world.getPoiManager(); ++ final int chunkX = this.chunkX; ++ final int chunkZ = this.chunkZ; ++ ++ for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) { ++ final PoiSection chunk = this.sections[sectionY - this.minSection]; ++ if (chunk == null || chunk.isEmpty()) { ++ continue; ++ } ++ ++ final long key = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ); ++ // codecs are honestly such a fucking disaster. What the fuck is this trash? ++ final Codec codec = PoiSection.codec(() -> { ++ poiManager.setDirty(key); ++ }); ++ ++ final DataResult serializedResult = codec.encodeStart(registryOps, chunk); ++ final int finalSectionY = sectionY; ++ final Tag serialized = serializedResult.resultOrPartial((final String description) -> { ++ LOGGER.error("Failed to serialize poi chunk for world: " + world.getWorld().getName() + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description); ++ }).orElse(null); ++ if (serialized == null) { ++ // failed, should be logged from the resultOrPartial ++ continue; ++ } ++ ++ sections.put(Integer.toString(sectionY), serialized); ++ } ++ ++ return sections.isEmpty() ? null : ret; ++ } ++ ++ public static PoiChunk empty(final ServerLevel world, final int chunkX, final int chunkZ) { ++ final PoiChunk ret = new PoiChunk(world, chunkX, chunkZ, WorldUtil.getMinSection(world), WorldUtil.getMaxSection(world)); ++ ret.loaded = true; ++ return ret; ++ } ++ ++ public static PoiChunk parse(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data) { ++ final PoiChunk ret = empty(world, chunkX, chunkZ); ++ ++ final RegistryOps registryOps = RegistryOps.create(NbtOps.INSTANCE, world.getPoiManager().registryAccess); ++ ++ final CompoundTag sections = data.getCompound("Sections"); ++ ++ if (sections.isEmpty()) { ++ // nothing to parse ++ return ret; ++ } ++ ++ final PoiManager poiManager = world.getPoiManager(); ++ ++ boolean readAnything = false; ++ ++ for (int sectionY = ret.minSection; sectionY <= ret.maxSection; ++sectionY) { ++ final String key = Integer.toString(sectionY); ++ if (!sections.contains(key)) { ++ continue; ++ } ++ ++ final long coordinateKey = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ); ++ // codecs are honestly such a fucking disaster. What the fuck is this trash? ++ final Codec codec = PoiSection.codec(() -> { ++ poiManager.setDirty(coordinateKey); ++ }); ++ ++ final CompoundTag section = sections.getCompound(key); ++ final DataResult deserializeResult = codec.parse(registryOps, section); ++ final int finalSectionY = sectionY; ++ final PoiSection deserialized = deserializeResult.resultOrPartial((final String description) -> { ++ LOGGER.error("Failed to deserialize poi chunk for world: " + world.getWorld().getName() + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description); ++ }).orElse(null); ++ ++ if (deserialized == null || deserialized.isEmpty()) { ++ // completely empty, no point in storing this ++ continue; ++ } ++ ++ readAnything = true; ++ ret.sections[sectionY - ret.minSection] = deserialized; ++ } ++ ++ ret.loaded = !readAnything; // Set loaded to false if we read anything to ensure proper callbacks to PoiManager are made on #load ++ ++ return ret; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkFullTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkFullTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c307b084f59f7bb94dc02f25bbcd3e01e01d2306 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkFullTask.java +@@ -0,0 +1,131 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.chunk.system.poi.PoiChunk; ++import net.minecraft.server.level.ChunkMap; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.chunk.ImposterProtoChunk; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.ProtoChunk; ++import org.slf4j.Logger; ++import java.lang.invoke.VarHandle; ++ ++public final class ChunkFullTask extends ChunkProgressionTask implements Runnable { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ protected final NewChunkHolder chunkHolder; ++ protected final ChunkAccess fromChunk; ++ protected final PrioritisedExecutor.PrioritisedTask convertToFullTask; ++ ++ public ChunkFullTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, ++ final NewChunkHolder chunkHolder, final ChunkAccess fromChunk, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ); ++ this.chunkHolder = chunkHolder; ++ this.fromChunk = fromChunk; ++ this.convertToFullTask = scheduler.createChunkTask(chunkX, chunkZ, this, priority); ++ } ++ ++ @Override ++ public ChunkStatus getTargetStatus() { ++ return ChunkStatus.FULL; ++ } ++ ++ @Override ++ public void run() { ++ // See Vanilla protoChunkToFullChunk for what this function should be doing ++ final LevelChunk chunk; ++ try { ++ // moved from the load from nbt stage into here ++ final PoiChunk poiChunk = this.chunkHolder.getPoiChunk(); ++ if (poiChunk == null) { ++ LOGGER.error("Expected poi chunk to be loaded with chunk for task " + this.toString()); ++ } else { ++ poiChunk.load(); ++ this.world.getPoiManager().checkConsistency(this.fromChunk); ++ } ++ ++ if (this.fromChunk instanceof ImposterProtoChunk wrappedFull) { ++ chunk = wrappedFull.getWrapped(); ++ } else { ++ final ServerLevel world = this.world; ++ final ProtoChunk protoChunk = (ProtoChunk)this.fromChunk; ++ chunk = new LevelChunk(this.world, protoChunk, (final LevelChunk unused) -> { ++ ChunkMap.postLoadProtoChunk(world, protoChunk.getEntities(), protoChunk.getPos()); // Paper - rewrite chunk system ++ }); ++ } ++ ++ chunk.setChunkHolder(this.scheduler.chunkHolderManager.getChunkHolder(this.chunkX, this.chunkZ)); // replaces setFullStatus ++ chunk.runPostLoad(); ++ // Unlike Vanilla, we load the entity chunk here, as we load the NBT in empty status (unlike Vanilla) ++ // This brings entity addition back in line with older versions of the game ++ // Since we load the NBT in the empty status, this will never block for I/O ++ this.world.chunkTaskScheduler.chunkHolderManager.getOrCreateEntityChunk(this.chunkX, this.chunkZ, false); ++ ++ // we don't need the entitiesInLevel trash, this system doesn't double run callbacks ++ chunk.setLoaded(true); ++ chunk.registerAllBlockEntitiesAfterLevelLoad(); ++ chunk.registerTickContainerInLevel(this.world); ++ } catch (final Throwable throwable) { ++ this.complete(null, throwable); ++ return; ++ } ++ this.complete(chunk, null); ++ } ++ ++ protected volatile boolean scheduled; ++ protected static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkFullTask.class, "scheduled", boolean.class); ++ ++ @Override ++ public boolean isScheduled() { ++ return this.scheduled; ++ } ++ ++ @Override ++ public void schedule() { ++ if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkFullTask)this, true)) { ++ throw new IllegalStateException("Cannot double call schedule()"); ++ } ++ this.convertToFullTask.queue(); ++ } ++ ++ @Override ++ public void cancel() { ++ if (this.convertToFullTask.cancel()) { ++ this.complete(null, null); ++ } ++ } ++ ++ @Override ++ public PrioritisedExecutor.Priority getPriority() { ++ return this.convertToFullTask.getPriority(); ++ } ++ ++ @Override ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.convertToFullTask.lowerPriority(priority); ++ } ++ ++ @Override ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.convertToFullTask.setPriority(priority); ++ } ++ ++ @Override ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.convertToFullTask.raisePriority(priority); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e251bc4c4 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java +@@ -0,0 +1,1500 @@ ++package io.papermc.paper.chunk.system.scheduling; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; ++import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable; ++import com.google.common.collect.ImmutableList; ++import com.google.gson.JsonArray; ++import com.google.gson.JsonObject; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader; ++import io.papermc.paper.chunk.system.io.RegionFileIOThread; ++import io.papermc.paper.chunk.system.poi.PoiChunk; ++import io.papermc.paper.threadedregions.TickRegions; ++import io.papermc.paper.util.CoordinateUtils; ++import io.papermc.paper.util.TickThread; ++import io.papermc.paper.world.ChunkEntitySlices; ++import it.unimi.dsi.fastutil.longs.Long2ByteLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.longs.Long2ByteMap; ++import it.unimi.dsi.fastutil.longs.Long2IntLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.longs.Long2IntMap; ++import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; ++import it.unimi.dsi.fastutil.longs.Long2ObjectMap; ++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.longs.LongArrayList; ++import it.unimi.dsi.fastutil.longs.LongIterator; ++import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet; ++import net.minecraft.nbt.CompoundTag; ++import io.papermc.paper.chunk.system.ChunkSystem; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.ChunkLevel; ++import net.minecraft.server.level.FullChunkStatus; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.Ticket; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.util.SortedArraySet; ++import net.minecraft.util.Unit; ++import net.minecraft.world.level.ChunkPos; ++import org.bukkit.plugin.Plugin; ++import org.slf4j.Logger; ++import java.io.IOException; ++import java.text.DecimalFormat; ++import java.util.ArrayDeque; ++import java.util.ArrayList; ++import java.util.Collection; ++import java.util.Collections; ++import java.util.Iterator; ++import java.util.List; ++import java.util.concurrent.ConcurrentHashMap; ++import java.util.concurrent.TimeUnit; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.concurrent.atomic.AtomicLong; ++import java.util.concurrent.atomic.AtomicReference; ++import java.util.concurrent.locks.LockSupport; ++import java.util.function.Predicate; ++ ++public final class ChunkHolderManager { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ public static final int FULL_LOADED_TICKET_LEVEL = 33; ++ public static final int BLOCK_TICKING_TICKET_LEVEL = 32; ++ public static final int ENTITY_TICKING_TICKET_LEVEL = 31; ++ public static final int MAX_TICKET_LEVEL = ChunkLevel.MAX_LEVEL; // inclusive ++ ++ private static final long NO_TIMEOUT_MARKER = Long.MIN_VALUE; ++ private static final long PROBE_MARKER = Long.MIN_VALUE + 1; ++ public final ReentrantAreaLock ticketLockArea; ++ ++ private final ConcurrentHashMap>> tickets = new java.util.concurrent.ConcurrentHashMap<>(); ++ private final ConcurrentHashMap sectionToChunkToExpireCount = new java.util.concurrent.ConcurrentHashMap<>(); ++ final ChunkQueue unloadQueue; ++ ++ public boolean processTicketUpdates(final int posX, final int posZ) { ++ final int ticketShift = ThreadedTicketLevelPropagator.SECTION_SHIFT; ++ final int ticketMask = (1 << ticketShift) - 1; ++ final List scheduledTasks = new ArrayList<>(); ++ final List changedFullStatus = new ArrayList<>(); ++ final boolean ret; ++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( ++ ((posX >> ticketShift) - 1) << ticketShift, ++ ((posZ >> ticketShift) - 1) << ticketShift, ++ (((posX >> ticketShift) + 1) << ticketShift) | ticketMask, ++ (((posZ >> ticketShift) + 1) << ticketShift) | ticketMask ++ ); ++ try { ++ ret = this.processTicketUpdatesNoLock(posX >> ticketShift, posZ >> ticketShift, scheduledTasks, changedFullStatus); ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ ++ this.addChangedStatuses(changedFullStatus); ++ ++ for (int i = 0, len = scheduledTasks.size(); i < len; ++i) { ++ scheduledTasks.get(i).schedule(); ++ } ++ ++ return ret; ++ } ++ ++ private boolean processTicketUpdatesNoLock(final int sectionX, final int sectionZ, final List scheduledTasks, ++ final List changedFullStatus) { ++ return this.ticketLevelPropagator.performUpdate( ++ sectionX, sectionZ, this.taskScheduler.schedulingLockArea, scheduledTasks, changedFullStatus ++ ); ++ } ++ ++ private final SWMRLong2ObjectHashTable chunkHolders = new SWMRLong2ObjectHashTable<>(16384, 0.25f); ++ // what a disaster of a name ++ // this is a map of removal tick to a map of chunks and the number of tickets a chunk has that are to expire that tick ++ private final Long2ObjectOpenHashMap removeTickToChunkExpireTicketCount = new Long2ObjectOpenHashMap<>(); ++ private final ServerLevel world; ++ private final ChunkTaskScheduler taskScheduler; ++ private long currentTick; ++ ++ private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>(); ++ private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { ++ if (c1 == c2) { ++ return 0; ++ } ++ ++ final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); ++ ++ if (saveTickCompare != 0) { ++ return saveTickCompare; ++ } ++ ++ final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); ++ final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); ++ ++ if (coord1 == coord2) { ++ throw new IllegalStateException("Duplicate chunkholder in auto save queue"); ++ } ++ ++ return Long.compare(coord1, coord2); ++ }); ++ ++ public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) { ++ this.world = world; ++ this.taskScheduler = taskScheduler; ++ this.ticketLockArea = new ReentrantAreaLock(taskScheduler.getChunkSystemLockShift()); ++ this.unloadQueue = new ChunkQueue(world.getRegionChunkShift()); ++ } ++ ++ private final AtomicLong statusUpgradeId = new AtomicLong(); ++ ++ long getNextStatusUpgradeId() { ++ return this.statusUpgradeId.incrementAndGet(); ++ } ++ ++ public List getOldChunkHolders() { ++ final List holders = this.getChunkHolders(); ++ final List ret = new ArrayList<>(holders.size()); ++ for (final NewChunkHolder holder : holders) { ++ ret.add(holder.vanillaChunkHolder); ++ } ++ return ret; ++ } ++ ++ public List getChunkHolders() { ++ final List ret = new ArrayList<>(this.chunkHolders.size()); ++ this.chunkHolders.forEachValue(ret::add); ++ return ret; ++ } ++ ++ public int size() { ++ return this.chunkHolders.size(); ++ } ++ ++ public void close(final boolean save, final boolean halt) { ++ TickThread.ensureTickThread("Closing world off-main"); ++ if (halt) { ++ LOGGER.info("Waiting 60s for chunk system to halt for world '" + this.world.getWorld().getName() + "'"); ++ if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) { ++ LOGGER.warn("Failed to halt world generation/loading tasks for world '" + this.world.getWorld().getName() + "'"); ++ } else { ++ LOGGER.info("Halted chunk system for world '" + this.world.getWorld().getName() + "'"); ++ } ++ } ++ ++ if (save) { ++ this.saveAllChunks(true, true, true); ++ } ++ ++ if (this.world.chunkDataControllerNew.hasTasks() || this.world.entityDataControllerNew.hasTasks() || this.world.poiDataControllerNew.hasTasks()) { ++ RegionFileIOThread.flush(); ++ } ++ ++ // kill regionfile cache ++ try { ++ this.world.chunkDataControllerNew.getCache().close(); ++ } catch (final IOException ex) { ++ LOGGER.error("Failed to close chunk regionfile cache for world '" + this.world.getWorld().getName() + "'", ex); ++ } ++ try { ++ this.world.entityDataControllerNew.getCache().close(); ++ } catch (final IOException ex) { ++ LOGGER.error("Failed to close entity regionfile cache for world '" + this.world.getWorld().getName() + "'", ex); ++ } ++ try { ++ this.world.poiDataControllerNew.getCache().close(); ++ } catch (final IOException ex) { ++ LOGGER.error("Failed to close poi regionfile cache for world '" + this.world.getWorld().getName() + "'", ex); ++ } ++ } ++ ++ void ensureInAutosave(final NewChunkHolder holder) { ++ if (!this.autoSaveQueue.contains(holder)) { ++ holder.lastAutoSave = MinecraftServer.currentTick; ++ this.autoSaveQueue.add(holder); ++ } ++ } ++ ++ public void autoSave() { ++ final List reschedule = new ArrayList<>(); ++ final long currentTick = MinecraftServer.currentTickLong; ++ final long maxSaveTime = currentTick - this.world.paperConfig().chunks.autoSaveInterval.value(); ++ for (int autoSaved = 0; autoSaved < this.world.paperConfig().chunks.maxAutoSaveChunksPerTick && !this.autoSaveQueue.isEmpty();) { ++ final NewChunkHolder holder = this.autoSaveQueue.first(); ++ ++ if (holder.lastAutoSave > maxSaveTime) { ++ break; ++ } ++ ++ this.autoSaveQueue.remove(holder); ++ ++ holder.lastAutoSave = currentTick; ++ if (holder.save(false, false) != null) { ++ ++autoSaved; ++ } ++ ++ if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) { ++ reschedule.add(holder); ++ } ++ } ++ ++ for (final NewChunkHolder holder : reschedule) { ++ if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) { ++ this.autoSaveQueue.add(holder); ++ } ++ } ++ } ++ ++ public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) { ++ final List holders = this.getChunkHolders(); ++ ++ if (logProgress) { ++ LOGGER.info("Saving all chunkholders for world '" + this.world.getWorld().getName() + "'"); ++ } ++ ++ final DecimalFormat format = new DecimalFormat("#0.00"); ++ ++ int saved = 0; ++ ++ long start = System.nanoTime(); ++ long lastLog = start; ++ boolean needsFlush = false; ++ final int flushInterval = 50; ++ ++ int savedChunk = 0; ++ int savedEntity = 0; ++ int savedPoi = 0; ++ ++ for (int i = 0, len = holders.size(); i < len; ++i) { ++ final NewChunkHolder holder = holders.get(i); ++ try { ++ final NewChunkHolder.SaveStat saveStat = holder.save(shutdown, false); ++ if (saveStat != null) { ++ ++saved; ++ needsFlush = flush; ++ if (saveStat.savedChunk()) { ++ ++savedChunk; ++ } ++ if (saveStat.savedEntityChunk()) { ++ ++savedEntity; ++ } ++ if (saveStat.savedPoiChunk()) { ++ ++savedPoi; ++ } ++ } ++ } catch (final ThreadDeath thr) { ++ throw thr; ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to save chunk (" + holder.chunkX + "," + holder.chunkZ + ") in world '" + this.world.getWorld().getName() + "'", thr); ++ } ++ if (needsFlush && (saved % flushInterval) == 0) { ++ needsFlush = false; ++ RegionFileIOThread.partialFlush(flushInterval / 2); ++ } ++ if (logProgress) { ++ final long currTime = System.nanoTime(); ++ if ((currTime - lastLog) > TimeUnit.SECONDS.toNanos(10L)) { ++ lastLog = currTime; ++ LOGGER.info("Saved " + saved + " chunks (" + format.format((double)(i+1)/(double)len * 100.0) + "%) in world '" + this.world.getWorld().getName() + "'"); ++ } ++ } ++ } ++ if (flush) { ++ RegionFileIOThread.flush(); ++ if (this.world.paperConfig().chunks.flushRegionsOnSave) { ++ try { ++ this.world.chunkSource.chunkMap.regionFileCache.flush(); ++ } catch (IOException ex) { ++ LOGGER.error("Exception when flushing regions in world {}", this.world.getWorld().getName(), ex); ++ } ++ } ++ } ++ if (logProgress) { ++ LOGGER.info("Saved " + savedChunk + " block chunks, " + savedEntity + " entity chunks, " + savedPoi + " poi chunks in world '" + this.world.getWorld().getName() + "' in " + format.format(1.0E-9 * (System.nanoTime() - start)) + "s"); ++ } ++ } ++ ++ protected final ThreadedTicketLevelPropagator ticketLevelPropagator = new ThreadedTicketLevelPropagator() { ++ @Override ++ protected void processLevelUpdates(final Long2ByteLinkedOpenHashMap updates) { ++ // first the necessary chunkholders must be created, so just update the ticket levels ++ for (final Iterator iterator = updates.long2ByteEntrySet().fastIterator(); iterator.hasNext();) { ++ final Long2ByteMap.Entry entry = iterator.next(); ++ final long key = entry.getLongKey(); ++ final int newLevel = convertBetweenTicketLevels((int)entry.getByteValue()); ++ ++ NewChunkHolder current = ChunkHolderManager.this.chunkHolders.get(key); ++ if (current == null && newLevel > MAX_TICKET_LEVEL) { ++ // not loaded and it shouldn't be loaded! ++ iterator.remove(); ++ continue; ++ } ++ ++ final int currentLevel = current == null ? MAX_TICKET_LEVEL + 1 : current.getCurrentTicketLevel(); ++ if (currentLevel == newLevel) { ++ // nothing to do ++ iterator.remove(); ++ continue; ++ } ++ ++ if (current == null) { ++ // must create ++ current = ChunkHolderManager.this.createChunkHolder(key); ++ synchronized (ChunkHolderManager.this.chunkHolders) { ++ ChunkHolderManager.this.chunkHolders.put(key, current); ++ } ++ current.updateTicketLevel(newLevel); ++ } else { ++ current.updateTicketLevel(newLevel); ++ } ++ } ++ } ++ ++ @Override ++ protected void processSchedulingUpdates(final Long2ByteLinkedOpenHashMap updates, final List scheduledTasks, ++ final List changedFullStatus) { ++ final List prev = CURRENT_TICKET_UPDATE_SCHEDULING.get(); ++ CURRENT_TICKET_UPDATE_SCHEDULING.set(scheduledTasks); ++ try { ++ for (final LongIterator iterator = updates.keySet().iterator(); iterator.hasNext();) { ++ final long key = iterator.nextLong(); ++ final NewChunkHolder current = ChunkHolderManager.this.chunkHolders.get(key); ++ ++ if (current == null) { ++ throw new IllegalStateException("Expected chunk holder to be created"); ++ } ++ ++ current.processTicketLevelUpdate(scheduledTasks, changedFullStatus); ++ } ++ } finally { ++ CURRENT_TICKET_UPDATE_SCHEDULING.set(prev); ++ } ++ } ++ }; ++ // function for converting between ticket levels and propagator levels and vice versa ++ // the problem is the ticket level propagator will propagate from a set source down to zero, whereas mojang expects ++ // levels to propagate from a set value up to a maximum value. so we need to convert the levels we put into the propagator ++ // and the levels we get out of the propagator ++ ++ public static int convertBetweenTicketLevels(final int level) { ++ return ChunkLevel.MAX_LEVEL - level + 1; ++ } ++ ++ public String getTicketDebugString(final long coordinate) { ++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate)); ++ try { ++ final SortedArraySet> tickets = this.tickets.get(new RegionFileIOThread.ChunkCoordinate(coordinate)); ++ ++ return tickets != null ? tickets.first().toString() : "no_ticket"; ++ } finally { ++ if (ticketLock != null) { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ } ++ } ++ ++ public Long2ObjectOpenHashMap>> getTicketsCopy() { ++ final Long2ObjectOpenHashMap>> ret = new Long2ObjectOpenHashMap<>(); ++ final Long2ObjectOpenHashMap> sections = new Long2ObjectOpenHashMap(); ++ final int sectionShift = this.taskScheduler.getChunkSystemLockShift(); ++ for (final RegionFileIOThread.ChunkCoordinate coord : this.tickets.keySet()) { ++ sections.computeIfAbsent( ++ CoordinateUtils.getChunkKey( ++ CoordinateUtils.getChunkX(coord.key) >> sectionShift, ++ CoordinateUtils.getChunkZ(coord.key) >> sectionShift ++ ), ++ (final long keyInMap) -> { ++ return new ArrayList<>(); ++ } ++ ).add(coord); ++ } ++ ++ for (final Iterator>> iterator = sections.long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry> entry = iterator.next(); ++ final long sectionKey = entry.getLongKey(); ++ final List coordinates = entry.getValue(); ++ ++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( ++ CoordinateUtils.getChunkX(sectionKey) << sectionShift, ++ CoordinateUtils.getChunkZ(sectionKey) << sectionShift ++ ); ++ try { ++ for (final RegionFileIOThread.ChunkCoordinate coord : coordinates) { ++ final SortedArraySet> tickets = this.tickets.get(coord); ++ if (tickets == null) { ++ // removed before we acquired lock ++ continue; ++ } ++ ret.put(coord.key, new SortedArraySet<>(tickets)); ++ } ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ } ++ ++ return ret; ++ } ++ ++ public Collection getPluginChunkTickets(int x, int z) { ++ ImmutableList.Builder ret; ++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(x, z); ++ try { ++ final long coordinate = CoordinateUtils.getChunkKey(x, z); ++ final SortedArraySet> tickets = this.tickets.get(new RegionFileIOThread.ChunkCoordinate(coordinate)); ++ ++ if (tickets == null) { ++ return Collections.emptyList(); ++ } ++ ++ ret = ImmutableList.builder(); ++ for (Ticket ticket : tickets) { ++ if (ticket.getType() == TicketType.PLUGIN_TICKET) { ++ ret.add((Plugin)ticket.key); ++ } ++ } ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ ++ return ret.build(); ++ } ++ ++ protected final void updateTicketLevel(final long coordinate, final int ticketLevel) { ++ if (ticketLevel > ChunkLevel.MAX_LEVEL) { ++ this.ticketLevelPropagator.removeSource(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate)); ++ } else { ++ this.ticketLevelPropagator.setSource(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate), convertBetweenTicketLevels(ticketLevel)); ++ } ++ } ++ ++ private static int getTicketLevelAt(SortedArraySet> tickets) { ++ return !tickets.isEmpty() ? tickets.first().getTicketLevel() : MAX_TICKET_LEVEL + 1; ++ } ++ ++ public boolean addTicketAtLevel(final TicketType type, final ChunkPos chunkPos, final int level, ++ final T identifier) { ++ return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier); ++ } ++ ++ public boolean addTicketAtLevel(final TicketType type, final int chunkX, final int chunkZ, final int level, ++ final T identifier) { ++ return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier); ++ } ++ ++ private void addExpireCount(final int chunkX, final int chunkZ) { ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ final int sectionShift = this.world.getRegionChunkShift(); ++ final RegionFileIOThread.ChunkCoordinate sectionKey = new RegionFileIOThread.ChunkCoordinate(CoordinateUtils.getChunkKey( ++ chunkX >> sectionShift, ++ chunkZ >> sectionShift ++ )); ++ ++ this.sectionToChunkToExpireCount.computeIfAbsent(sectionKey, (final RegionFileIOThread.ChunkCoordinate keyInMap) -> { ++ return new Long2IntOpenHashMap(); ++ }).addTo(chunkKey, 1); ++ } ++ ++ private void removeExpireCount(final int chunkX, final int chunkZ) { ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ final int sectionShift = this.world.getRegionChunkShift(); ++ final RegionFileIOThread.ChunkCoordinate sectionKey = new RegionFileIOThread.ChunkCoordinate(CoordinateUtils.getChunkKey( ++ chunkX >> sectionShift, ++ chunkZ >> sectionShift ++ )); ++ ++ final Long2IntOpenHashMap removeCounts = this.sectionToChunkToExpireCount.get(sectionKey); ++ final int prevCount = removeCounts.addTo(chunkKey, -1); ++ ++ if (prevCount == 1) { ++ removeCounts.remove(chunkKey); ++ if (removeCounts.isEmpty()) { ++ this.sectionToChunkToExpireCount.remove(sectionKey); ++ } ++ } ++ } ++ ++ // supposed to return true if the ticket was added and did not replace another ++ // but, we always return false if the ticket cannot be added ++ public boolean addTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier) { ++ return this.addTicketAtLevel(type, chunk, level, identifier, true); ++ } ++ ++ boolean addTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier, final boolean lock) { ++ final long removeDelay = type.timeout <= 0 ? NO_TIMEOUT_MARKER : type.timeout; ++ if (level > MAX_TICKET_LEVEL) { ++ return false; ++ } ++ ++ final int chunkX = CoordinateUtils.getChunkX(chunk); ++ final int chunkZ = CoordinateUtils.getChunkZ(chunk); ++ final RegionFileIOThread.ChunkCoordinate chunkCoord = new RegionFileIOThread.ChunkCoordinate(chunk); ++ final Ticket ticket = new Ticket<>(type, level, identifier, removeDelay); ++ ++ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null; ++ try { ++ final SortedArraySet> ticketsAtChunk = this.tickets.computeIfAbsent(chunkCoord, (final RegionFileIOThread.ChunkCoordinate keyInMap) -> { ++ return SortedArraySet.create(4); ++ }); ++ ++ final int levelBefore = getTicketLevelAt(ticketsAtChunk); ++ final Ticket current = (Ticket)ticketsAtChunk.replace(ticket); ++ final int levelAfter = getTicketLevelAt(ticketsAtChunk); ++ ++ if (current != ticket) { ++ final long oldRemoveDelay = current.removeDelay; ++ if (removeDelay != oldRemoveDelay) { ++ if (oldRemoveDelay != NO_TIMEOUT_MARKER && removeDelay == NO_TIMEOUT_MARKER) { ++ this.removeExpireCount(chunkX, chunkZ); ++ } else if (oldRemoveDelay == NO_TIMEOUT_MARKER) { ++ // since old != new, we have that NO_TIMEOUT_MARKER != new ++ this.addExpireCount(chunkX, chunkZ); ++ } ++ } ++ } else { ++ if (removeDelay != NO_TIMEOUT_MARKER) { ++ this.addExpireCount(chunkX, chunkZ); ++ } ++ } ++ ++ if (levelBefore != levelAfter) { ++ this.updateTicketLevel(chunk, levelAfter); ++ } ++ ++ return current == ticket; ++ } finally { ++ if (ticketLock != null) { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ } ++ } ++ ++ public boolean removeTicketAtLevel(final TicketType type, final ChunkPos chunkPos, final int level, final T identifier) { ++ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier); ++ } ++ ++ public boolean removeTicketAtLevel(final TicketType type, final int chunkX, final int chunkZ, final int level, final T identifier) { ++ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier); ++ } ++ ++ public boolean removeTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier) { ++ return this.removeTicketAtLevel(type, chunk, level, identifier, true); ++ } ++ ++ boolean removeTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier, final boolean lock) { ++ if (level > MAX_TICKET_LEVEL) { ++ return false; ++ } ++ ++ final int chunkX = CoordinateUtils.getChunkX(chunk); ++ final int chunkZ = CoordinateUtils.getChunkZ(chunk); ++ final RegionFileIOThread.ChunkCoordinate chunkCoord = new RegionFileIOThread.ChunkCoordinate(chunk); ++ final Ticket probe = new Ticket<>(type, level, identifier, PROBE_MARKER); ++ ++ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null; ++ try { ++ final SortedArraySet> ticketsAtChunk = this.tickets.get(chunkCoord); ++ if (ticketsAtChunk == null) { ++ return false; ++ } ++ ++ final int oldLevel = getTicketLevelAt(ticketsAtChunk); ++ final Ticket ticket = (Ticket)ticketsAtChunk.removeAndGet(probe); ++ ++ if (ticket == null) { ++ return false; ++ } ++ ++ final int newLevel = getTicketLevelAt(ticketsAtChunk); ++ // we should not change the ticket levels while the target region may be ticking ++ if (oldLevel != newLevel) { ++ // Delay unload chunk patch originally by Aikar, updated to 1.20 by jpenilla ++ // these days, the patch is mostly useful to keep chunks ticking when players teleport ++ // so that their pets can teleport with them as well. ++ final long delayTimeout = this.world.paperConfig().chunks.delayChunkUnloadsBy.ticks(); ++ final TicketType toAdd; ++ final long timeout; ++ if (type == RegionizedPlayerChunkLoader.REGION_PLAYER_TICKET && delayTimeout > 0) { ++ toAdd = TicketType.DELAY_UNLOAD; ++ timeout = delayTimeout; ++ } else { ++ toAdd = TicketType.UNKNOWN; ++ // always expect UNKNOWN to be > 1, but just in case ++ timeout = Math.max(1, toAdd.timeout); ++ } ++ final Ticket unknownTicket = new Ticket<>(toAdd, level, new ChunkPos(chunk), timeout); ++ if (ticketsAtChunk.add(unknownTicket)) { ++ this.addExpireCount(chunkX, chunkZ); ++ } else { ++ throw new IllegalStateException("Should have been able to add " + unknownTicket + " to " + ticketsAtChunk); ++ } ++ } ++ ++ final long removeDelay = ticket.removeDelay; ++ if (removeDelay != NO_TIMEOUT_MARKER) { ++ this.removeExpireCount(chunkX, chunkZ); ++ } ++ ++ return true; ++ } finally { ++ if (ticketLock != null) { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ } ++ } ++ ++ // atomic with respect to all add/remove/addandremove ticket calls for the given chunk ++ public void addAndRemoveTickets(final long chunk, final TicketType addType, final int addLevel, final T addIdentifier, ++ final TicketType removeType, final int removeLevel, final V removeIdentifier) { ++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)); ++ try { ++ this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier, false); ++ this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier, false); ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ } ++ ++ // atomic with respect to all add/remove/addandremove ticket calls for the given chunk ++ public boolean addIfRemovedTicket(final long chunk, final TicketType addType, final int addLevel, final T addIdentifier, ++ final TicketType removeType, final int removeLevel, final V removeIdentifier) { ++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)); ++ try { ++ if (this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier, false)) { ++ this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier, false); ++ return true; ++ } ++ return false; ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ } ++ ++ public void removeAllTicketsFor(final TicketType ticketType, final int ticketLevel, final T ticketIdentifier) { ++ if (ticketLevel > MAX_TICKET_LEVEL) { ++ return; ++ } ++ ++ final Long2ObjectOpenHashMap> sections = new Long2ObjectOpenHashMap(); ++ final int sectionShift = this.taskScheduler.getChunkSystemLockShift(); ++ for (final RegionFileIOThread.ChunkCoordinate coord : this.tickets.keySet()) { ++ sections.computeIfAbsent( ++ CoordinateUtils.getChunkKey( ++ CoordinateUtils.getChunkX(coord.key) >> sectionShift, ++ CoordinateUtils.getChunkZ(coord.key) >> sectionShift ++ ), ++ (final long keyInMap) -> { ++ return new ArrayList<>(); ++ } ++ ).add(coord); ++ } ++ ++ for (final Iterator>> iterator = sections.long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry> entry = iterator.next(); ++ final long sectionKey = entry.getLongKey(); ++ final List coordinates = entry.getValue(); ++ ++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( ++ CoordinateUtils.getChunkX(sectionKey) << sectionShift, ++ CoordinateUtils.getChunkZ(sectionKey) << sectionShift ++ ); ++ try { ++ for (final RegionFileIOThread.ChunkCoordinate coord : coordinates) { ++ this.removeTicketAtLevel(ticketType, coord.key, ticketLevel, ticketIdentifier, false); ++ } ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ } ++ } ++ ++ public void tick() { ++ final int sectionShift = this.world.getRegionChunkShift(); ++ ++ final Predicate> expireNow = (final Ticket ticket) -> { ++ if (ticket.removeDelay == NO_TIMEOUT_MARKER) { ++ return false; ++ } ++ return --ticket.removeDelay <= 0L; ++ }; ++ ++ for (final Iterator iterator = this.sectionToChunkToExpireCount.keySet().iterator(); iterator.hasNext();) { ++ final RegionFileIOThread.ChunkCoordinate section = iterator.next(); ++ final long sectionKey = section.key; ++ ++ if (!this.sectionToChunkToExpireCount.containsKey(section)) { ++ // removed concurrently ++ continue; ++ } ++ ++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( ++ CoordinateUtils.getChunkX(sectionKey) << sectionShift, ++ CoordinateUtils.getChunkZ(sectionKey) << sectionShift ++ ); ++ ++ try { ++ final Long2IntOpenHashMap chunkToExpireCount = this.sectionToChunkToExpireCount.get(section); ++ if (chunkToExpireCount == null) { ++ // lost to some race ++ continue; ++ } ++ ++ for (final Iterator iterator1 = chunkToExpireCount.long2IntEntrySet().fastIterator(); iterator1.hasNext();) { ++ final Long2IntMap.Entry entry = iterator1.next(); ++ ++ final long chunkKey = entry.getLongKey(); ++ final int expireCount = entry.getIntValue(); ++ ++ final RegionFileIOThread.ChunkCoordinate chunk = new RegionFileIOThread.ChunkCoordinate(chunkKey); ++ ++ final SortedArraySet> tickets = this.tickets.get(chunk); ++ final int levelBefore = getTicketLevelAt(tickets); ++ ++ final int sizeBefore = tickets.size(); ++ tickets.removeIf(expireNow); ++ final int sizeAfter = tickets.size(); ++ final int levelAfter = getTicketLevelAt(tickets); ++ ++ if (tickets.isEmpty()) { ++ this.tickets.remove(chunk); ++ } ++ if (levelBefore != levelAfter) { ++ this.updateTicketLevel(chunkKey, levelAfter); ++ } ++ ++ final int newExpireCount = expireCount - (sizeBefore - sizeAfter); ++ ++ if (newExpireCount == expireCount) { ++ continue; ++ } ++ ++ if (newExpireCount != 0) { ++ entry.setValue(newExpireCount); ++ } else { ++ iterator1.remove(); ++ } ++ } ++ ++ if (chunkToExpireCount.isEmpty()) { ++ this.sectionToChunkToExpireCount.remove(section); ++ } ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ } ++ ++ this.processTicketUpdates(); ++ } ++ ++ public NewChunkHolder getChunkHolder(final int chunkX, final int chunkZ) { ++ return this.chunkHolders.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ ++ public NewChunkHolder getChunkHolder(final long position) { ++ return this.chunkHolders.get(position); ++ } ++ ++ public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); ++ if (chunkHolder != null) { ++ chunkHolder.raisePriority(priority); ++ } ++ } ++ ++ public void setPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); ++ if (chunkHolder != null) { ++ chunkHolder.setPriority(priority); ++ } ++ } ++ ++ public void lowerPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); ++ if (chunkHolder != null) { ++ chunkHolder.lowerPriority(priority); ++ } ++ } ++ ++ private NewChunkHolder createChunkHolder(final long position) { ++ final NewChunkHolder ret = new NewChunkHolder(this.world, CoordinateUtils.getChunkX(position), CoordinateUtils.getChunkZ(position), this.taskScheduler); ++ ++ ChunkSystem.onChunkHolderCreate(this.world, ret.vanillaChunkHolder); ++ ret.vanillaChunkHolder.onChunkAdd(); ++ ++ return ret; ++ } ++ ++ // because this function creates the chunk holder without a ticket, it is the caller's responsibility to ensure ++ // the chunk holder eventually unloads. this should only be used to avoid using processTicketUpdates to create chunkholders, ++ // as processTicketUpdates may call plugin logic; in every other case a ticket is appropriate ++ private NewChunkHolder getOrCreateChunkHolder(final int chunkX, final int chunkZ) { ++ return this.getOrCreateChunkHolder(CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ } ++ ++ private NewChunkHolder getOrCreateChunkHolder(final long position) { ++ final int chunkX = CoordinateUtils.getChunkX(position); ++ final int chunkZ = CoordinateUtils.getChunkZ(position); ++ ++ if (!this.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ)) { ++ throw new IllegalStateException("Must hold ticket level update lock!"); ++ } ++ if (!this.taskScheduler.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ)) { ++ throw new IllegalStateException("Must hold scheduler lock!!"); ++ } ++ ++ // we could just acquire these locks, but... ++ // must own the locks because the caller needs to ensure that no unload can occur AFTER this function returns ++ ++ NewChunkHolder current = this.chunkHolders.get(position); ++ if (current != null) { ++ return current; ++ } ++ ++ current = this.createChunkHolder(position); ++ synchronized (this.chunkHolders) { ++ this.chunkHolders.put(position, current); ++ } ++ ++ return current; ++ } ++ ++ private final AtomicLong entityLoadCounter = new AtomicLong(); ++ ++ public ChunkEntitySlices getOrCreateEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) { ++ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create entity chunk off-main"); ++ ChunkEntitySlices ret; ++ ++ NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ); ++ if (current != null && (ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) { ++ return ret; ++ } ++ ++ final AtomicBoolean isCompleted = new AtomicBoolean(); ++ final Thread waiter = Thread.currentThread(); ++ final Long entityLoadId = Long.valueOf(this.entityLoadCounter.getAndIncrement()); ++ NewChunkHolder.GenericDataLoadTaskCallback loadTask = null; ++ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ); ++ try { ++ this.addTicketAtLevel(TicketType.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId); ++ final ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ); ++ try { ++ current = this.getOrCreateChunkHolder(chunkX, chunkZ); ++ if ((ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) { ++ this.removeTicketAtLevel(TicketType.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId); ++ return ret; ++ } ++ ++ if (current.isEntityChunkNBTLoaded()) { ++ isCompleted.setPlain(true); ++ } else { ++ loadTask = current.getOrLoadEntityData((final GenericDataLoadTask.TaskResult result) -> { ++ if (!transientChunk) { ++ isCompleted.set(true); ++ LockSupport.unpark(waiter); ++ } ++ }); ++ final ChunkLoadTask.EntityDataLoadTask entityLoad = current.getEntityDataLoadTask(); ++ ++ if (entityLoad != null && !transientChunk) { ++ entityLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING); ++ } ++ } ++ } finally { ++ this.taskScheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ ++ if (loadTask != null) { ++ loadTask.schedule(); ++ } ++ ++ if (!transientChunk) { ++ // Note: no need to busy wait on the chunk queue, entity load will complete off-main ++ boolean interrupted = false; ++ while (!isCompleted.get()) { ++ interrupted |= Thread.interrupted(); ++ LockSupport.park(); ++ } ++ ++ if (interrupted) { ++ Thread.currentThread().interrupt(); ++ } ++ } ++ ++ // now that the entity data is loaded, we can load it into the world ++ ++ ret = current.loadInEntityChunk(transientChunk); ++ ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ this.addAndRemoveTickets(chunkKey, ++ TicketType.UNKNOWN, MAX_TICKET_LEVEL, new ChunkPos(chunkX, chunkZ), ++ TicketType.ENTITY_LOAD, MAX_TICKET_LEVEL, entityLoadId ++ ); ++ ++ return ret; ++ } ++ ++ public PoiChunk getPoiChunkIfLoaded(final int chunkX, final int chunkZ, final boolean checkLoadInCallback) { ++ final NewChunkHolder holder = this.getChunkHolder(chunkX, chunkZ); ++ if (holder != null) { ++ final PoiChunk ret = holder.getPoiChunk(); ++ return ret == null || (checkLoadInCallback && !ret.isLoaded()) ? null : ret; ++ } ++ return null; ++ } ++ ++ private final AtomicLong poiLoadCounter = new AtomicLong(); ++ ++ public PoiChunk loadPoiChunk(final int chunkX, final int chunkZ) { ++ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create poi chunk off-main"); ++ PoiChunk ret; ++ ++ NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ); ++ if (current != null && (ret = current.getPoiChunk()) != null) { ++ if (!ret.isLoaded()) { ++ ret.load(); ++ } ++ return ret; ++ } ++ ++ final AtomicReference completed = new AtomicReference<>(); ++ final AtomicBoolean isCompleted = new AtomicBoolean(); ++ final Thread waiter = Thread.currentThread(); ++ final Long poiLoadId = Long.valueOf(this.poiLoadCounter.getAndIncrement()); ++ NewChunkHolder.GenericDataLoadTaskCallback loadTask = null; ++ final ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ); // Folia - use area based lock to reduce contention ++ try { ++ // Folia - use area based lock to reduce contention ++ this.addTicketAtLevel(TicketType.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId); ++ final ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ); // Folia - use area based lock to reduce contention ++ try { ++ current = this.getOrCreateChunkHolder(chunkX, chunkZ); ++ if (current.isPoiChunkLoaded()) { ++ this.removeTicketAtLevel(TicketType.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId); ++ return current.getPoiChunk(); ++ } ++ ++ loadTask = current.getOrLoadPoiData((final GenericDataLoadTask.TaskResult result) -> { ++ completed.setPlain(result.left()); ++ isCompleted.set(true); ++ LockSupport.unpark(waiter); ++ }); ++ final ChunkLoadTask.PoiDataLoadTask poiLoad = current.getPoiDataLoadTask(); ++ ++ if (poiLoad != null) { ++ poiLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING); ++ } ++ } finally { ++ this.taskScheduler.schedulingLockArea.unlock(schedulingLock); // Folia - use area based lock to reduce contention ++ } ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); // Folia - use area based lock to reduce contention ++ } ++ ++ if (loadTask != null) { ++ loadTask.schedule(); ++ } ++ ++ // Note: no need to busy wait on the chunk queue, poi load will complete off-main ++ ++ boolean interrupted = false; ++ while (!isCompleted.get()) { ++ interrupted |= Thread.interrupted(); ++ LockSupport.park(); ++ } ++ ++ if (interrupted) { ++ Thread.currentThread().interrupt(); ++ } ++ ++ ret = completed.getPlain(); ++ ++ ret.load(); ++ ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ this.addAndRemoveTickets(chunkKey, ++ TicketType.UNKNOWN, MAX_TICKET_LEVEL, new ChunkPos(chunkX, chunkZ), ++ TicketType.POI_LOAD, MAX_TICKET_LEVEL, poiLoadId ++ ); ++ ++ return ret; ++ } ++ ++ void addChangedStatuses(final List changedFullStatus) { ++ if (changedFullStatus.isEmpty()) { ++ return; ++ } ++ if (!TickThread.isTickThread()) { ++ this.taskScheduler.scheduleChunkTask(() -> { ++ final ArrayDeque pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate; ++ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { ++ pendingFullLoadUpdate.add(changedFullStatus.get(i)); ++ } ++ ++ ChunkHolderManager.this.processPendingFullUpdate(); ++ }, PrioritisedExecutor.Priority.HIGHEST); ++ } else { ++ final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; ++ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { ++ pendingFullLoadUpdate.add(changedFullStatus.get(i)); ++ } ++ } ++ } ++ ++ private void removeChunkHolder(final NewChunkHolder holder) { ++ holder.killed = true; ++ holder.vanillaChunkHolder.onChunkRemove(); ++ this.autoSaveQueue.remove(holder); ++ ChunkSystem.onChunkHolderDelete(this.world, holder.vanillaChunkHolder); ++ synchronized (this.chunkHolders) { ++ this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ)); ++ } ++ } ++ ++ // note: never call while inside the chunk system, this will absolutely break everything ++ public void processUnloads() { ++ TickThread.ensureTickThread("Cannot unload chunks off-main"); ++ ++ if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) { ++ throw new IllegalStateException("Cannot unload chunks recursively"); ++ } ++ final int sectionShift = this.unloadQueue.coordinateShift; // sectionShift <= lock shift ++ final List unloadSectionsForRegion = this.unloadQueue.retrieveForAllRegions(); ++ int unloadCountTentative = 0; ++ for (final ChunkQueue.SectionToUnload sectionRef : unloadSectionsForRegion) { ++ final ChunkQueue.UnloadSection section ++ = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ()); ++ ++ if (section == null) { ++ // removed concurrently ++ continue; ++ } ++ ++ // technically reading the size field is unsafe, and it may be incorrect. ++ // We assume that the error here cumulatively goes away over many ticks. If it did not, then it is possible ++ // for chunks to never unload or not unload fast enough. ++ unloadCountTentative += section.chunks.size(); ++ } ++ ++ if (unloadCountTentative <= 0) { ++ // no work to do ++ return; ++ } ++ ++ // Note: The behaviour that we process ticket updates while holding the lock has been dropped here, as it is racey behavior. ++ // But, we do need to process updates here so that any add ticket that is synchronised before this call does not go missed. ++ this.processTicketUpdates(); ++ ++ final int toUnloadCount = Math.max(50, (int)(unloadCountTentative * 0.05)); ++ int processedCount = 0; ++ ++ for (final ChunkQueue.SectionToUnload sectionRef : unloadSectionsForRegion) { ++ final List stage1 = new ArrayList<>(); ++ final List stage2 = new ArrayList<>(); ++ ++ final int sectionLowerX = sectionRef.sectionX() << sectionShift; ++ final int sectionLowerZ = sectionRef.sectionZ() << sectionShift; ++ ++ // stage 1: set up for stage 2 while holding critical locks ++ ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(sectionLowerX, sectionLowerZ); ++ try { ++ final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ); ++ try { ++ final ChunkQueue.UnloadSection section ++ = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ()); ++ ++ if (section == null) { ++ // removed concurrently ++ continue; ++ } ++ ++ // collect the holders to run stage 1 on ++ final int sectionCount = section.chunks.size(); ++ ++ if ((sectionCount + processedCount) <= toUnloadCount) { ++ // we can just drain the entire section ++ ++ for (final LongIterator iterator = section.chunks.iterator(); iterator.hasNext();) { ++ final NewChunkHolder holder = this.chunkHolders.get(iterator.nextLong()); ++ if (holder == null) { ++ throw new IllegalStateException(); ++ } ++ stage1.add(holder); ++ } ++ ++ // remove section ++ this.unloadQueue.removeSection(sectionRef.sectionX(), sectionRef.sectionZ()); ++ } else { ++ // processedCount + len = toUnloadCount ++ // we cannot drain the entire section ++ for (int i = 0, len = toUnloadCount - processedCount; i < len; ++i) { ++ final NewChunkHolder holder = this.chunkHolders.get(section.chunks.removeFirstLong()); ++ if (holder == null) { ++ throw new IllegalStateException(); ++ } ++ stage1.add(holder); ++ } ++ } ++ ++ // run stage 1 ++ for (int i = 0, len = stage1.size(); i < len; ++i) { ++ final NewChunkHolder chunkHolder = stage1.get(i); ++ if (chunkHolder.isSafeToUnload() != null) { ++ LOGGER.error("Chunkholder " + chunkHolder + " is not safe to unload but is inside the unload queue?"); ++ continue; ++ } ++ final NewChunkHolder.UnloadState state = chunkHolder.unloadStage1(); ++ if (state == null) { ++ // can unload immediately ++ this.removeChunkHolder(chunkHolder); ++ continue; ++ } ++ stage2.add(state); ++ } ++ } finally { ++ this.taskScheduler.schedulingLockArea.unlock(scheduleLock); ++ } ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ ++ // stage 2: invoke expensive unload logic, designed to run without locks thanks to stage 1 ++ final List stage3 = new ArrayList<>(stage2.size()); ++ ++ final Boolean before = this.blockTicketUpdates(); ++ try { ++ for (int i = 0, len = stage2.size(); i < len; ++i) { ++ final NewChunkHolder.UnloadState state = stage2.get(i); ++ final NewChunkHolder holder = state.holder(); ++ ++ holder.unloadStage2(state); ++ stage3.add(holder); ++ } ++ } finally { ++ this.unblockTicketUpdates(before); ++ } ++ ++ // stage 3: actually attempt to remove the chunk holders ++ ticketLock = this.ticketLockArea.lock(sectionLowerX, sectionLowerZ); ++ try { ++ final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ); ++ try { ++ for (int i = 0, len = stage3.size(); i < len; ++i) { ++ final NewChunkHolder holder = stage3.get(i); ++ ++ if (holder.unloadStage3()) { ++ this.removeChunkHolder(holder); ++ } else { ++ // add cooldown so the next unload check is not immediately next tick ++ this.addTicketAtLevel(TicketType.UNLOAD_COOLDOWN, CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ), MAX_TICKET_LEVEL, Unit.INSTANCE, false); ++ } ++ } ++ } finally { ++ this.taskScheduler.schedulingLockArea.unlock(scheduleLock); ++ } ++ } finally { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ ++ processedCount += stage1.size(); ++ ++ if (processedCount >= toUnloadCount) { ++ break; ++ } ++ } ++ } ++ ++ public enum TicketOperationType { ++ ADD, REMOVE, ADD_IF_REMOVED, ADD_AND_REMOVE ++ } ++ ++ public static record TicketOperation ( ++ TicketOperationType op, long chunkCoord, ++ TicketType ticketType, int ticketLevel, T identifier, ++ TicketType ticketType2, int ticketLevel2, V identifier2 ++ ) { ++ ++ private TicketOperation(TicketOperationType op, long chunkCoord, ++ TicketType ticketType, int ticketLevel, T identifier) { ++ this(op, chunkCoord, ticketType, ticketLevel, identifier, null, 0, null); ++ } ++ ++ public static TicketOperation addOp(final ChunkPos chunk, final TicketType type, final int ticketLevel, final T identifier) { ++ return addOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier); ++ } ++ ++ public static TicketOperation addOp(final int chunkX, final int chunkZ, final TicketType type, final int ticketLevel, final T identifier) { ++ return addOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier); ++ } ++ ++ public static TicketOperation addOp(final long chunk, final TicketType type, final int ticketLevel, final T identifier) { ++ return new TicketOperation<>(TicketOperationType.ADD, chunk, type, ticketLevel, identifier); ++ } ++ ++ public static TicketOperation removeOp(final ChunkPos chunk, final TicketType type, final int ticketLevel, final T identifier) { ++ return removeOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier); ++ } ++ ++ public static TicketOperation removeOp(final int chunkX, final int chunkZ, final TicketType type, final int ticketLevel, final T identifier) { ++ return removeOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier); ++ } ++ ++ public static TicketOperation removeOp(final long chunk, final TicketType type, final int ticketLevel, final T identifier) { ++ return new TicketOperation<>(TicketOperationType.REMOVE, chunk, type, ticketLevel, identifier); ++ } ++ ++ public static TicketOperation addIfRemovedOp(final long chunk, ++ final TicketType addType, final int addLevel, final T addIdentifier, ++ final TicketType removeType, final int removeLevel, final V removeIdentifier) { ++ return new TicketOperation<>( ++ TicketOperationType.ADD_IF_REMOVED, chunk, addType, addLevel, addIdentifier, ++ removeType, removeLevel, removeIdentifier ++ ); ++ } ++ ++ public static TicketOperation addAndRemove(final long chunk, ++ final TicketType addType, final int addLevel, final T addIdentifier, ++ final TicketType removeType, final int removeLevel, final V removeIdentifier) { ++ return new TicketOperation<>( ++ TicketOperationType.ADD_AND_REMOVE, chunk, addType, addLevel, addIdentifier, ++ removeType, removeLevel, removeIdentifier ++ ); ++ } ++ } ++ ++ private boolean processTicketOp(TicketOperation operation) { ++ boolean ret = false; ++ switch (operation.op) { ++ case ADD: { ++ ret |= this.addTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier); ++ break; ++ } ++ case REMOVE: { ++ ret |= this.removeTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier); ++ break; ++ } ++ case ADD_IF_REMOVED: { ++ ret |= this.addIfRemovedTicket( ++ operation.chunkCoord, ++ operation.ticketType, operation.ticketLevel, operation.identifier, ++ operation.ticketType2, operation.ticketLevel2, operation.identifier2 ++ ); ++ break; ++ } ++ case ADD_AND_REMOVE: { ++ ret = true; ++ this.addAndRemoveTickets( ++ operation.chunkCoord, ++ operation.ticketType, operation.ticketLevel, operation.identifier, ++ operation.ticketType2, operation.ticketLevel2, operation.identifier2 ++ ); ++ break; ++ } ++ } ++ ++ return ret; ++ } ++ ++ public void performTicketUpdates(final Collection> operations) { ++ for (final TicketOperation operation : operations) { ++ this.processTicketOp(operation); ++ } ++ } ++ ++ private final ThreadLocal BLOCK_TICKET_UPDATES = ThreadLocal.withInitial(() -> { ++ return Boolean.FALSE; ++ }); ++ ++ public Boolean blockTicketUpdates() { ++ final Boolean ret = BLOCK_TICKET_UPDATES.get(); ++ BLOCK_TICKET_UPDATES.set(Boolean.TRUE); ++ return ret; ++ } ++ ++ public void unblockTicketUpdates(final Boolean before) { ++ BLOCK_TICKET_UPDATES.set(before); ++ } ++ ++ public boolean processTicketUpdates() { ++ return this.processTicketUpdates(true, true, null); ++ } ++ ++ private static final ThreadLocal> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>(); ++ ++ static List getCurrentTicketUpdateScheduling() { ++ return CURRENT_TICKET_UPDATE_SCHEDULING.get(); ++ } ++ ++ private boolean processTicketUpdates(final boolean checkLocks, final boolean processFullUpdates, List scheduledTasks) { ++ TickThread.ensureTickThread("Cannot process ticket levels off-main"); ++ if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) { ++ throw new IllegalStateException("Cannot update ticket level while unloading chunks or updating entity manager"); ++ } ++ ++ List changedFullStatus = null; ++ ++ final boolean isTickThread = TickThread.isTickThread(); ++ ++ boolean ret = false; ++ final boolean canProcessFullUpdates = processFullUpdates & isTickThread; ++ final boolean canProcessScheduling = scheduledTasks == null; ++ ++ if (this.ticketLevelPropagator.hasPendingUpdates()) { ++ if (scheduledTasks == null) { ++ scheduledTasks = new ArrayList<>(); ++ } ++ changedFullStatus = new ArrayList<>(); ++ ++ ret |= this.ticketLevelPropagator.performUpdates( ++ this.ticketLockArea, this.taskScheduler.schedulingLockArea, ++ scheduledTasks, changedFullStatus ++ ); ++ } ++ ++ if (changedFullStatus != null) { ++ this.addChangedStatuses(changedFullStatus); ++ } ++ ++ if (canProcessScheduling && scheduledTasks != null) { ++ for (int i = 0, len = scheduledTasks.size(); i < len; ++i) { ++ scheduledTasks.get(i).schedule(); ++ } ++ } ++ ++ if (canProcessFullUpdates) { ++ ret |= this.processPendingFullUpdate(); ++ } ++ ++ return ret; ++ } ++ ++ // only call on tick thread ++ protected final boolean processPendingFullUpdate() { ++ final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; ++ ++ boolean ret = false; ++ ++ List changedFullStatus = new ArrayList<>(); ++ ++ NewChunkHolder holder; ++ while ((holder = pendingFullLoadUpdate.poll()) != null) { ++ ret |= holder.handleFullStatusChange(changedFullStatus); ++ ++ if (!changedFullStatus.isEmpty()) { ++ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { ++ pendingFullLoadUpdate.add(changedFullStatus.get(i)); ++ } ++ changedFullStatus.clear(); ++ } ++ } ++ ++ return ret; ++ } ++ ++ public JsonObject getDebugJsonForWatchdog() { ++ return this.getDebugJsonNoLock(); ++ } ++ ++ private JsonObject getDebugJsonNoLock() { ++ final JsonObject ret = new JsonObject(); ++ ret.addProperty("current_tick", Long.valueOf(this.currentTick)); ++ ++ final JsonArray unloadQueue = new JsonArray(); ++ ret.add("unload_queue", unloadQueue); ++ ret.addProperty("lock_shift", Integer.valueOf(this.taskScheduler.getChunkSystemLockShift())); ++ ret.addProperty("ticket_shift", Integer.valueOf(ThreadedTicketLevelPropagator.SECTION_SHIFT)); ++ ret.addProperty("region_shift", Integer.valueOf(this.world.getRegionChunkShift())); ++ for (final ChunkQueue.SectionToUnload section : this.unloadQueue.retrieveForAllRegions()) { ++ final JsonObject sectionJson = new JsonObject(); ++ unloadQueue.add(sectionJson); ++ sectionJson.addProperty("sectionX", section.sectionX()); ++ sectionJson.addProperty("sectionZ", section.sectionX()); ++ sectionJson.addProperty("order", section.order()); ++ ++ final JsonArray coordinates = new JsonArray(); ++ sectionJson.add("coordinates", coordinates); ++ ++ final ChunkQueue.UnloadSection actualSection = this.unloadQueue.getSectionUnsynchronized(section.sectionX(), section.sectionZ()); ++ for (final LongIterator iterator = actualSection.chunks.iterator(); iterator.hasNext();) { ++ final long coordinate = iterator.nextLong(); ++ ++ final JsonObject coordinateJson = new JsonObject(); ++ coordinates.add(coordinateJson); ++ ++ coordinateJson.addProperty("chunkX", Integer.valueOf(CoordinateUtils.getChunkX(coordinate))); ++ coordinateJson.addProperty("chunkZ", Integer.valueOf(CoordinateUtils.getChunkZ(coordinate))); ++ } ++ } ++ ++ final JsonArray holders = new JsonArray(); ++ ret.add("chunkholders", holders); ++ ++ for (final NewChunkHolder holder : this.getChunkHolders()) { ++ holders.add(holder.getDebugJson()); ++ } ++ ++ // TODO ++ /* ++ final JsonArray removeTickToChunkExpireTicketCount = new JsonArray(); ++ ret.add("remove_tick_to_chunk_expire_ticket_count", removeTickToChunkExpireTicketCount); ++ ++ for (final Long2ObjectMap.Entry tickEntry : this.removeTickToChunkExpireTicketCount.long2ObjectEntrySet()) { ++ final long tick = tickEntry.getLongKey(); ++ final Long2IntOpenHashMap coordinateToCount = tickEntry.getValue(); ++ ++ final JsonObject tickJson = new JsonObject(); ++ removeTickToChunkExpireTicketCount.add(tickJson); ++ ++ tickJson.addProperty("tick", Long.valueOf(tick)); ++ ++ final JsonArray tickEntries = new JsonArray(); ++ tickJson.add("entries", tickEntries); ++ ++ for (final Long2IntMap.Entry entry : coordinateToCount.long2IntEntrySet()) { ++ final long coordinate = entry.getLongKey(); ++ final int count = entry.getIntValue(); ++ ++ final JsonObject entryJson = new JsonObject(); ++ tickEntries.add(entryJson); ++ ++ entryJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate))); ++ entryJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate))); ++ entryJson.addProperty("count", Integer.valueOf(count)); ++ } ++ } ++ ++ final JsonArray allTicketsJson = new JsonArray(); ++ ret.add("tickets", allTicketsJson); ++ ++ for (final Long2ObjectMap.Entry>> coordinateTickets : this.tickets.long2ObjectEntrySet()) { ++ final long coordinate = coordinateTickets.getLongKey(); ++ final SortedArraySet> tickets = coordinateTickets.getValue(); ++ ++ final JsonObject coordinateJson = new JsonObject(); ++ allTicketsJson.add(coordinateJson); ++ ++ coordinateJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate))); ++ coordinateJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate))); ++ ++ final JsonArray ticketsSerialized = new JsonArray(); ++ coordinateJson.add("tickets", ticketsSerialized); ++ ++ for (final Ticket ticket : tickets) { ++ final JsonObject ticketSerialized = new JsonObject(); ++ ticketsSerialized.add(ticketSerialized); ++ ++ ticketSerialized.addProperty("type", ticket.getType().toString()); ++ ticketSerialized.addProperty("level", Integer.valueOf(ticket.getTicketLevel())); ++ ticketSerialized.addProperty("identifier", Objects.toString(ticket.key)); ++ ticketSerialized.addProperty("remove_tick", Long.valueOf(ticket.removalTick)); ++ } ++ } ++ */ ++ ++ return ret; ++ } ++ ++ public JsonObject getDebugJson() { ++ return this.getDebugJsonNoLock(); // Folia - use area based lock to reduce contention ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLightTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLightTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..86e618586d2ad9d843ad761b7336bb3073ed4c23 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLightTask.java +@@ -0,0 +1,181 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.starlight.common.light.StarLightEngine; ++import ca.spottedleaf.starlight.common.light.StarLightInterface; ++import io.papermc.paper.chunk.system.light.LightQueue; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.chunk.ProtoChunk; ++import org.apache.logging.log4j.LogManager; ++import org.apache.logging.log4j.Logger; ++import java.util.function.BooleanSupplier; ++ ++public final class ChunkLightTask extends ChunkProgressionTask { ++ ++ private static final Logger LOGGER = LogManager.getLogger(); ++ ++ protected final ChunkAccess fromChunk; ++ ++ private final LightTaskPriorityHolder priorityHolder; ++ ++ public ChunkLightTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, ++ final ChunkAccess chunk, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ); ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.priorityHolder = new LightTaskPriorityHolder(priority, this); ++ this.fromChunk = chunk; ++ } ++ ++ @Override ++ public boolean isScheduled() { ++ return this.priorityHolder.isScheduled(); ++ } ++ ++ @Override ++ public ChunkStatus getTargetStatus() { ++ return ChunkStatus.LIGHT; ++ } ++ ++ @Override ++ public void schedule() { ++ this.priorityHolder.schedule(); ++ } ++ ++ @Override ++ public void cancel() { ++ this.priorityHolder.cancel(); ++ } ++ ++ @Override ++ public PrioritisedExecutor.Priority getPriority() { ++ return this.priorityHolder.getPriority(); ++ } ++ ++ @Override ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ this.priorityHolder.raisePriority(priority); ++ } ++ ++ @Override ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ this.priorityHolder.setPriority(priority); ++ } ++ ++ @Override ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ this.priorityHolder.raisePriority(priority); ++ } ++ ++ private static final class LightTaskPriorityHolder extends PriorityHolder { ++ ++ protected final ChunkLightTask task; ++ ++ protected LightTaskPriorityHolder(final PrioritisedExecutor.Priority priority, final ChunkLightTask task) { ++ super(priority); ++ this.task = task; ++ } ++ ++ @Override ++ protected void cancelScheduled() { ++ final ChunkLightTask task = this.task; ++ task.complete(null, null); ++ } ++ ++ @Override ++ protected PrioritisedExecutor.Priority getScheduledPriority() { ++ final ChunkLightTask task = this.task; ++ return task.world.getChunkSource().getLightEngine().theLightEngine.lightQueue.getPriority(task.chunkX, task.chunkZ); ++ } ++ ++ @Override ++ protected void scheduleTask(final PrioritisedExecutor.Priority priority) { ++ final ChunkLightTask task = this.task; ++ final StarLightInterface starLightInterface = task.world.getChunkSource().getLightEngine().theLightEngine; ++ final LightQueue lightQueue = starLightInterface.lightQueue; ++ lightQueue.queueChunkLightTask(new ChunkPos(task.chunkX, task.chunkZ), new LightTask(starLightInterface, task), priority); ++ lightQueue.setPriority(task.chunkX, task.chunkZ, priority); ++ } ++ ++ @Override ++ protected void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority) { ++ final ChunkLightTask task = this.task; ++ final StarLightInterface starLightInterface = task.world.getChunkSource().getLightEngine().theLightEngine; ++ final LightQueue lightQueue = starLightInterface.lightQueue; ++ lightQueue.lowerPriority(task.chunkX, task.chunkZ, priority); ++ } ++ ++ @Override ++ protected void setPriorityScheduled(final PrioritisedExecutor.Priority priority) { ++ final ChunkLightTask task = this.task; ++ final StarLightInterface starLightInterface = task.world.getChunkSource().getLightEngine().theLightEngine; ++ final LightQueue lightQueue = starLightInterface.lightQueue; ++ lightQueue.setPriority(task.chunkX, task.chunkZ, priority); ++ } ++ ++ @Override ++ protected void raisePriorityScheduled(final PrioritisedExecutor.Priority priority) { ++ final ChunkLightTask task = this.task; ++ final StarLightInterface starLightInterface = task.world.getChunkSource().getLightEngine().theLightEngine; ++ final LightQueue lightQueue = starLightInterface.lightQueue; ++ lightQueue.raisePriority(task.chunkX, task.chunkZ, priority); ++ } ++ } ++ ++ private static final class LightTask implements BooleanSupplier { ++ ++ protected final StarLightInterface lightEngine; ++ protected final ChunkLightTask task; ++ ++ public LightTask(final StarLightInterface lightEngine, final ChunkLightTask task) { ++ this.lightEngine = lightEngine; ++ this.task = task; ++ } ++ ++ @Override ++ public boolean getAsBoolean() { ++ final ChunkLightTask task = this.task; ++ // executed on light thread ++ if (!task.priorityHolder.markExecuting()) { ++ // cancelled ++ return false; ++ } ++ ++ try { ++ final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(task.fromChunk); ++ ++ if (task.fromChunk.isLightCorrect() && task.fromChunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { ++ this.lightEngine.forceLoadInChunk(task.fromChunk, emptySections); ++ this.lightEngine.checkChunkEdges(task.chunkX, task.chunkZ); ++ } else { ++ task.fromChunk.setLightCorrect(false); ++ this.lightEngine.lightChunk(task.fromChunk, emptySections); ++ task.fromChunk.setLightCorrect(true); ++ } ++ // we need to advance status ++ if (task.fromChunk instanceof ProtoChunk chunk && chunk.getStatus() == ChunkStatus.LIGHT.getParent()) { ++ chunk.setStatus(ChunkStatus.LIGHT); ++ } ++ } catch (final Throwable thr) { ++ if (!(thr instanceof ThreadDeath)) { ++ LOGGER.fatal("Failed to light chunk " + task.fromChunk.getPos().toString() + " in world '" + this.lightEngine.getWorld().getWorld().getName() + "'", thr); ++ } ++ ++ task.complete(null, thr); ++ ++ if (thr instanceof ThreadDeath) { ++ throw (ThreadDeath)thr; ++ } ++ ++ return true; ++ } ++ ++ task.complete(task.fromChunk, null); ++ return true; ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5ff994d5af24b0bdd7b3a16e245b2c4100bef3f0 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java +@@ -0,0 +1,484 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.dataconverter.minecraft.MCDataConverter; ++import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.chunk.system.io.RegionFileIOThread; ++import io.papermc.paper.chunk.system.poi.PoiChunk; ++import net.minecraft.SharedConstants; ++import net.minecraft.core.registries.Registries; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.level.ChunkMap; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.chunk.ProtoChunk; ++import net.minecraft.world.level.chunk.UpgradeData; ++import net.minecraft.world.level.chunk.storage.ChunkSerializer; ++import net.minecraft.world.level.chunk.storage.EntityStorage; ++import net.minecraft.world.level.levelgen.blending.BlendingData; ++import org.slf4j.Logger; ++import java.lang.invoke.VarHandle; ++import java.util.Map; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.function.Consumer; ++ ++public final class ChunkLoadTask extends ChunkProgressionTask { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ private final NewChunkHolder chunkHolder; ++ private final ChunkDataLoadTask loadTask; ++ ++ private volatile boolean cancelled; ++ private NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask; ++ private NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask; ++ private GenericDataLoadTask.TaskResult loadResult; ++ private final AtomicInteger taskCountToComplete = new AtomicInteger(3); // one for poi, one for entity, and one for chunk data ++ ++ protected ChunkLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, ++ final NewChunkHolder chunkHolder, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ); ++ this.chunkHolder = chunkHolder; ++ this.loadTask = new ChunkDataLoadTask(scheduler, world, chunkX, chunkZ, priority); ++ this.loadTask.addCallback((final GenericDataLoadTask.TaskResult result) -> { ++ ChunkLoadTask.this.loadResult = result; // must be before getAndDecrement ++ ChunkLoadTask.this.tryCompleteLoad(); ++ }); ++ } ++ ++ private void tryCompleteLoad() { ++ if (this.taskCountToComplete.decrementAndGet() == 0) { ++ final GenericDataLoadTask.TaskResult result = this.cancelled ? null : this.loadResult; // only after the getAndDecrement ++ ChunkLoadTask.this.complete(result == null ? null : result.left(), result == null ? null : result.right()); ++ } ++ } ++ ++ @Override ++ public ChunkStatus getTargetStatus() { ++ return ChunkStatus.EMPTY; ++ } ++ ++ private boolean scheduled; ++ ++ @Override ++ public boolean isScheduled() { ++ return this.scheduled; ++ } ++ ++ @Override ++ public void schedule() { ++ final NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask; ++ final NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask; ++ ++ final Consumer> scheduleLoadTask = (final GenericDataLoadTask.TaskResult result) -> { ++ ChunkLoadTask.this.tryCompleteLoad(); ++ }; ++ ++ // NOTE: it is IMPOSSIBLE for getOrLoadEntityData/getOrLoadPoiData to complete synchronously, because ++ // they must schedule a task to off main or to on main to complete ++ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ if (this.scheduled) { ++ throw new IllegalStateException("schedule() called twice"); ++ } ++ this.scheduled = true; ++ if (this.cancelled) { ++ return; ++ } ++ if (!this.chunkHolder.isEntityChunkNBTLoaded()) { ++ entityLoadTask = this.chunkHolder.getOrLoadEntityData((Consumer)scheduleLoadTask); ++ } else { ++ entityLoadTask = null; ++ this.taskCountToComplete.getAndDecrement(); // we know the chunk load is not done here, as it is not scheduled ++ } ++ ++ if (!this.chunkHolder.isPoiChunkLoaded()) { ++ poiLoadTask = this.chunkHolder.getOrLoadPoiData((Consumer)scheduleLoadTask); ++ } else { ++ poiLoadTask = null; ++ this.taskCountToComplete.getAndDecrement(); // we know the chunk load is not done here, as it is not scheduled ++ } ++ ++ this.entityLoadTask = entityLoadTask; ++ this.poiLoadTask = poiLoadTask; ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ ++ if (entityLoadTask != null) { ++ entityLoadTask.schedule(); ++ } ++ ++ if (poiLoadTask != null) { ++ poiLoadTask.schedule(); ++ } ++ ++ this.loadTask.schedule(false); ++ } ++ ++ @Override ++ public void cancel() { ++ // must be before load task access, so we can synchronise with the writes to the fields ++ final boolean scheduled; ++ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ // must read field here, as it may be written later conucrrently - ++ // we need to know if we scheduled _before_ cancellation ++ scheduled = this.scheduled; ++ this.cancelled = true; ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ ++ /* ++ Note: The entityLoadTask/poiLoadTask do not complete when cancelled, ++ so we need to manually try to complete in those cases ++ It is also important to note that we set the cancelled field first, just in case ++ the chunk load task attempts to complete with a non-null value ++ */ ++ ++ if (scheduled) { ++ // since we scheduled, we need to cancel the tasks ++ if (this.entityLoadTask != null) { ++ if (this.entityLoadTask.cancel()) { ++ this.tryCompleteLoad(); ++ } ++ } ++ if (this.poiLoadTask != null) { ++ if (this.poiLoadTask.cancel()) { ++ this.tryCompleteLoad(); ++ } ++ } ++ } else { ++ // since nothing was scheduled, we need to decrement the task count here ourselves ++ ++ // for entity load task ++ this.tryCompleteLoad(); ++ ++ // for poi load task ++ this.tryCompleteLoad(); ++ } ++ this.loadTask.cancel(); ++ } ++ ++ @Override ++ public PrioritisedExecutor.Priority getPriority() { ++ return this.loadTask.getPriority(); ++ } ++ ++ @Override ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); ++ if (entityLoad != null) { ++ entityLoad.lowerPriority(priority); ++ } ++ ++ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); ++ ++ if (poiLoad != null) { ++ poiLoad.lowerPriority(priority); ++ } ++ ++ this.loadTask.lowerPriority(priority); ++ } ++ ++ @Override ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); ++ if (entityLoad != null) { ++ entityLoad.setPriority(priority); ++ } ++ ++ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); ++ ++ if (poiLoad != null) { ++ poiLoad.setPriority(priority); ++ } ++ ++ this.loadTask.setPriority(priority); ++ } ++ ++ @Override ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); ++ if (entityLoad != null) { ++ entityLoad.raisePriority(priority); ++ } ++ ++ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); ++ ++ if (poiLoad != null) { ++ poiLoad.raisePriority(priority); ++ } ++ ++ this.loadTask.raisePriority(priority); ++ } ++ ++ protected static abstract class CallbackDataLoadTask extends GenericDataLoadTask { ++ ++ private TaskResult result; ++ private final MultiThreadedQueue>> waiters = new MultiThreadedQueue<>(); ++ ++ protected volatile boolean completed; ++ protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(CallbackDataLoadTask.class, "completed", boolean.class); ++ ++ protected CallbackDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final RegionFileIOThread.RegionFileType type, ++ final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ, type, priority); ++ } ++ ++ public void addCallback(final Consumer> consumer) { ++ if (!this.waiters.add(consumer)) { ++ try { ++ consumer.accept(this.result); ++ } catch (final Throwable throwable) { ++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Consumer", ChunkTaskScheduler.stringIfNull(consumer), ++ "Completed throwable", ChunkTaskScheduler.stringIfNull(this.result.right()) ++ ), throwable); ++ if (throwable instanceof ThreadDeath) { ++ throw (ThreadDeath)throwable; ++ } ++ } ++ } ++ } ++ ++ @Override ++ protected void onComplete(final TaskResult result) { ++ if ((boolean)COMPLETED_HANDLE.getAndSet((CallbackDataLoadTask)this, (boolean)true)) { ++ throw new IllegalStateException("Already completed"); ++ } ++ this.result = result; ++ Consumer> consumer; ++ while ((consumer = this.waiters.pollOrBlockAdds()) != null) { ++ try { ++ consumer.accept(result); ++ } catch (final Throwable throwable) { ++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Consumer", ChunkTaskScheduler.stringIfNull(consumer), ++ "Completed throwable", ChunkTaskScheduler.stringIfNull(result.right()) ++ ), throwable); ++ if (throwable instanceof ThreadDeath) { ++ throw (ThreadDeath)throwable; ++ } ++ return; ++ } ++ } ++ } ++ } ++ ++ public static final class ChunkDataLoadTask extends CallbackDataLoadTask { ++ protected ChunkDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.CHUNK_DATA, priority); ++ } ++ ++ @Override ++ protected boolean hasOffMain() { ++ return true; ++ } ++ ++ @Override ++ protected boolean hasOnMain() { ++ return false; ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ return this.scheduler.loadExecutor.createTask(run, priority); ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ protected TaskResult completeOnMainOffMain(final ChunkAccess data, final Throwable throwable) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ private ProtoChunk getEmptyChunk() { ++ return new ProtoChunk( ++ new ChunkPos(this.chunkX, this.chunkZ), UpgradeData.EMPTY, this.world, ++ this.world.registryAccess().registryOrThrow(Registries.BIOME), (BlendingData)null ++ ); ++ } ++ ++ @Override ++ protected TaskResult runOffMain(final CompoundTag data, final Throwable throwable) { ++ if (throwable != null) { ++ LOGGER.error("Failed to load chunk data for task: " + this.toString() + ", chunk data will be lost", throwable); ++ return new TaskResult<>(this.getEmptyChunk(), null); ++ } ++ ++ if (data == null) { ++ return new TaskResult<>(this.getEmptyChunk(), null); ++ } ++ ++ // need to convert data, and then deserialize it ++ ++ try { ++ final ChunkPos chunkPos = new ChunkPos(this.chunkX, this.chunkZ); ++ final ChunkMap chunkMap = this.world.getChunkSource().chunkMap; ++ // run converters ++ // note: upgradeChunkTag copies the data already ++ final CompoundTag converted = chunkMap.upgradeChunkTag( ++ this.world.getTypeKey(), chunkMap.overworldDataStorage, data, chunkMap.generator.getTypeNameForDataFixer(), ++ chunkPos, this.world ++ ); ++ // deserialize ++ final ChunkSerializer.InProgressChunkHolder chunkHolder = ChunkSerializer.readInProgressChunkHolder( ++ this.world, chunkMap.getPoiManager(), chunkPos, converted ++ ); ++ ++ return new TaskResult<>(chunkHolder.protoChunk, null); ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable thr2) { ++ LOGGER.error("Failed to parse chunk data for task: " + this.toString() + ", chunk data will be lost", thr2); ++ return new TaskResult<>(this.getEmptyChunk(), null); ++ } ++ } ++ ++ @Override ++ protected TaskResult runOnMain(final ChunkAccess data, final Throwable throwable) { ++ throw new UnsupportedOperationException(); ++ } ++ } ++ ++ public static final class PoiDataLoadTask extends CallbackDataLoadTask { ++ public PoiDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.POI_DATA, priority); ++ } ++ ++ @Override ++ protected boolean hasOffMain() { ++ return true; ++ } ++ ++ @Override ++ protected boolean hasOnMain() { ++ return false; ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ return this.scheduler.loadExecutor.createTask(run, priority); ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ protected TaskResult completeOnMainOffMain(final PoiChunk data, final Throwable throwable) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ protected TaskResult runOffMain(CompoundTag data, final Throwable throwable) { ++ if (throwable != null) { ++ LOGGER.error("Failed to load poi data for task: " + this.toString() + ", poi data will be lost", throwable); ++ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); ++ } ++ ++ if (data == null || data.isEmpty()) { ++ // nothing to do ++ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); ++ } ++ ++ try { ++ data = data.copy(); // coming from the I/O thread, so we need to copy ++ // run converters ++ final int dataVersion = !data.contains(SharedConstants.DATA_VERSION_TAG, net.minecraft.nbt.Tag.TAG_ANY_NUMERIC) ? 1945 : data.getInt(SharedConstants.DATA_VERSION_TAG); ++ final CompoundTag converted = MCDataConverter.convertTag( ++ MCTypeRegistry.POI_CHUNK, data, dataVersion, SharedConstants.getCurrentVersion().getDataVersion().getVersion() ++ ); ++ ++ // now we need to parse it ++ return new TaskResult<>(PoiChunk.parse(this.world, this.chunkX, this.chunkZ, converted), null); ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable thr2) { ++ LOGGER.error("Failed to run parse poi data for task: " + this.toString() + ", poi data will be lost", thr2); ++ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); ++ } ++ } ++ ++ @Override ++ protected TaskResult runOnMain(final PoiChunk data, final Throwable throwable) { ++ throw new UnsupportedOperationException(); ++ } ++ } ++ ++ public static final class EntityDataLoadTask extends CallbackDataLoadTask { ++ ++ public EntityDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, priority); ++ } ++ ++ @Override ++ protected boolean hasOffMain() { ++ return true; ++ } ++ ++ @Override ++ protected boolean hasOnMain() { ++ return false; ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ return this.scheduler.loadExecutor.createTask(run, priority); ++ } ++ ++ @Override ++ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ protected TaskResult completeOnMainOffMain(final CompoundTag data, final Throwable throwable) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ protected TaskResult runOffMain(final CompoundTag data, final Throwable throwable) { ++ if (throwable != null) { ++ LOGGER.error("Failed to load entity data for task: " + this.toString() + ", entity data will be lost", throwable); ++ return new TaskResult<>(null, null); ++ } ++ ++ if (data == null || data.isEmpty()) { ++ // nothing to do ++ return new TaskResult<>(null, null); ++ } ++ ++ try { ++ // note: data comes from the I/O thread, so we need to copy it ++ return new TaskResult<>(EntityStorage.upgradeChunkTag(data.copy()), null); ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable thr2) { ++ LOGGER.error("Failed to run converters for entity data for task: " + this.toString() + ", entity data will be lost", thr2); ++ return new TaskResult<>(null, thr2); ++ } ++ } ++ ++ @Override ++ protected TaskResult runOnMain(final CompoundTag data, final Throwable throwable) { ++ throw new UnsupportedOperationException(); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkProgressionTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkProgressionTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b2341328bb22f08836ef18785dc27393a36ce8d6 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkProgressionTask.java +@@ -0,0 +1,105 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import java.lang.invoke.VarHandle; ++import java.util.Map; ++import java.util.function.BiConsumer; ++ ++public abstract class ChunkProgressionTask { ++ ++ private final MultiThreadedQueue> waiters = new MultiThreadedQueue<>(); ++ private ChunkAccess completedChunk; ++ private Throwable completedThrowable; ++ ++ protected final ChunkTaskScheduler scheduler; ++ protected final ServerLevel world; ++ protected final int chunkX; ++ protected final int chunkZ; ++ ++ protected volatile boolean completed; ++ protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(ChunkProgressionTask.class, "completed", boolean.class); ++ ++ protected ChunkProgressionTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ) { ++ this.scheduler = scheduler; ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ } ++ ++ // Used only for debug json ++ public abstract boolean isScheduled(); ++ ++ // Note: It is the responsibility of the task to set the chunk's status once it has completed ++ public abstract ChunkStatus getTargetStatus(); ++ ++ /* Only executed once */ ++ /* Implementations must be prepared to handle cases where cancel() is called before schedule() */ ++ public abstract void schedule(); ++ ++ /* May be called multiple times */ ++ public abstract void cancel(); ++ ++ public abstract PrioritisedExecutor.Priority getPriority(); ++ ++ /* Schedule lock is always held for the priority update calls */ ++ ++ public abstract void lowerPriority(final PrioritisedExecutor.Priority priority); ++ ++ public abstract void setPriority(final PrioritisedExecutor.Priority priority); ++ ++ public abstract void raisePriority(final PrioritisedExecutor.Priority priority); ++ ++ public final void onComplete(final BiConsumer onComplete) { ++ if (!this.waiters.add(onComplete)) { ++ try { ++ onComplete.accept(this.completedChunk, this.completedThrowable); ++ } catch (final Throwable throwable) { ++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Consumer", ChunkTaskScheduler.stringIfNull(onComplete), ++ "Completed throwable", ChunkTaskScheduler.stringIfNull(this.completedThrowable) ++ ), throwable); ++ if (throwable instanceof ThreadDeath) { ++ throw (ThreadDeath)throwable; ++ } ++ } ++ } ++ } ++ ++ protected final void complete(final ChunkAccess chunk, final Throwable throwable) { ++ try { ++ this.complete0(chunk, throwable); ++ } catch (final Throwable thr2) { ++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable) ++ ), thr2); ++ if (thr2 instanceof ThreadDeath) { ++ throw (ThreadDeath)thr2; ++ } ++ } ++ } ++ ++ private void complete0(final ChunkAccess chunk, final Throwable throwable) { ++ if ((boolean)COMPLETED_HANDLE.getAndSet((ChunkProgressionTask)this, (boolean)true)) { ++ throw new IllegalStateException("Already completed"); ++ } ++ this.completedChunk = chunk; ++ this.completedThrowable = throwable; ++ ++ BiConsumer consumer; ++ while ((consumer = this.waiters.pollOrBlockAdds()) != null) { ++ consumer.accept(chunk, throwable); ++ } ++ } ++ ++ @Override ++ public String toString() { ++ return "ChunkProgressionTask{class: " + this.getClass().getName() + ", for world: " + this.world.getWorld().getName() + ++ ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() + ++ ", status: " + this.getTargetStatus().toString() + ", scheduled: " + this.isScheduled() + "}"; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkQueue.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkQueue.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4cc1b3ba6d093a9683dbd8b7fe76106ae391e019 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkQueue.java +@@ -0,0 +1,160 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import it.unimi.dsi.fastutil.HashCommon; ++import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Map; ++import java.util.concurrent.ConcurrentHashMap; ++import java.util.concurrent.atomic.AtomicLong; ++ ++public final class ChunkQueue { ++ ++ public final int coordinateShift; ++ private final AtomicLong orderGenerator = new AtomicLong(); ++ private final ConcurrentHashMap unloadSections = new ConcurrentHashMap<>(); ++ ++ /* ++ * Note: write operations do not occur in parallel for any given section. ++ * Note: coordinateShift <= region shift in order for retrieveForCurrentRegion() to function correctly ++ */ ++ ++ public ChunkQueue(final int coordinateShift) { ++ this.coordinateShift = coordinateShift; ++ } ++ ++ public static record SectionToUnload(int sectionX, int sectionZ, Coordinate coord, long order, int count) {} ++ ++ public List retrieveForAllRegions() { ++ final List ret = new ArrayList<>(); ++ ++ for (final Map.Entry entry : this.unloadSections.entrySet()) { ++ final Coordinate coord = entry.getKey(); ++ final long key = coord.key; ++ final UnloadSection section = entry.getValue(); ++ final int sectionX = Coordinate.x(key); ++ final int sectionZ = Coordinate.z(key); ++ ++ ret.add(new SectionToUnload(sectionX, sectionZ, coord, section.order, section.chunks.size())); ++ } ++ ++ ret.sort((final SectionToUnload s1, final SectionToUnload s2) -> { ++ return Long.compare(s1.order, s2.order); ++ }); ++ ++ return ret; ++ } ++ ++ public UnloadSection getSectionUnsynchronized(final int sectionX, final int sectionZ) { ++ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ)); ++ return this.unloadSections.get(coordinate); ++ } ++ ++ public UnloadSection removeSection(final int sectionX, final int sectionZ) { ++ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ)); ++ return this.unloadSections.remove(coordinate); ++ } ++ ++ // write operation ++ public boolean addChunk(final int chunkX, final int chunkZ) { ++ final int shift = this.coordinateShift; ++ final int sectionX = chunkX >> shift; ++ final int sectionZ = chunkZ >> shift; ++ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ)); ++ final long chunkKey = Coordinate.key(chunkX, chunkZ); ++ ++ UnloadSection section = this.unloadSections.get(coordinate); ++ if (section == null) { ++ section = new UnloadSection(this.orderGenerator.getAndIncrement()); ++ // write operations do not occur in parallel for a given section ++ this.unloadSections.put(coordinate, section); ++ } ++ ++ return section.chunks.add(chunkKey); ++ } ++ ++ // write operation ++ public boolean removeChunk(final int chunkX, final int chunkZ) { ++ final int shift = this.coordinateShift; ++ final int sectionX = chunkX >> shift; ++ final int sectionZ = chunkZ >> shift; ++ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ)); ++ final long chunkKey = Coordinate.key(chunkX, chunkZ); ++ ++ final UnloadSection section = this.unloadSections.get(coordinate); ++ ++ if (section == null) { ++ return false; ++ } ++ ++ if (!section.chunks.remove(chunkKey)) { ++ return false; ++ } ++ ++ if (section.chunks.isEmpty()) { ++ this.unloadSections.remove(coordinate); ++ } ++ ++ return true; ++ } ++ ++ public static final class UnloadSection { ++ ++ public final long order; ++ public final LongLinkedOpenHashSet chunks = new LongLinkedOpenHashSet(); ++ ++ public UnloadSection(final long order) { ++ this.order = order; ++ } ++ } ++ ++ private static final class Coordinate implements Comparable { ++ ++ public final long key; ++ ++ public Coordinate(final long key) { ++ this.key = key; ++ } ++ ++ public Coordinate(final int x, final int z) { ++ this.key = key(x, z); ++ } ++ ++ public static long key(final int x, final int z) { ++ return ((long)z << 32) | (x & 0xFFFFFFFFL); ++ } ++ ++ public static int x(final long key) { ++ return (int)key; ++ } ++ ++ public static int z(final long key) { ++ return (int)(key >>> 32); ++ } ++ ++ @Override ++ public int hashCode() { ++ return (int)HashCommon.mix(this.key); ++ } ++ ++ @Override ++ public boolean equals(final Object obj) { ++ if (this == obj) { ++ return true; ++ } ++ ++ if (!(obj instanceof Coordinate other)) { ++ return false; ++ } ++ ++ return this.key == other.key; ++ } ++ ++ // This class is intended for HashMap/ConcurrentHashMap usage, which do treeify bin nodes if the chain ++ // is too large. So we should implement compareTo to help. ++ @Override ++ public int compareTo(final Coordinate other) { ++ return Long.compare(this.key, other.key); ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java +new file mode 100644 +index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f4926d778 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java +@@ -0,0 +1,883 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue; ++import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.chunk.system.io.RegionFileIOThread; ++import io.papermc.paper.chunk.system.scheduling.queue.RadiusAwarePrioritisedExecutor; ++import io.papermc.paper.configuration.GlobalConfiguration; ++import io.papermc.paper.util.CoordinateUtils; ++import io.papermc.paper.util.TickThread; ++import java.util.function.BooleanSupplier; ++import net.minecraft.CrashReport; ++import net.minecraft.CrashReportCategory; ++import net.minecraft.ReportedException; ++import io.papermc.paper.util.MCUtil; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ChunkMap; ++import net.minecraft.server.level.FullChunkStatus; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.chunk.LevelChunk; ++import org.bukkit.Bukkit; ++import org.slf4j.Logger; ++import java.io.File; ++import java.util.ArrayDeque; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Collections; ++import java.util.List; ++import java.util.Map; ++import java.util.Objects; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.concurrent.atomic.AtomicLong; ++import java.util.function.Consumer; ++ ++public final class ChunkTaskScheduler { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ static int newChunkSystemIOThreads; ++ static int newChunkSystemWorkerThreads; ++ static int newChunkSystemGenParallelism; ++ static int newChunkSystemLoadParallelism; ++ ++ public static ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool workerThreads; ++ ++ private static boolean initialised = false; ++ ++ public static void init(final GlobalConfiguration.ChunkSystem config) { ++ if (initialised) { ++ return; ++ } ++ initialised = true; ++ newChunkSystemIOThreads = config.ioThreads; ++ newChunkSystemWorkerThreads = config.workerThreads; ++ if (newChunkSystemIOThreads < 0) { ++ newChunkSystemIOThreads = 1; ++ } else { ++ newChunkSystemIOThreads = Math.max(1, newChunkSystemIOThreads); ++ } ++ int defaultWorkerThreads = Runtime.getRuntime().availableProcessors() / 2; ++ if (defaultWorkerThreads <= 4) { ++ defaultWorkerThreads = defaultWorkerThreads <= 3 ? 1 : 2; ++ } else { ++ defaultWorkerThreads = defaultWorkerThreads / 2; ++ } ++ defaultWorkerThreads = Integer.getInteger("Paper.WorkerThreadCount", Integer.valueOf(defaultWorkerThreads)); ++ ++ if (newChunkSystemWorkerThreads < 0) { ++ newChunkSystemWorkerThreads = defaultWorkerThreads; ++ } else { ++ newChunkSystemWorkerThreads = Math.max(1, newChunkSystemWorkerThreads); ++ } ++ ++ String newChunkSystemGenParallelism = config.genParallelism; ++ if (newChunkSystemGenParallelism.equalsIgnoreCase("default")) { ++ newChunkSystemGenParallelism = "true"; ++ } ++ boolean useParallelGen; ++ if (newChunkSystemGenParallelism.equalsIgnoreCase("on") || newChunkSystemGenParallelism.equalsIgnoreCase("enabled") ++ || newChunkSystemGenParallelism.equalsIgnoreCase("true")) { ++ useParallelGen = true; ++ } else if (newChunkSystemGenParallelism.equalsIgnoreCase("off") || newChunkSystemGenParallelism.equalsIgnoreCase("disabled") ++ || newChunkSystemGenParallelism.equalsIgnoreCase("false")) { ++ useParallelGen = false; ++ } else { ++ throw new IllegalStateException("Invalid option for gen-parallelism: must be one of [on, off, enabled, disabled, true, false, default]"); ++ } ++ ++ ChunkTaskScheduler.newChunkSystemGenParallelism = useParallelGen ? newChunkSystemWorkerThreads : 1; ++ ChunkTaskScheduler.newChunkSystemLoadParallelism = newChunkSystemWorkerThreads; ++ ++ RegionFileIOThread.init(newChunkSystemIOThreads); ++ workerThreads = new ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool( ++ "Paper Chunk System Worker Pool", newChunkSystemWorkerThreads, ++ (final Thread thread, final Integer id) -> { ++ thread.setPriority(Thread.NORM_PRIORITY - 2); ++ thread.setName("Tuinity Chunk System Worker #" + id.intValue()); ++ thread.setUncaughtExceptionHandler(io.papermc.paper.chunk.system.scheduling.NewChunkHolder.CHUNKSYSTEM_UNCAUGHT_EXCEPTION_HANDLER); ++ }, (long)(20.0e6)); // 20ms ++ ++ LOGGER.info("Chunk system is using " + newChunkSystemIOThreads + " I/O threads, " + newChunkSystemWorkerThreads + " worker threads, and gen parallelism of " + ChunkTaskScheduler.newChunkSystemGenParallelism + " threads"); ++ } ++ ++ public final ServerLevel world; ++ public final PrioritisedThreadPool workers; ++ public final RadiusAwarePrioritisedExecutor radiusAwareScheduler; ++ public final PrioritisedThreadPool.PrioritisedPoolExecutor parallelGenExecutor; ++ private final PrioritisedThreadPool.PrioritisedPoolExecutor radiusAwareGenExecutor; ++ public final PrioritisedThreadPool.PrioritisedPoolExecutor loadExecutor; ++ ++ private final PrioritisedThreadedTaskQueue mainThreadExecutor = new PrioritisedThreadedTaskQueue(); ++ ++ public final ChunkHolderManager chunkHolderManager; ++ ++ static { ++ ChunkStatus.EMPTY.writeRadius = 0; ++ ChunkStatus.STRUCTURE_STARTS.writeRadius = 0; ++ ChunkStatus.STRUCTURE_REFERENCES.writeRadius = 0; ++ ChunkStatus.BIOMES.writeRadius = 0; ++ ChunkStatus.NOISE.writeRadius = 0; ++ ChunkStatus.SURFACE.writeRadius = 0; ++ ChunkStatus.CARVERS.writeRadius = 0; ++ ChunkStatus.FEATURES.writeRadius = 1; ++ ChunkStatus.INITIALIZE_LIGHT.writeRadius = 0; ++ ChunkStatus.LIGHT.writeRadius = 2; ++ ChunkStatus.SPAWN.writeRadius = 0; ++ ChunkStatus.FULL.writeRadius = 0; ++ ++ /* ++ It's important that the neighbour read radius is taken into account. If _any_ later status is using some chunk as ++ a neighbour, it must be also safe if that neighbour is being generated. i.e for any status later than FEATURES, ++ for a status to be parallel safe it must not read the block data from its neighbours. ++ */ ++ final List parallelCapableStatus = Arrays.asList( ++ // No-op executor. ++ ChunkStatus.EMPTY, ++ ++ // This is parallel capable, as CB has fixed the concurrency issue with stronghold generations. ++ // Does not touch neighbour chunks. ++ ChunkStatus.STRUCTURE_STARTS, ++ ++ // Surprisingly this is parallel capable. It is simply reading the already-created structure starts ++ // into the structure references for the chunk. So while it reads from it neighbours, its neighbours ++ // will not change, even if executed in parallel. ++ ChunkStatus.STRUCTURE_REFERENCES, ++ ++ // Safe. Mojang runs it in parallel as well. ++ ChunkStatus.BIOMES, ++ ++ // Safe. Mojang runs it in parallel as well. ++ ChunkStatus.NOISE, ++ ++ // Parallel safe. Only touches the target chunk. Biome retrieval is now noise based, which is ++ // completely thread-safe. ++ ChunkStatus.SURFACE, ++ ++ // No global state is modified in the carvers. It only touches the specified chunk. So it is parallel safe. ++ ChunkStatus.CARVERS, ++ ++ // FEATURES is not parallel safe. It writes to neighbours. ++ ++ // no-op executor ++ ChunkStatus.INITIALIZE_LIGHT ++ ++ // LIGHT is not parallel safe. It also doesn't run on the generation executor, so no point. ++ ++ // Only writes to the specified chunk. State is not read by later statuses. Parallel safe. ++ // Note: it may look unsafe because it writes to a worldgenregion, but the region size is always 0 - ++ // see the task margin. ++ // However, if the neighbouring FEATURES chunk is unloaded, but then fails to load in again (for whatever ++ // reason), then it would write to this chunk - and since this status reads blocks from itself, it's not ++ // safe to execute this in parallel. ++ // SPAWN ++ ++ // FULL is executed on main. ++ ); ++ ++ for (final ChunkStatus status : parallelCapableStatus) { ++ status.isParallelCapable = true; ++ } ++ } ++ ++ private static final int[] ACCESS_RADIUS_TABLE = new int[ChunkStatus.getStatusList().size()]; ++ private static final int[] MAX_ACCESS_RADIUS_TABLE = new int[ACCESS_RADIUS_TABLE.length]; ++ static { ++ Arrays.fill(ACCESS_RADIUS_TABLE, -1); ++ } ++ ++ private static int getAccessRadius0(final ChunkStatus genStatus) { ++ if (genStatus == ChunkStatus.EMPTY) { ++ return 0; ++ } ++ ++ final int radius = Math.max(genStatus.loadRange, genStatus.getRange()); ++ int maxRange = radius; ++ ++ for (int dist = 1; dist <= radius; ++dist) { ++ final ChunkStatus requiredNeighbourStatus = ChunkMap.getDependencyStatus(genStatus, radius); ++ final int rad = ACCESS_RADIUS_TABLE[requiredNeighbourStatus.getIndex()]; ++ if (rad == -1) { ++ throw new IllegalStateException(); ++ } ++ ++ maxRange = Math.max(maxRange, dist + rad); ++ } ++ ++ return maxRange; ++ } ++ ++ private static int maxAccessRadius; ++ ++ static { ++ final List statuses = ChunkStatus.getStatusList(); ++ for (int i = 0, len = statuses.size(); i < len; ++i) { ++ ACCESS_RADIUS_TABLE[i] = getAccessRadius0(statuses.get(i)); ++ } ++ int max = 0; ++ for (int i = 0, len = statuses.size(); i < len; ++i) { ++ MAX_ACCESS_RADIUS_TABLE[i] = max = Math.max(ACCESS_RADIUS_TABLE[i], max); ++ } ++ maxAccessRadius = max; ++ } ++ ++ public static int getMaxAccessRadius() { ++ return maxAccessRadius; ++ } ++ ++ public static int getAccessRadius(final ChunkStatus genStatus) { ++ return ACCESS_RADIUS_TABLE[genStatus.getIndex()]; ++ } ++ ++ public static int getAccessRadius(final FullChunkStatus status) { ++ return (status.ordinal() - 1) + getAccessRadius(ChunkStatus.FULL); ++ } ++ ++ final ReentrantAreaLock schedulingLockArea; ++ private final int lockShift; ++ ++ public final int getChunkSystemLockShift() { ++ return this.lockShift; ++ } ++ // Folia end - use area based lock to reduce contention ++ ++ public ChunkTaskScheduler(final ServerLevel world, final PrioritisedThreadPool workers) { ++ this.world = world; ++ this.workers = workers; ++ // must be >= region shift (in paper, doesn't exist) and must be >= ticket propagator section shift ++ // it must be >= region shift since the regioniser assumes ticket updates do not occur in parallel for the region sections ++ // it must be >= ticket propagator section shift so that the ticket propagator can assume that owning a position implies owning ++ // the entire section ++ // we just take the max, as we want the smallest shift that satisfies these properties ++ this.lockShift = Math.max(world.getRegionChunkShift(), ThreadedTicketLevelPropagator.SECTION_SHIFT); ++ this.schedulingLockArea = new ReentrantAreaLock(this.getChunkSystemLockShift()); ++ ++ final String worldName = world.getWorld().getName(); ++ this.parallelGenExecutor = workers.createExecutor("Chunk parallel generation executor for world '" + worldName + "'", Math.max(1, newChunkSystemGenParallelism)); ++ this.radiusAwareGenExecutor = ++ newChunkSystemGenParallelism <= 1 ? this.parallelGenExecutor : workers.createExecutor("Chunk radius aware generator for world '" + worldName + "'", newChunkSystemGenParallelism); ++ this.loadExecutor = workers.createExecutor("Chunk load executor for world '" + worldName + "'", newChunkSystemLoadParallelism); ++ this.radiusAwareScheduler = new RadiusAwarePrioritisedExecutor(this.radiusAwareGenExecutor, Math.max(1, newChunkSystemGenParallelism)); ++ this.chunkHolderManager = new ChunkHolderManager(world, this); ++ } ++ ++ private final AtomicBoolean failedChunkSystem = new AtomicBoolean(); ++ ++ public static Object stringIfNull(final Object obj) { ++ return obj == null ? "null" : obj; ++ } ++ ++ public void unrecoverableChunkSystemFailure(final int chunkX, final int chunkZ, final Map objectsOfInterest, final Throwable thr) { ++ final NewChunkHolder holder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ); ++ LOGGER.error("Chunk system error at chunk (" + chunkX + "," + chunkZ + "), holder: " + holder + ", exception:", new Throwable(thr)); ++ ++ if (this.failedChunkSystem.getAndSet(true)) { ++ return; ++ } ++ ++ final ReportedException reportedException = thr instanceof ReportedException ? (ReportedException)thr : new ReportedException(new CrashReport("Chunk system error", thr)); ++ ++ CrashReportCategory crashReportCategory = reportedException.getReport().addCategory("Chunk system details"); ++ crashReportCategory.setDetail("Chunk coordinate", new ChunkPos(chunkX, chunkZ).toString()); ++ crashReportCategory.setDetail("ChunkHolder", Objects.toString(holder)); ++ crashReportCategory.setDetail("unrecoverableChunkSystemFailure caller thread", Thread.currentThread().getName()); ++ ++ crashReportCategory = reportedException.getReport().addCategory("Chunk System Objects of Interest"); ++ for (final Map.Entry entry : objectsOfInterest.entrySet()) { ++ if (entry.getValue() instanceof Throwable thrObject) { ++ crashReportCategory.setDetailError(Objects.toString(entry.getKey()), thrObject); ++ } else { ++ crashReportCategory.setDetail(Objects.toString(entry.getKey()), Objects.toString(entry.getValue())); ++ } ++ } ++ ++ final Runnable crash = () -> { ++ throw new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException); ++ }; ++ ++ // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions ++ this.scheduleChunkTask(chunkX, chunkZ, crash, PrioritisedExecutor.Priority.BLOCKING); ++ // so, make the main thread pick it up ++ MinecraftServer.chunkSystemCrash = new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException); ++ } ++ ++ public boolean executeMainThreadTask() { ++ TickThread.ensureTickThread("Cannot execute main thread task off-main"); ++ return this.mainThreadExecutor.executeTask(); ++ } ++ ++ public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ this.chunkHolderManager.raisePriority(x, z, priority); ++ } ++ ++ public void setPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ this.chunkHolderManager.setPriority(x, z, priority); ++ } ++ ++ public void lowerPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { ++ this.chunkHolderManager.lowerPriority(x, z, priority); ++ } ++ ++ private final AtomicLong chunkLoadCounter = new AtomicLong(); ++ ++ public void scheduleTickingState(final int chunkX, final int chunkZ, final FullChunkStatus toStatus, ++ final boolean addTicket, final PrioritisedExecutor.Priority priority, ++ final Consumer onComplete) { ++ if (!TickThread.isTickThread()) { ++ this.scheduleChunkTask(chunkX, chunkZ, () -> { ++ ChunkTaskScheduler.this.scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ }, priority); ++ return; ++ } ++ final int accessRadius = getAccessRadius(toStatus); ++ if (this.chunkHolderManager.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { ++ throw new IllegalStateException("Cannot schedule chunk load during ticket level update"); ++ } ++ if (this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { ++ throw new IllegalStateException("Cannot schedule chunk loading recursively"); ++ } ++ ++ if (toStatus == FullChunkStatus.INACCESSIBLE) { ++ throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status"); ++ } ++ ++ final int minLevel = 33 - (toStatus.ordinal() - 1); ++ final Long chunkReference = addTicket ? Long.valueOf(this.chunkLoadCounter.getAndIncrement()) : null; ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ if (addTicket) { ++ this.chunkHolderManager.addTicketAtLevel(TicketType.CHUNK_LOAD, chunkKey, minLevel, chunkReference); ++ this.chunkHolderManager.processTicketUpdates(); ++ } ++ ++ final Consumer loadCallback = (final LevelChunk chunk) -> { ++ try { ++ if (onComplete != null) { ++ onComplete.accept(chunk); ++ } ++ } finally { ++ if (addTicket) { ++ ChunkTaskScheduler.this.chunkHolderManager.addAndRemoveTickets(chunkKey, ++ TicketType.UNKNOWN, minLevel, new ChunkPos(chunkKey), ++ TicketType.CHUNK_LOAD, minLevel, chunkReference ++ ); ++ } ++ } ++ }; ++ ++ final boolean scheduled; ++ final LevelChunk chunk; ++ final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); ++ try { ++ final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); ++ try { ++ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); ++ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { ++ scheduled = false; ++ chunk = null; ++ } else { ++ final FullChunkStatus currStatus = chunkHolder.getChunkStatus(); ++ if (currStatus.isOrAfter(toStatus)) { ++ scheduled = false; ++ chunk = (LevelChunk)chunkHolder.getCurrentChunk(); ++ } else { ++ scheduled = true; ++ chunk = null; ++ ++ final int radius = toStatus.ordinal() - 1; // 0 -> BORDER, 1 -> TICKING, 2 -> ENTITY_TICKING ++ for (int dz = -radius; dz <= radius; ++dz) { ++ for (int dx = -radius; dx <= radius; ++dx) { ++ final NewChunkHolder neighbour = ++ (dx | dz) == 0 ? chunkHolder : this.chunkHolderManager.getChunkHolder(dx + chunkX, dz + chunkZ); ++ if (neighbour != null) { ++ neighbour.raisePriority(priority); ++ } ++ } ++ } ++ ++ // ticket level should schedule for us ++ chunkHolder.addFullStatusConsumer(toStatus, loadCallback); ++ } ++ } ++ } finally { ++ this.schedulingLockArea.unlock(schedulingLock); ++ } ++ } finally { ++ this.chunkHolderManager.ticketLockArea.unlock(ticketLock); ++ } ++ ++ if (!scheduled) { ++ // couldn't schedule ++ try { ++ loadCallback.accept(chunk); ++ } catch (final ThreadDeath thr) { ++ throw thr; ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to process chunk full status callback", thr); ++ } ++ } ++ } ++ ++ public void scheduleChunkLoad(final int chunkX, final int chunkZ, final boolean gen, final ChunkStatus toStatus, final boolean addTicket, ++ final PrioritisedExecutor.Priority priority, final Consumer onComplete) { ++ if (gen) { ++ this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ return; ++ } ++ this.scheduleChunkLoad(chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> { ++ if (chunk == null) { ++ onComplete.accept(null); ++ } else { ++ if (chunk.getStatus().isOrAfter(toStatus)) { ++ this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ } else { ++ onComplete.accept(null); ++ } ++ } ++ }); ++ } ++ ++ // only appropriate to use with ServerLevel#syncLoadNonFull ++ public boolean beginChunkLoadForNonFullSync(final int chunkX, final int chunkZ, final ChunkStatus toStatus, ++ final PrioritisedExecutor.Priority priority) { ++ final int accessRadius = getAccessRadius(toStatus); ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ final int minLevel = 33 + ChunkStatus.getDistance(toStatus); ++ final List tasks = new ArrayList<>(); ++ final ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention ++ try { ++ final ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention ++ try { ++ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); ++ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { ++ return false; ++ } else { ++ final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus(); ++ if (genStatus != null && genStatus.isOrAfter(toStatus)) { ++ return true; ++ } else { ++ chunkHolder.raisePriority(priority); ++ ++ if (!chunkHolder.upgradeGenTarget(toStatus)) { ++ this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks); ++ } ++ } ++ } ++ } finally { ++ this.schedulingLockArea.unlock(schedulingLock); ++ } ++ } finally { ++ this.chunkHolderManager.ticketLockArea.unlock(ticketLock); ++ } ++ ++ for (int i = 0, len = tasks.size(); i < len; ++i) { ++ tasks.get(i).schedule(); ++ } ++ ++ return true; ++ } ++ ++ public void scheduleChunkLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus, final boolean addTicket, ++ final PrioritisedExecutor.Priority priority, final Consumer onComplete) { ++ if (!TickThread.isTickThread()) { ++ this.scheduleChunkTask(chunkX, chunkZ, () -> { ++ ChunkTaskScheduler.this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); ++ }, priority); ++ return; ++ } ++ final int accessRadius = getAccessRadius(toStatus); ++ if (this.chunkHolderManager.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { ++ throw new IllegalStateException("Cannot schedule chunk load during ticket level update"); ++ } ++ if (this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { ++ throw new IllegalStateException("Cannot schedule chunk loading recursively"); ++ } ++ ++ if (toStatus == ChunkStatus.FULL) { ++ this.scheduleTickingState(chunkX, chunkZ, FullChunkStatus.FULL, addTicket, priority, (Consumer)onComplete); ++ return; ++ } ++ ++ final int minLevel = 33 + ChunkStatus.getDistance(toStatus); ++ final Long chunkReference = addTicket ? Long.valueOf(this.chunkLoadCounter.getAndIncrement()) : null; ++ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ ++ if (addTicket) { ++ this.chunkHolderManager.addTicketAtLevel(TicketType.CHUNK_LOAD, chunkKey, minLevel, chunkReference); ++ this.chunkHolderManager.processTicketUpdates(); ++ } ++ ++ final Consumer loadCallback = (final ChunkAccess chunk) -> { ++ try { ++ if (onComplete != null) { ++ onComplete.accept(chunk); ++ } ++ } finally { ++ if (addTicket) { ++ ChunkTaskScheduler.this.chunkHolderManager.addAndRemoveTickets(chunkKey, ++ TicketType.UNKNOWN, minLevel, new ChunkPos(chunkKey), ++ TicketType.CHUNK_LOAD, minLevel, chunkReference ++ ); ++ } ++ } ++ }; ++ ++ final List tasks = new ArrayList<>(); ++ ++ final boolean scheduled; ++ final ChunkAccess chunk; ++ final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); ++ try { ++ final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); ++ try { ++ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); ++ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { ++ scheduled = false; ++ chunk = null; ++ } else { ++ final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus(); ++ if (genStatus != null && genStatus.isOrAfter(toStatus)) { ++ scheduled = false; ++ chunk = chunkHolder.getCurrentChunk(); ++ } else { ++ scheduled = true; ++ chunk = null; ++ chunkHolder.raisePriority(priority); ++ ++ if (!chunkHolder.upgradeGenTarget(toStatus)) { ++ this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks); ++ } ++ chunkHolder.addStatusConsumer(toStatus, loadCallback); ++ } ++ } ++ } finally { ++ this.schedulingLockArea.unlock(schedulingLock); ++ } ++ } finally { ++ this.chunkHolderManager.ticketLockArea.unlock(ticketLock); ++ } ++ ++ for (int i = 0, len = tasks.size(); i < len; ++i) { ++ tasks.get(i).schedule(); ++ } ++ ++ if (!scheduled) { ++ // couldn't schedule ++ try { ++ loadCallback.accept(chunk); ++ } catch (final ThreadDeath thr) { ++ throw thr; ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to process chunk status callback", thr); ++ } ++ } ++ } ++ ++ private ChunkProgressionTask createTask(final int chunkX, final int chunkZ, final ChunkAccess chunk, ++ final NewChunkHolder chunkHolder, final List neighbours, ++ final ChunkStatus toStatus, final PrioritisedExecutor.Priority initialPriority) { ++ if (toStatus == ChunkStatus.EMPTY) { ++ return new ChunkLoadTask(this, this.world, chunkX, chunkZ, chunkHolder, initialPriority); ++ } ++ if (toStatus == ChunkStatus.LIGHT) { ++ return new ChunkLightTask(this, this.world, chunkX, chunkZ, chunk, initialPriority); ++ } ++ if (toStatus == ChunkStatus.FULL) { ++ return new ChunkFullTask(this, this.world, chunkX, chunkZ, chunkHolder, chunk, initialPriority); ++ } ++ ++ return new ChunkUpgradeGenericStatusTask(this, this.world, chunkX, chunkZ, chunk, neighbours, toStatus, initialPriority); ++ } ++ ++ ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus, final NewChunkHolder chunkHolder, ++ final List allTasks) { ++ return this.schedule(chunkX, chunkZ, targetStatus, chunkHolder, allTasks, chunkHolder.getEffectivePriority()); ++ } ++ ++ // rets new task scheduled for the _specified_ chunk ++ // note: this must hold the scheduling lock ++ // minPriority is only used to pass the priority through to neighbours, as priority calculation has not yet been done ++ // schedule will ignore the generation target, so it should be checked by the caller to ensure the target is not regressed! ++ private ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus, ++ final NewChunkHolder chunkHolder, final List allTasks, ++ final PrioritisedExecutor.Priority minPriority) { ++ if (!this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, getAccessRadius(targetStatus))) { ++ throw new IllegalStateException("Not holding scheduling lock"); ++ } ++ ++ if (chunkHolder.hasGenerationTask()) { ++ chunkHolder.upgradeGenTarget(targetStatus); ++ return null; ++ } ++ ++ final PrioritisedExecutor.Priority requestedPriority = PrioritisedExecutor.Priority.max(minPriority, chunkHolder.getEffectivePriority()); ++ final ChunkStatus currentGenStatus = chunkHolder.getCurrentGenStatus(); ++ final ChunkAccess chunk = chunkHolder.getCurrentChunk(); ++ ++ if (currentGenStatus == null) { ++ // not yet loaded ++ final ChunkProgressionTask task = this.createTask( ++ chunkX, chunkZ, chunk, chunkHolder, Collections.emptyList(), ChunkStatus.EMPTY, requestedPriority ++ ); ++ ++ allTasks.add(task); ++ ++ final List chunkHolderNeighbours = new ArrayList<>(1); ++ chunkHolderNeighbours.add(chunkHolder); ++ ++ chunkHolder.setGenerationTarget(targetStatus); ++ chunkHolder.setGenerationTask(task, ChunkStatus.EMPTY, chunkHolderNeighbours); ++ ++ return task; ++ } ++ ++ if (currentGenStatus.isOrAfter(targetStatus)) { ++ // nothing to do ++ return null; ++ } ++ ++ // we know for sure now that we want to schedule _something_, so set the target ++ chunkHolder.setGenerationTarget(targetStatus); ++ ++ final ChunkStatus chunkRealStatus = chunk.getStatus(); ++ final ChunkStatus toStatus = currentGenStatus.getNextStatus(); ++ ++ // if this chunk has already generated up to or past the specified status, then we don't ++ // need the neighbours AT ALL. ++ final int neighbourReadRadius = chunkRealStatus.isOrAfter(toStatus) ? toStatus.loadRange : toStatus.getRange(); ++ ++ boolean unGeneratedNeighbours = false; ++ ++ // copied from MCUtil.getSpiralOutChunks ++ for (int r = 1; r <= neighbourReadRadius; r++) { ++ int x = -r; ++ int z = r; ++ ++ // Iterates the edge of half of the box; then negates for other half. ++ while (x <= r && z > -r) { ++ final int radius = Math.max(Math.abs(x), Math.abs(z)); ++ final ChunkStatus requiredNeighbourStatus = ChunkMap.getDependencyStatus(toStatus, radius); ++ ++ unGeneratedNeighbours |= this.checkNeighbour( ++ chunkX + x, chunkZ + z, requiredNeighbourStatus, chunkHolder, allTasks, requestedPriority ++ ); ++ unGeneratedNeighbours |= this.checkNeighbour( ++ chunkX - x, chunkZ - z, requiredNeighbourStatus, chunkHolder, allTasks, requestedPriority ++ ); ++ ++ if (x < r) { ++ x++; ++ } else { ++ z--; ++ } ++ } ++ } ++ ++ if (unGeneratedNeighbours) { ++ // can't schedule, but neighbour completion will schedule for us when they're ALL done ++ ++ // propagate our priority to neighbours ++ chunkHolder.recalculateNeighbourPriorities(); ++ return null; ++ } ++ ++ // need to gather neighbours ++ ++ final List neighbours; ++ final List chunkHolderNeighbours; ++ if (neighbourReadRadius <= 0) { ++ neighbours = new ArrayList<>(1); ++ chunkHolderNeighbours = new ArrayList<>(1); ++ neighbours.add(chunk); ++ chunkHolderNeighbours.add(chunkHolder); ++ } else { ++ // the iteration order is _very_ important, as all generation statuses expect a certain order such that: ++ // chunkAtRelative = neighbours.get(relX + relZ * (2 * radius + 1)) ++ neighbours = new ArrayList<>((2 * neighbourReadRadius + 1) * (2 * neighbourReadRadius + 1)); ++ chunkHolderNeighbours = new ArrayList<>((2 * neighbourReadRadius + 1) * (2 * neighbourReadRadius + 1)); ++ for (int dz = -neighbourReadRadius; dz <= neighbourReadRadius; ++dz) { ++ for (int dx = -neighbourReadRadius; dx <= neighbourReadRadius; ++dx) { ++ final NewChunkHolder holder = (dx | dz) == 0 ? chunkHolder : this.chunkHolderManager.getChunkHolder(dx + chunkX, dz + chunkZ); ++ neighbours.add(holder.getChunkForNeighbourAccess()); ++ chunkHolderNeighbours.add(holder); ++ } ++ } ++ } ++ ++ final ChunkProgressionTask task = this.createTask(chunkX, chunkZ, chunk, chunkHolder, neighbours, toStatus, chunkHolder.getEffectivePriority()); ++ allTasks.add(task); ++ ++ chunkHolder.setGenerationTask(task, toStatus, chunkHolderNeighbours); ++ ++ return task; ++ } ++ ++ // rets true if the neighbour is not at the required status, false otherwise ++ private boolean checkNeighbour(final int chunkX, final int chunkZ, final ChunkStatus requiredStatus, final NewChunkHolder center, ++ final List tasks, final PrioritisedExecutor.Priority minPriority) { ++ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ); ++ ++ if (chunkHolder == null) { ++ throw new IllegalStateException("Missing chunkholder when required"); ++ } ++ ++ final ChunkStatus holderStatus = chunkHolder.getCurrentGenStatus(); ++ if (holderStatus != null && holderStatus.isOrAfter(requiredStatus)) { ++ return false; ++ } ++ ++ if (chunkHolder.hasFailedGeneration()) { ++ return true; ++ } ++ ++ center.addGenerationBlockingNeighbour(chunkHolder); ++ chunkHolder.addWaitingNeighbour(center, requiredStatus); ++ ++ if (chunkHolder.upgradeGenTarget(requiredStatus)) { ++ return true; ++ } ++ ++ // not at status required, so we need to schedule its generation ++ this.schedule( ++ chunkX, chunkZ, requiredStatus, chunkHolder, tasks, minPriority ++ ); ++ ++ return true; ++ } ++ ++ /** ++ * @deprecated Chunk tasks must be tied to coordinates in the future ++ */ ++ @Deprecated ++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run) { ++ return this.scheduleChunkTask(run, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ /** ++ * @deprecated Chunk tasks must be tied to coordinates in the future ++ */ ++ @Deprecated ++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ return this.mainThreadExecutor.queueRunnable(run, priority); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) { ++ return this.createChunkTask(chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run, ++ final PrioritisedExecutor.Priority priority) { ++ return this.mainThreadExecutor.createTask(run, priority); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) { ++ return this.mainThreadExecutor.queueRunnable(run); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run, ++ final PrioritisedExecutor.Priority priority) { ++ return this.mainThreadExecutor.queueRunnable(run, priority); ++ } ++ ++ public void executeTasksUntil(final BooleanSupplier exit) { ++ if (Bukkit.isPrimaryThread()) { ++ this.mainThreadExecutor.executeConditionally(exit); ++ } else { ++ long counter = 1L; ++ while (!exit.getAsBoolean()) { ++ counter = ConcurrentUtil.linearLongBackoff(counter, 100_000L, 5_000_000L); // 100us, 5ms ++ } ++ } ++ } ++ ++ public boolean halt(final boolean sync, final long maxWaitNS) { ++ this.radiusAwareGenExecutor.halt(); ++ this.parallelGenExecutor.halt(); ++ this.loadExecutor.halt(); ++ final long time = System.nanoTime(); ++ if (sync) { ++ for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) { ++ if ( ++ !this.radiusAwareGenExecutor.isActive() && ++ !this.parallelGenExecutor.isActive() && ++ !this.loadExecutor.isActive() ++ ) { ++ return true; ++ } ++ if ((System.nanoTime() - time) >= maxWaitNS) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ public static final ArrayDeque WAITING_CHUNKS = new ArrayDeque<>(); // stack ++ ++ public static final class ChunkInfo { ++ ++ public final int chunkX; ++ public final int chunkZ; ++ public final ServerLevel world; ++ ++ public ChunkInfo(final int chunkX, final int chunkZ, final ServerLevel world) { ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.world = world; ++ } ++ ++ @Override ++ public String toString() { ++ return "[( " + this.chunkX + "," + this.chunkZ + ") in '" + this.world.getWorld().getName() + "']"; ++ } ++ } ++ ++ public static void pushChunkWait(final ServerLevel world, final int chunkX, final int chunkZ) { ++ synchronized (WAITING_CHUNKS) { ++ WAITING_CHUNKS.push(new ChunkInfo(chunkX, chunkZ, world)); ++ } ++ } ++ ++ public static void popChunkWait() { ++ synchronized (WAITING_CHUNKS) { ++ WAITING_CHUNKS.pop(); ++ } ++ } ++ ++ public static ChunkInfo[] getChunkInfos() { ++ synchronized (WAITING_CHUNKS) { ++ return WAITING_CHUNKS.toArray(new ChunkInfo[0]); ++ } ++ } ++ ++ public static void dumpAllChunkLoadInfo(final boolean longPrint) { ++ final ChunkInfo[] chunkInfos = getChunkInfos(); ++ if (chunkInfos.length > 0) { ++ LOGGER.error("Chunk wait task info below: "); ++ for (final ChunkInfo chunkInfo : chunkInfos) { ++ final NewChunkHolder holder = chunkInfo.world.chunkTaskScheduler.chunkHolderManager.getChunkHolder(chunkInfo.chunkX, chunkInfo.chunkZ); ++ LOGGER.error("Chunk wait: " + chunkInfo); ++ LOGGER.error("Chunk holder: " + holder); ++ } ++ ++ if (longPrint) { ++ final File file = new File(new File(new File("."), "debug"), "chunks-watchdog.txt"); ++ LOGGER.error("Writing chunk information dump to " + file); ++ try { ++ MCUtil.dumpChunks(file, true); ++ LOGGER.error("Successfully written chunk information!"); ++ } catch (final Throwable thr) { ++ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr); ++ } ++ } ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkUpgradeGenericStatusTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkUpgradeGenericStatusTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..bd0d0c4436f357392e13d9efd4412886385a6924 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkUpgradeGenericStatusTask.java +@@ -0,0 +1,214 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import com.mojang.logging.LogUtils; ++import net.minecraft.server.level.ChunkMap; ++import net.minecraft.server.level.ChunkResult; ++import net.minecraft.server.level.ServerChunkCache; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.chunk.ProtoChunk; ++import net.minecraft.world.level.chunk.status.WorldGenContext; ++import org.slf4j.Logger; ++import java.lang.invoke.VarHandle; ++import java.util.List; ++import java.util.Map; ++import java.util.concurrent.CompletableFuture; ++ ++public final class ChunkUpgradeGenericStatusTask extends ChunkProgressionTask implements Runnable { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ protected final ChunkAccess fromChunk; ++ protected final ChunkStatus fromStatus; ++ protected final ChunkStatus toStatus; ++ protected final List neighbours; ++ ++ protected final PrioritisedExecutor.PrioritisedTask generateTask; ++ ++ public ChunkUpgradeGenericStatusTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final ChunkAccess chunk, final List neighbours, ++ final ChunkStatus toStatus, final PrioritisedExecutor.Priority priority) { ++ super(scheduler, world, chunkX, chunkZ); ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.fromChunk = chunk; ++ this.fromStatus = chunk.getStatus(); ++ this.toStatus = toStatus; ++ this.neighbours = neighbours; ++ if (this.toStatus.isParallelCapable) { ++ this.generateTask = this.scheduler.parallelGenExecutor.createTask(this, priority); ++ } else { ++ this.generateTask = this.scheduler.radiusAwareScheduler.createTask(chunkX, chunkZ, this.toStatus.writeRadius, this, priority); ++ } ++ } ++ ++ @Override ++ public ChunkStatus getTargetStatus() { ++ return this.toStatus; ++ } ++ ++ private boolean isEmptyTask() { ++ // must use fromStatus here to avoid any race condition with run() overwriting the status ++ final boolean generation = !this.fromStatus.isOrAfter(this.toStatus); ++ return (generation && this.toStatus.isEmptyGenStatus()) || (!generation && this.toStatus.isEmptyLoadStatus()); ++ } ++ ++ @Override ++ public void run() { ++ final ChunkAccess chunk = this.fromChunk; ++ ++ final ServerChunkCache serverChunkCache = this.world.chunkSource; ++ final ChunkMap chunkMap = serverChunkCache.chunkMap; ++ ++ final CompletableFuture completeFuture; ++ ++ final boolean generation; ++ boolean completing = false; ++ ++ // note: should optimise the case where the chunk does not need to execute the status, because ++ // schedule() calls this synchronously if it will run through that path ++ ++ final WorldGenContext ctx = new WorldGenContext( ++ this.world, ++ chunkMap.generator, ++ chunkMap.getWorldGenContext().structureManager(), ++ serverChunkCache.getLightEngine() ++ ); ++ try { ++ generation = !chunk.getStatus().isOrAfter(this.toStatus); ++ if (generation) { ++ if (this.toStatus.isEmptyGenStatus()) { ++ if (chunk instanceof ProtoChunk) { ++ ((ProtoChunk)chunk).setStatus(this.toStatus); ++ } ++ completing = true; ++ this.complete(chunk, null); ++ return; ++ } ++ completeFuture = this.toStatus.generate(ctx, Runnable::run, null, this.neighbours) ++ .whenComplete((final ChunkAccess either, final Throwable throwable) -> { ++ if (either instanceof ProtoChunk proto) { ++ proto.setStatus(ChunkUpgradeGenericStatusTask.this.toStatus); ++ } ++ } ++ ); ++ } else { ++ if (this.toStatus.isEmptyLoadStatus()) { ++ completing = true; ++ this.complete(chunk, null); ++ return; ++ } ++ completeFuture = this.toStatus.load(ctx, null, chunk); ++ } ++ } catch (final Throwable throwable) { ++ if (!completing) { ++ this.complete(null, throwable); ++ ++ if (throwable instanceof ThreadDeath) { ++ throw (ThreadDeath)throwable; ++ } ++ return; ++ } ++ ++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Target status", ChunkTaskScheduler.stringIfNull(this.toStatus), ++ "From status", ChunkTaskScheduler.stringIfNull(this.fromStatus), ++ "Generation task", this ++ ), throwable); ++ ++ if (!(throwable instanceof ThreadDeath)) { ++ LOGGER.error("Failed to complete status for chunk: status:" + this.toStatus + ", chunk: (" + this.chunkX + "," + this.chunkZ + "), world: " + this.world.getWorld().getName(), throwable); ++ } else { ++ // ensure the chunk system can respond, then die ++ throw (ThreadDeath)throwable; ++ } ++ return; ++ } ++ ++ if (!completeFuture.isDone() && !this.toStatus.warnedAboutNoImmediateComplete.getAndSet(true)) { ++ LOGGER.warn("Future status not complete after scheduling: " + this.toStatus.toString() + ", generate: " + generation); ++ } ++ ++ final ChunkAccess newChunk; ++ ++ try { ++ newChunk = completeFuture.join(); ++ } catch (final Throwable throwable) { ++ this.complete(null, throwable); ++ // ensure the chunk system can respond, then die ++ if (throwable instanceof ThreadDeath) { ++ throw (ThreadDeath)throwable; ++ } ++ return; ++ } ++ ++ if (newChunk == null) { ++ this.complete(null, new IllegalStateException("Chunk for status: " + ChunkUpgradeGenericStatusTask.this.toStatus.toString() + ", generation: " + generation + " should not be null! Future: " + completeFuture).fillInStackTrace()); ++ return; ++ } ++ ++ this.complete(newChunk, null); ++ } ++ ++ protected volatile boolean scheduled; ++ protected static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkUpgradeGenericStatusTask.class, "scheduled", boolean.class); ++ ++ @Override ++ public boolean isScheduled() { ++ return this.scheduled; ++ } ++ ++ @Override ++ public void schedule() { ++ if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkUpgradeGenericStatusTask)this, true)) { ++ throw new IllegalStateException("Cannot double call schedule()"); ++ } ++ if (this.isEmptyTask()) { ++ if (this.generateTask.cancel()) { ++ this.run(); ++ } ++ } else { ++ this.generateTask.queue(); ++ } ++ } ++ ++ @Override ++ public void cancel() { ++ if (this.generateTask.cancel()) { ++ this.complete(null, null); ++ } ++ } ++ ++ @Override ++ public PrioritisedExecutor.Priority getPriority() { ++ return this.generateTask.getPriority(); ++ } ++ ++ @Override ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.generateTask.lowerPriority(priority); ++ } ++ ++ @Override ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.generateTask.setPriority(priority); ++ } ++ ++ @Override ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.generateTask.raisePriority(priority); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/GenericDataLoadTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/GenericDataLoadTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..396d72c00e47cf1669ae20dc839c1c961b1f262a +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/GenericDataLoadTask.java +@@ -0,0 +1,746 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.completable.Completable; ++import ca.spottedleaf.concurrentutil.executor.Cancellable; ++import ca.spottedleaf.concurrentutil.executor.standard.DelayedPrioritisedTask; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.chunk.system.io.RegionFileIOThread; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.level.ServerLevel; ++import org.slf4j.Logger; ++import java.lang.invoke.VarHandle; ++import java.util.Map; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.concurrent.atomic.AtomicLong; ++import java.util.function.BiConsumer; ++ ++public abstract class GenericDataLoadTask { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ protected static final CompoundTag CANCELLED_DATA = new CompoundTag(); ++ ++ // reference count is the upper 32 bits ++ protected final AtomicLong stageAndReferenceCount = new AtomicLong(STAGE_NOT_STARTED); ++ ++ protected static final long STAGE_MASK = 0xFFFFFFFFL; ++ protected static final long STAGE_CANCELLED = 0xFFFFFFFFL; ++ protected static final long STAGE_NOT_STARTED = 0L; ++ protected static final long STAGE_LOADING = 1L; ++ protected static final long STAGE_PROCESSING = 2L; ++ protected static final long STAGE_COMPLETED = 3L; ++ ++ // for loading data off disk ++ protected final LoadDataFromDiskTask loadDataFromDiskTask; ++ // processing off-main ++ protected final PrioritisedExecutor.PrioritisedTask processOffMain; ++ // processing on-main ++ protected final PrioritisedExecutor.PrioritisedTask processOnMain; ++ ++ protected final ChunkTaskScheduler scheduler; ++ protected final ServerLevel world; ++ protected final int chunkX; ++ protected final int chunkZ; ++ protected final RegionFileIOThread.RegionFileType type; ++ ++ public GenericDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, ++ final int chunkZ, final RegionFileIOThread.RegionFileType type, ++ final PrioritisedExecutor.Priority priority) { ++ this.scheduler = scheduler; ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.type = type; ++ ++ final ProcessOnMainTask mainTask; ++ if (this.hasOnMain()) { ++ mainTask = new ProcessOnMainTask(); ++ this.processOnMain = this.createOnMain(mainTask, priority); ++ } else { ++ mainTask = null; ++ this.processOnMain = null; ++ } ++ ++ final ProcessOffMainTask offMainTask; ++ if (this.hasOffMain()) { ++ offMainTask = new ProcessOffMainTask(mainTask); ++ this.processOffMain = this.createOffMain(offMainTask, priority); ++ } else { ++ offMainTask = null; ++ this.processOffMain = null; ++ } ++ ++ if (this.processOffMain == null && this.processOnMain == null) { ++ throw new IllegalStateException("Illegal class implementation: " + this.getClass().getName() + ", should be able to schedule at least one task!"); ++ } ++ ++ this.loadDataFromDiskTask = new LoadDataFromDiskTask(world, chunkX, chunkZ, type, new DataLoadCallback(offMainTask, mainTask), priority); ++ } ++ ++ public static final record TaskResult(L left, R right) {} ++ ++ protected abstract boolean hasOffMain(); ++ ++ protected abstract boolean hasOnMain(); ++ ++ protected abstract PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority); ++ ++ protected abstract PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority); ++ ++ protected abstract TaskResult runOffMain(final CompoundTag data, final Throwable throwable); ++ ++ protected abstract TaskResult runOnMain(final OnMain data, final Throwable throwable); ++ ++ protected abstract void onComplete(final TaskResult result); ++ ++ protected abstract TaskResult completeOnMainOffMain(final OnMain data, final Throwable throwable); ++ ++ @Override ++ public String toString() { ++ return "GenericDataLoadTask{class: " + this.getClass().getName() + ", world: " + this.world.getWorld().getName() + ++ ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() + ++ ", type: " + this.type.toString() + "}"; ++ } ++ ++ public PrioritisedExecutor.Priority getPriority() { ++ if (this.processOnMain != null) { ++ return this.processOnMain.getPriority(); ++ } else { ++ return this.processOffMain.getPriority(); ++ } ++ } ++ ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ // can't lower I/O tasks, we don't know what they affect ++ if (this.processOffMain != null) { ++ this.processOffMain.lowerPriority(priority); ++ } ++ if (this.processOnMain != null) { ++ this.processOnMain.lowerPriority(priority); ++ } ++ } ++ ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ // can't lower I/O tasks, we don't know what they affect ++ this.loadDataFromDiskTask.raisePriority(priority); ++ if (this.processOffMain != null) { ++ this.processOffMain.setPriority(priority); ++ } ++ if (this.processOnMain != null) { ++ this.processOnMain.setPriority(priority); ++ } ++ } ++ ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ // can't lower I/O tasks, we don't know what they affect ++ this.loadDataFromDiskTask.raisePriority(priority); ++ if (this.processOffMain != null) { ++ this.processOffMain.raisePriority(priority); ++ } ++ if (this.processOnMain != null) { ++ this.processOnMain.raisePriority(priority); ++ } ++ } ++ ++ // returns whether scheduleNow() needs to be called ++ public boolean schedule(final boolean delay) { ++ if (this.stageAndReferenceCount.get() != STAGE_NOT_STARTED || ++ !this.stageAndReferenceCount.compareAndSet(STAGE_NOT_STARTED, (1L << 32) | STAGE_LOADING)) { ++ // try and increment reference count ++ int failures = 0; ++ for (long curr = this.stageAndReferenceCount.get();;) { ++ if ((curr & STAGE_MASK) == STAGE_CANCELLED || (curr & STAGE_MASK) == STAGE_COMPLETED) { ++ // cancelled or completed, nothing to do here ++ return false; ++ } ++ ++ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, curr + (1L << 32)))) { ++ // successful ++ return false; ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ if (!delay) { ++ this.scheduleNow(); ++ return false; ++ } ++ return true; ++ } ++ ++ public void scheduleNow() { ++ this.loadDataFromDiskTask.schedule(); // will schedule the rest ++ } ++ ++ // assumes the current stage cannot be completed ++ // returns false if cancelled, returns true if can proceed ++ private boolean advanceStage(final long expect, final long to) { ++ int failures = 0; ++ for (long curr = this.stageAndReferenceCount.get();;) { ++ if ((curr & STAGE_MASK) != expect) { ++ // must be cancelled ++ return false; ++ } ++ ++ final long newVal = (curr & ~STAGE_MASK) | to; ++ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { ++ return true; ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public boolean cancel() { ++ int failures = 0; ++ for (long curr = this.stageAndReferenceCount.get();;) { ++ if ((curr & STAGE_MASK) == STAGE_COMPLETED || (curr & STAGE_MASK) == STAGE_CANCELLED) { ++ return false; ++ } ++ ++ if ((curr & STAGE_MASK) == STAGE_NOT_STARTED || (curr & ~STAGE_MASK) == (1L << 32)) { ++ // no other references, so we can cancel ++ final long newVal = STAGE_CANCELLED; ++ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { ++ this.loadDataFromDiskTask.cancel(); ++ if (this.processOffMain != null) { ++ this.processOffMain.cancel(); ++ } ++ if (this.processOnMain != null) { ++ this.processOnMain.cancel(); ++ } ++ this.onComplete(null); ++ return true; ++ } ++ } else { ++ if ((curr & ~STAGE_MASK) == (0L << 32)) { ++ throw new IllegalStateException("Reference count cannot be zero here"); ++ } ++ // just decrease the reference count ++ final long newVal = curr - (1L << 32); ++ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { ++ return false; ++ } ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ protected final class DataLoadCallback implements BiConsumer { ++ ++ protected final ProcessOffMainTask offMainTask; ++ protected final ProcessOnMainTask onMainTask; ++ ++ public DataLoadCallback(final ProcessOffMainTask offMainTask, final ProcessOnMainTask onMainTask) { ++ this.offMainTask = offMainTask; ++ this.onMainTask = onMainTask; ++ } ++ ++ @Override ++ public void accept(final CompoundTag compoundTag, final Throwable throwable) { ++ if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) { ++ // don't try to schedule further ++ return; ++ } ++ ++ try { ++ if (compoundTag == CANCELLED_DATA) { ++ // cancelled, except this isn't possible ++ LOGGER.error("Data callback says cancelled, but stage does not?"); ++ return; ++ } ++ ++ // get off of the regionfile callback ASAP, no clue what locks are held right now... ++ if (GenericDataLoadTask.this.processOffMain != null) { ++ this.offMainTask.data = compoundTag; ++ this.offMainTask.throwable = throwable; ++ GenericDataLoadTask.this.processOffMain.queue(); ++ return; ++ } else { ++ // no off-main task, so go straight to main ++ this.onMainTask.data = (OnMain)compoundTag; ++ this.onMainTask.throwable = throwable; ++ GenericDataLoadTask.this.processOnMain.queue(); ++ } ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable thr2) { ++ LOGGER.error("Failed I/O callback for task: " + GenericDataLoadTask.this.toString(), thr2); ++ GenericDataLoadTask.this.scheduler.unrecoverableChunkSystemFailure( ++ GenericDataLoadTask.this.chunkX, GenericDataLoadTask.this.chunkZ, Map.of( ++ "Callback throwable", ChunkTaskScheduler.stringIfNull(throwable) ++ ), thr2); ++ } ++ } ++ } ++ ++ protected final class ProcessOffMainTask implements Runnable { ++ ++ protected CompoundTag data; ++ protected Throwable throwable; ++ protected final ProcessOnMainTask schedule; ++ ++ public ProcessOffMainTask(final ProcessOnMainTask schedule) { ++ this.schedule = schedule; ++ } ++ ++ @Override ++ public void run() { ++ if (!GenericDataLoadTask.this.advanceStage(STAGE_LOADING, this.schedule == null ? STAGE_COMPLETED : STAGE_PROCESSING)) { ++ // cancelled ++ return; ++ } ++ final TaskResult newData = GenericDataLoadTask.this.runOffMain(this.data, this.throwable); ++ ++ if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) { ++ // don't try to schedule further ++ return; ++ } ++ ++ if (this.schedule != null) { ++ final TaskResult syncComplete = GenericDataLoadTask.this.completeOnMainOffMain(newData.left, newData.right); ++ ++ if (syncComplete != null) { ++ if (GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) { ++ GenericDataLoadTask.this.onComplete(syncComplete); ++ } // else: cancelled ++ return; ++ } ++ ++ this.schedule.data = newData.left; ++ this.schedule.throwable = newData.right; ++ ++ GenericDataLoadTask.this.processOnMain.queue(); ++ } else { ++ GenericDataLoadTask.this.onComplete((TaskResult)newData); ++ } ++ } ++ } ++ ++ protected final class ProcessOnMainTask implements Runnable { ++ ++ protected OnMain data; ++ protected Throwable throwable; ++ ++ @Override ++ public void run() { ++ if (!GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) { ++ // cancelled ++ return; ++ } ++ final TaskResult result = GenericDataLoadTask.this.runOnMain(this.data, this.throwable); ++ ++ GenericDataLoadTask.this.onComplete(result); ++ } ++ } ++ ++ public static final class LoadDataFromDiskTask { ++ ++ protected volatile int priority; ++ protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(LoadDataFromDiskTask.class, "priority", int.class); ++ ++ protected static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 0; ++ protected static final int PRIORITY_LOAD_SCHEDULED = Integer.MIN_VALUE >>> 1; ++ protected static final int PRIORITY_UNLOAD_SCHEDULED = Integer.MIN_VALUE >>> 2; ++ ++ protected static final int PRIORITY_FLAGS = ~Character.MAX_VALUE; ++ ++ protected final int getPriorityVolatile() { ++ return (int)PRIORITY_HANDLE.getVolatile((LoadDataFromDiskTask)this); ++ } ++ ++ protected final int compareAndExchangePriorityVolatile(final int expect, final int update) { ++ return (int)PRIORITY_HANDLE.compareAndExchange((LoadDataFromDiskTask)this, (int)expect, (int)update); ++ } ++ ++ protected final int getAndOrPriorityVolatile(final int val) { ++ return (int)PRIORITY_HANDLE.getAndBitwiseOr((LoadDataFromDiskTask)this, (int)val); ++ } ++ ++ protected final void setPriorityPlain(final int val) { ++ PRIORITY_HANDLE.set((LoadDataFromDiskTask)this, (int)val); ++ } ++ ++ private final ServerLevel world; ++ private final int chunkX; ++ private final int chunkZ; ++ ++ private final RegionFileIOThread.RegionFileType type; ++ private Cancellable dataLoadTask; ++ private Cancellable dataUnloadCancellable; ++ private DelayedPrioritisedTask dataUnloadTask; ++ ++ private final BiConsumer onComplete; ++ ++ // onComplete should be caller sensitive, it may complete synchronously with schedule() - which does ++ // hold a priority lock. ++ public LoadDataFromDiskTask(final ServerLevel world, final int chunkX, final int chunkZ, ++ final RegionFileIOThread.RegionFileType type, ++ final BiConsumer onComplete, ++ final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.type = type; ++ this.onComplete = onComplete; ++ this.setPriorityPlain(priority.priority); ++ } ++ ++ private void complete(final CompoundTag data, final Throwable throwable) { ++ try { ++ this.onComplete.accept(data, throwable); ++ } catch (final Throwable thr2) { ++ this.world.chunkTaskScheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable), ++ "Regionfile type", ChunkTaskScheduler.stringIfNull(this.type) ++ ), thr2); ++ if (thr2 instanceof ThreadDeath) { ++ throw (ThreadDeath)thr2; ++ } ++ } ++ } ++ ++ protected boolean markExecuting() { ++ return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0; ++ } ++ ++ protected boolean isMarkedExecuted() { ++ return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0; ++ } ++ ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_EXECUTED) != 0) { ++ // cancelled or executed ++ return; ++ } ++ ++ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { ++ RegionFileIOThread.lowerPriority(this.world, this.chunkX, this.chunkZ, this.type, priority); ++ return; ++ } ++ ++ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { ++ if (this.dataUnloadTask != null) { ++ this.dataUnloadTask.lowerPriority(priority); ++ } ++ // no return - we need to propagate priority ++ } ++ ++ if (!priority.isHigherPriority(curr & ~PRIORITY_FLAGS)) { ++ return; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_EXECUTED) != 0) { ++ // cancelled or executed ++ return; ++ } ++ ++ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { ++ RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, priority); ++ return; ++ } ++ ++ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { ++ if (this.dataUnloadTask != null) { ++ this.dataUnloadTask.setPriority(priority); ++ } ++ // no return - we need to propagate priority ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_EXECUTED) != 0) { ++ // cancelled or executed ++ return; ++ } ++ ++ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { ++ RegionFileIOThread.raisePriority(this.world, this.chunkX, this.chunkZ, this.type, priority); ++ return; ++ } ++ ++ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { ++ if (this.dataUnloadTask != null) { ++ this.dataUnloadTask.raisePriority(priority); ++ } ++ // no return - we need to propagate priority ++ } ++ ++ if (!priority.isLowerPriority(curr & ~PRIORITY_FLAGS)) { ++ return; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public void cancel() { ++ if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) { ++ // cancelled or executed already ++ return; ++ } ++ ++ // OK if we miss the field read, the task cannot complete if the cancelled bit is set and ++ // the write to dataLoadTask will check for the cancelled bit ++ if (this.dataUnloadCancellable != null) { ++ this.dataUnloadCancellable.cancel(); ++ } ++ ++ if (this.dataLoadTask != null) { ++ this.dataLoadTask.cancel(); ++ } ++ ++ this.complete(CANCELLED_DATA, null); ++ } ++ ++ private final AtomicBoolean scheduled = new AtomicBoolean(); ++ ++ public void schedule() { ++ if (this.scheduled.getAndSet(true)) { ++ throw new IllegalStateException("schedule() called twice"); ++ } ++ int priority = this.getPriorityVolatile(); ++ ++ if ((priority & PRIORITY_EXECUTED) != 0) { ++ // cancelled ++ return; ++ } ++ ++ final BiConsumer consumer = (final CompoundTag data, final Throwable thr) -> { ++ // because cancelScheduled() cannot actually stop this task from executing in every case, we need ++ // to mark complete here to ensure we do not double complete ++ if (LoadDataFromDiskTask.this.markExecuting()) { ++ LoadDataFromDiskTask.this.complete(data, thr); ++ } // else: cancelled ++ }; ++ ++ final PrioritisedExecutor.Priority initialPriority = PrioritisedExecutor.Priority.getPriority(priority); ++ boolean scheduledUnload = false; ++ ++ final NewChunkHolder holder = this.world.chunkTaskScheduler.chunkHolderManager.getChunkHolder(this.chunkX, this.chunkZ); ++ if (holder != null) { ++ final BiConsumer unloadConsumer = (final CompoundTag data, final Throwable thr) -> { ++ if (data != null) { ++ consumer.accept(data, null); ++ } else { ++ // need to schedule task ++ LoadDataFromDiskTask.this.schedule(false, consumer, PrioritisedExecutor.Priority.getPriority(LoadDataFromDiskTask.this.getPriorityVolatile() & ~PRIORITY_FLAGS)); ++ } ++ }; ++ Cancellable unloadCancellable = null; ++ CompoundTag syncComplete = null; ++ final NewChunkHolder.UnloadTask unloadTask = holder.getUnloadTask(this.type); // can be null if no task exists ++ final Completable unloadCompletable = unloadTask == null ? null : unloadTask.completable(); ++ if (unloadCompletable != null) { ++ unloadCancellable = unloadCompletable.addAsynchronousWaiter(unloadConsumer); ++ if (unloadCancellable == null) { ++ syncComplete = unloadCompletable.getResult(); ++ } ++ } ++ ++ if (syncComplete != null) { ++ consumer.accept(syncComplete, null); ++ return; ++ } ++ ++ if (unloadCancellable != null) { ++ scheduledUnload = true; ++ this.dataUnloadCancellable = unloadCancellable; ++ this.dataUnloadTask = unloadTask.task(); ++ } ++ } ++ ++ this.schedule(scheduledUnload, consumer, initialPriority); ++ } ++ ++ private void schedule(final boolean scheduledUnload, final BiConsumer consumer, final PrioritisedExecutor.Priority initialPriority) { ++ int priority = this.getPriorityVolatile(); ++ ++ if ((priority & PRIORITY_EXECUTED) != 0) { ++ // cancelled ++ return; ++ } ++ ++ if (!scheduledUnload) { ++ this.dataLoadTask = RegionFileIOThread.loadDataAsync( ++ this.world, this.chunkX, this.chunkZ, this.type, consumer, ++ initialPriority.isHigherPriority(PrioritisedExecutor.Priority.NORMAL), initialPriority ++ ); ++ } ++ ++ int failures = 0; ++ for (;;) { ++ if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | (scheduledUnload ? PRIORITY_UNLOAD_SCHEDULED : PRIORITY_LOAD_SCHEDULED)))) { ++ return; ++ } ++ ++ if ((priority & PRIORITY_EXECUTED) != 0) { ++ // cancelled or executed ++ if (this.dataUnloadCancellable != null) { ++ this.dataUnloadCancellable.cancel(); ++ } ++ ++ if (this.dataLoadTask != null) { ++ this.dataLoadTask.cancel(); ++ } ++ return; ++ } ++ ++ if (scheduledUnload) { ++ if (this.dataUnloadTask != null) { ++ this.dataUnloadTask.setPriority(PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS)); ++ } ++ } else { ++ RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS)); ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ /* ++ private static final class LoadDataPriorityHolder extends PriorityHolder { ++ ++ protected final LoadDataFromDiskTask task; ++ ++ protected LoadDataPriorityHolder(final PrioritisedExecutor.Priority priority, final LoadDataFromDiskTask task) { ++ super(priority); ++ this.task = task; ++ } ++ ++ @Override ++ protected void cancelScheduled() { ++ final Cancellable dataLoadTask = this.task.dataLoadTask; ++ if (dataLoadTask != null) { ++ // OK if we miss the field read, the task cannot complete if the cancelled bit is set and ++ // the write to dataLoadTask will check for the cancelled bit ++ this.task.dataLoadTask.cancel(); ++ } ++ this.task.complete(CANCELLED_DATA, null); ++ } ++ ++ @Override ++ protected PrioritisedExecutor.Priority getScheduledPriority() { ++ final LoadDataFromDiskTask task = this.task; ++ return RegionFileIOThread.getPriority(task.world, task.chunkX, task.chunkZ, task.type); ++ } ++ ++ @Override ++ protected void scheduleTask(final PrioritisedExecutor.Priority priority) { ++ final LoadDataFromDiskTask task = this.task; ++ final BiConsumer consumer = (final CompoundTag data, final Throwable thr) -> { ++ // because cancelScheduled() cannot actually stop this task from executing in every case, we need ++ // to mark complete here to ensure we do not double complete ++ if (LoadDataPriorityHolder.this.markExecuting()) { ++ LoadDataPriorityHolder.this.task.complete(data, thr); ++ } // else: cancelled ++ }; ++ task.dataLoadTask = RegionFileIOThread.loadDataAsync( ++ task.world, task.chunkX, task.chunkZ, task.type, consumer, ++ priority.isHigherPriority(PrioritisedExecutor.Priority.NORMAL), priority ++ ); ++ if (this.isMarkedExecuted()) { ++ // if we are marked as completed, it could be: ++ // 1. we were cancelled ++ // 2. the consumer was completed ++ // in the 2nd case, cancel() does nothing ++ // in the 1st case, we ensure cancel() is called as it is possible for the cancelling thread ++ // to miss the field write here ++ task.dataLoadTask.cancel(); ++ } ++ } ++ ++ @Override ++ protected void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority) { ++ final LoadDataFromDiskTask task = this.task; ++ RegionFileIOThread.lowerPriority(task.world, task.chunkX, task.chunkZ, task.type, priority); ++ } ++ ++ @Override ++ protected void setPriorityScheduled(final PrioritisedExecutor.Priority priority) { ++ final LoadDataFromDiskTask task = this.task; ++ RegionFileIOThread.setPriority(task.world, task.chunkX, task.chunkZ, task.type, priority); ++ } ++ ++ @Override ++ protected void raisePriorityScheduled(final PrioritisedExecutor.Priority priority) { ++ final LoadDataFromDiskTask task = this.task; ++ RegionFileIOThread.raisePriority(task.world, task.chunkX, task.chunkZ, task.type, priority); ++ } ++ } ++ */ ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb0db6379a +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java +@@ -0,0 +1,2106 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.completable.Completable; ++import ca.spottedleaf.concurrentutil.executor.Cancellable; ++import ca.spottedleaf.concurrentutil.executor.standard.DelayedPrioritisedTask; ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import com.google.gson.JsonArray; ++import com.google.gson.JsonElement; ++import com.google.gson.JsonObject; ++import com.google.gson.JsonPrimitive; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.chunk.system.io.RegionFileIOThread; ++import io.papermc.paper.chunk.system.poi.PoiChunk; ++import io.papermc.paper.util.CoordinateUtils; ++import io.papermc.paper.util.TickThread; ++import io.papermc.paper.util.WorldUtil; ++import io.papermc.paper.world.ChunkEntitySlices; ++import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; ++import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.ChunkLevel; ++import net.minecraft.server.level.FullChunkStatus; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.chunk.ImposterProtoChunk; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.storage.ChunkSerializer; ++import net.minecraft.world.level.chunk.storage.EntityStorage; ++import org.slf4j.Logger; ++import java.lang.invoke.VarHandle; ++import java.util.ArrayList; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Map; ++import java.util.Objects; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.function.Consumer; ++ ++public final class NewChunkHolder { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ public static final Thread.UncaughtExceptionHandler CHUNKSYSTEM_UNCAUGHT_EXCEPTION_HANDLER = new Thread.UncaughtExceptionHandler() { ++ @Override ++ public void uncaughtException(final Thread thread, final Throwable throwable) { ++ if (!(throwable instanceof ThreadDeath)) { ++ LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable); ++ } ++ } ++ }; ++ ++ public final ServerLevel world; ++ public final int chunkX; ++ public final int chunkZ; ++ ++ public final ChunkTaskScheduler scheduler; ++ ++ // load/unload state ++ ++ // chunk data state ++ ++ private ChunkEntitySlices entityChunk; ++ // entity chunk that is loaded, but not yet deserialized ++ private CompoundTag pendingEntityChunk; ++ ++ ChunkEntitySlices loadInEntityChunk(final boolean transientChunk) { ++ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot sync load entity data off-main"); ++ final CompoundTag entityChunk; ++ final ChunkEntitySlices ret; ++ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ if (this.entityChunk != null && (transientChunk || !this.entityChunk.isTransient())) { ++ return this.entityChunk; ++ } ++ final CompoundTag pendingEntityChunk = this.pendingEntityChunk; ++ if (!transientChunk && pendingEntityChunk == null) { ++ throw new IllegalStateException("Must load entity data from disk before loading in the entity chunk!"); ++ } ++ ++ if (this.entityChunk == null) { ++ ret = this.entityChunk = new ChunkEntitySlices( ++ this.world, this.chunkX, this.chunkZ, this.getChunkStatus(), ++ WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world) ++ ); ++ ++ ret.setTransient(transientChunk); ++ ++ this.world.getEntityLookup().entitySectionLoad(this.chunkX, this.chunkZ, ret); ++ } else { ++ // transientChunk = false here ++ ret = this.entityChunk; ++ this.entityChunk.setTransient(false); ++ } ++ ++ if (!transientChunk) { ++ this.pendingEntityChunk = null; ++ entityChunk = pendingEntityChunk == EMPTY_ENTITY_CHUNK ? null : pendingEntityChunk; ++ } else { ++ entityChunk = null; ++ } ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ ++ if (!transientChunk) { ++ if (entityChunk != null) { ++ final List entities = EntityStorage.readEntities(this.world, entityChunk); ++ ++ this.world.getEntityLookup().addEntityChunkEntities(entities, new ChunkPos(this.chunkX, this.chunkZ)); ++ } ++ } ++ ++ return ret; ++ } ++ ++ // needed to distinguish whether the entity chunk has been read from disk but is empty or whether it has _not_ ++ // been read from disk ++ private static final CompoundTag EMPTY_ENTITY_CHUNK = new CompoundTag(); ++ ++ private ChunkLoadTask.EntityDataLoadTask entityDataLoadTask; ++ // note: if entityDataLoadTask is cancelled, but on its completion entityDataLoadTaskWaiters.size() != 0, ++ // then the task is rescheduled ++ private List entityDataLoadTaskWaiters; ++ ++ public ChunkLoadTask.EntityDataLoadTask getEntityDataLoadTask() { ++ return this.entityDataLoadTask; ++ } ++ ++ // must hold schedule lock for the two below functions ++ ++ // returns only if the data has been loaded from disk, DOES NOT relate to whether it has been deserialized ++ // or added into the world (or even into entityChunk) ++ public boolean isEntityChunkNBTLoaded() { ++ return (this.entityChunk != null && !this.entityChunk.isTransient()) || this.pendingEntityChunk != null; ++ } ++ ++ private void completeEntityLoad(final GenericDataLoadTask.TaskResult result) { ++ final List completeWaiters; ++ ChunkLoadTask.EntityDataLoadTask entityDataLoadTask = null; ++ boolean scheduleEntityTask = false; ++ ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ final List waiters = this.entityDataLoadTaskWaiters; ++ this.entityDataLoadTask = null; ++ if (result != null) { ++ this.entityDataLoadTaskWaiters = null; ++ this.pendingEntityChunk = result.left() == null ? EMPTY_ENTITY_CHUNK : result.left(); ++ if (result.right() != null) { ++ LOGGER.error("Unhandled entity data load exception, data data will be lost: ", result.right()); ++ } ++ ++ for (final GenericDataLoadTaskCallback callback : waiters) { ++ callback.markCompleted(); ++ } ++ ++ completeWaiters = waiters; ++ } else { ++ // cancelled ++ completeWaiters = null; ++ ++ // need to re-schedule? ++ if (waiters.isEmpty()) { ++ this.entityDataLoadTaskWaiters = null; ++ // no tasks to schedule _for_ ++ } else { ++ entityDataLoadTask = this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask( ++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority() ++ ); ++ entityDataLoadTask.addCallback(this::completeEntityLoad); ++ // need one schedule() per waiter ++ for (final GenericDataLoadTaskCallback callback : waiters) { ++ scheduleEntityTask |= entityDataLoadTask.schedule(true); ++ } ++ } ++ } ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ ++ if (scheduleEntityTask) { ++ entityDataLoadTask.scheduleNow(); ++ } ++ ++ // avoid holding the scheduling lock while completing ++ if (completeWaiters != null) { ++ for (final GenericDataLoadTaskCallback callback : completeWaiters) { ++ callback.acceptCompleted(result); ++ } ++ } ++ ++ schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ this.checkUnload(); ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ } ++ ++ // note: it is guaranteed that the consumer cannot be called for the entirety that the schedule lock is held ++ // however, when the consumer is invoked, it will hold the schedule lock ++ public GenericDataLoadTaskCallback getOrLoadEntityData(final Consumer> consumer) { ++ if (this.isEntityChunkNBTLoaded()) { ++ throw new IllegalStateException("Cannot load entity data, it is already loaded"); ++ } ++ // why not just acquire the lock? because the caller NEEDS to call isEntityChunkNBTLoaded before this! ++ if (!this.scheduler.schedulingLockArea.isHeldByCurrentThread(this.chunkX, this.chunkZ)) { ++ throw new IllegalStateException("Must hold scheduling lock"); ++ } ++ ++ final GenericDataLoadTaskCallback ret = new EntityDataLoadTaskCallback((Consumer)consumer, this); ++ ++ if (this.entityDataLoadTask == null) { ++ this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask( ++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority() ++ ); ++ this.entityDataLoadTask.addCallback(this::completeEntityLoad); ++ this.entityDataLoadTaskWaiters = new ArrayList<>(); ++ } ++ this.entityDataLoadTaskWaiters.add(ret); ++ if (this.entityDataLoadTask.schedule(true)) { ++ ret.schedule = this.entityDataLoadTask; ++ } ++ this.checkUnload(); ++ ++ return ret; ++ } ++ ++ private static final class EntityDataLoadTaskCallback extends GenericDataLoadTaskCallback { ++ ++ public EntityDataLoadTaskCallback(final Consumer> consumer, final NewChunkHolder chunkHolder) { ++ super(consumer, chunkHolder); ++ } ++ ++ @Override ++ void internalCancel() { ++ this.chunkHolder.entityDataLoadTaskWaiters.remove(this); ++ this.chunkHolder.entityDataLoadTask.cancel(); ++ } ++ } ++ ++ private PoiChunk poiChunk; ++ ++ private ChunkLoadTask.PoiDataLoadTask poiDataLoadTask; ++ // note: if entityDataLoadTask is cancelled, but on its completion entityDataLoadTaskWaiters.size() != 0, ++ // then the task is rescheduled ++ private List poiDataLoadTaskWaiters; ++ ++ public ChunkLoadTask.PoiDataLoadTask getPoiDataLoadTask() { ++ return this.poiDataLoadTask; ++ } ++ ++ // must hold schedule lock for the two below functions ++ ++ public boolean isPoiChunkLoaded() { ++ return this.poiChunk != null; ++ } ++ ++ private void completePoiLoad(final GenericDataLoadTask.TaskResult result) { ++ final List completeWaiters; ++ ChunkLoadTask.PoiDataLoadTask poiDataLoadTask = null; ++ boolean schedulePoiTask = false; ++ ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ final List waiters = this.poiDataLoadTaskWaiters; ++ this.poiDataLoadTask = null; ++ if (result != null) { ++ this.poiDataLoadTaskWaiters = null; ++ this.poiChunk = result.left(); ++ if (result.right() != null) { ++ LOGGER.error("Unhandled poi load exception, poi data will be lost: ", result.right()); ++ } ++ ++ for (final GenericDataLoadTaskCallback callback : waiters) { ++ callback.markCompleted(); ++ } ++ ++ completeWaiters = waiters; ++ } else { ++ // cancelled ++ completeWaiters = null; ++ ++ // need to re-schedule? ++ if (waiters.isEmpty()) { ++ this.poiDataLoadTaskWaiters = null; ++ // no tasks to schedule _for_ ++ } else { ++ poiDataLoadTask = this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask( ++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority() ++ ); ++ poiDataLoadTask.addCallback(this::completePoiLoad); ++ // need one schedule() per waiter ++ for (final GenericDataLoadTaskCallback callback : waiters) { ++ schedulePoiTask |= poiDataLoadTask.schedule(true); ++ } ++ } ++ } ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ ++ if (schedulePoiTask) { ++ poiDataLoadTask.scheduleNow(); ++ } ++ ++ // avoid holding the scheduling lock while completing ++ if (completeWaiters != null) { ++ for (final GenericDataLoadTaskCallback callback : completeWaiters) { ++ callback.acceptCompleted(result); ++ } ++ } ++ schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ this.checkUnload(); ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ } ++ ++ // note: it is guaranteed that the consumer cannot be called for the entirety that the schedule lock is held ++ // however, when the consumer is invoked, it will hold the schedule lock ++ public GenericDataLoadTaskCallback getOrLoadPoiData(final Consumer> consumer) { ++ if (this.isPoiChunkLoaded()) { ++ throw new IllegalStateException("Cannot load poi data, it is already loaded"); ++ } ++ // why not just acquire the lock? because the caller NEEDS to call isPoiChunkLoaded before this! ++ if (!this.scheduler.schedulingLockArea.isHeldByCurrentThread(this.chunkX, this.chunkZ)) { ++ throw new IllegalStateException("Must hold scheduling lock"); ++ } ++ ++ final GenericDataLoadTaskCallback ret = new PoiDataLoadTaskCallback((Consumer)consumer, this); ++ ++ if (this.poiDataLoadTask == null) { ++ this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask( ++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority() ++ ); ++ this.poiDataLoadTask.addCallback(this::completePoiLoad); ++ this.poiDataLoadTaskWaiters = new ArrayList<>(); ++ } ++ this.poiDataLoadTaskWaiters.add(ret); ++ if (this.poiDataLoadTask.schedule(true)) { ++ ret.schedule = this.poiDataLoadTask; ++ } ++ this.checkUnload(); ++ ++ return ret; ++ } ++ ++ private static final class PoiDataLoadTaskCallback extends GenericDataLoadTaskCallback { ++ ++ public PoiDataLoadTaskCallback(final Consumer> consumer, final NewChunkHolder chunkHolder) { ++ super(consumer, chunkHolder); ++ } ++ ++ @Override ++ void internalCancel() { ++ this.chunkHolder.poiDataLoadTaskWaiters.remove(this); ++ this.chunkHolder.poiDataLoadTask.cancel(); ++ } ++ } ++ ++ public static abstract class GenericDataLoadTaskCallback implements Cancellable { ++ ++ protected final Consumer> consumer; ++ protected final NewChunkHolder chunkHolder; ++ protected boolean completed; ++ protected GenericDataLoadTask schedule; ++ protected final AtomicBoolean scheduled = new AtomicBoolean(); ++ ++ public GenericDataLoadTaskCallback(final Consumer> consumer, ++ final NewChunkHolder chunkHolder) { ++ this.consumer = consumer; ++ this.chunkHolder = chunkHolder; ++ } ++ ++ public void schedule() { ++ if (this.scheduled.getAndSet(true)) { ++ throw new IllegalStateException("Double calling schedule()"); ++ } ++ if (this.schedule != null) { ++ this.schedule.scheduleNow(); ++ this.schedule = null; ++ } ++ } ++ ++ boolean isCompleted() { ++ return this.completed; ++ } ++ ++ // must hold scheduling lock ++ private boolean setCompleted() { ++ if (this.completed) { ++ return false; ++ } ++ return this.completed = true; ++ } ++ ++ // must hold scheduling lock ++ void markCompleted() { ++ if (this.completed) { ++ throw new IllegalStateException("May not be completed here"); ++ } ++ this.completed = true; ++ } ++ ++ void acceptCompleted(final GenericDataLoadTask.TaskResult result) { ++ if (result != null) { ++ if (this.completed) { ++ this.consumer.accept(result); ++ } else { ++ throw new IllegalStateException("Cannot be uncompleted at this point"); ++ } ++ } else { ++ throw new NullPointerException("Result cannot be null (cancelled)"); ++ } ++ } ++ ++ // holds scheduling lock ++ abstract void internalCancel(); ++ ++ @Override ++ public boolean cancel() { ++ final NewChunkHolder holder = this.chunkHolder; // Folia - use area based lock to reduce contention ++ final ReentrantAreaLock.Node schedulingLock = holder.scheduler.schedulingLockArea.lock(holder.chunkX, holder.chunkZ); ++ try { ++ if (!this.completed) { ++ this.completed = true; ++ this.internalCancel(); ++ return true; ++ } ++ return false; ++ } finally { ++ holder.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ } ++ } ++ ++ private ChunkAccess currentChunk; ++ ++ // generation status state ++ ++ /** ++ * Current status the chunk has been brought up to by the chunk system. null indicates no work at all ++ */ ++ private ChunkStatus currentGenStatus; ++ ++ // This allows unsynchronised access to the chunk and last gen status ++ private volatile ChunkCompletion lastChunkCompletion; ++ ++ public ChunkCompletion getLastChunkCompletion() { ++ return this.lastChunkCompletion; ++ } ++ ++ public static final record ChunkCompletion(ChunkAccess chunk, ChunkStatus genStatus) {}; ++ ++ /** ++ * The target final chunk status the chunk system will bring the chunk to. ++ */ ++ private ChunkStatus requestedGenStatus; ++ ++ private ChunkProgressionTask generationTask; ++ private ChunkStatus generationTaskStatus; ++ ++ /** ++ * contains the neighbours that this chunk generation is blocking on ++ */ ++ protected final ReferenceLinkedOpenHashSet neighboursBlockingGenTask = new ReferenceLinkedOpenHashSet<>(4); ++ ++ /** ++ * map of ChunkHolder -> Required Status for this chunk ++ */ ++ protected final Reference2ObjectLinkedOpenHashMap neighboursWaitingForUs = new Reference2ObjectLinkedOpenHashMap<>(); ++ ++ public void addGenerationBlockingNeighbour(final NewChunkHolder neighbour) { ++ this.neighboursBlockingGenTask.add(neighbour); ++ } ++ ++ public void addWaitingNeighbour(final NewChunkHolder neighbour, final ChunkStatus requiredStatus) { ++ final boolean wasEmpty = this.neighboursWaitingForUs.isEmpty(); ++ this.neighboursWaitingForUs.put(neighbour, requiredStatus); ++ if (wasEmpty) { ++ this.checkUnload(); ++ } ++ } ++ ++ // priority state ++ ++ // the target priority for this chunk to generate at ++ // TODO this will screw over scheduling at lower priorities to neighbours, fix ++ private PrioritisedExecutor.Priority priority = PrioritisedExecutor.Priority.NORMAL; ++ private boolean priorityLocked; ++ ++ // the priority neighbouring chunks have requested this chunk generate at ++ private PrioritisedExecutor.Priority neighbourRequestedPriority = PrioritisedExecutor.Priority.IDLE; ++ ++ public PrioritisedExecutor.Priority getEffectivePriority() { ++ return PrioritisedExecutor.Priority.max(this.priority, this.neighbourRequestedPriority); ++ } ++ ++ protected void recalculateNeighbourRequestedPriority() { ++ if (this.neighboursWaitingForUs.isEmpty()) { ++ this.neighbourRequestedPriority = PrioritisedExecutor.Priority.IDLE; ++ return; ++ } ++ ++ PrioritisedExecutor.Priority max = PrioritisedExecutor.Priority.IDLE; ++ ++ for (final NewChunkHolder holder : this.neighboursWaitingForUs.keySet()) { ++ final PrioritisedExecutor.Priority neighbourPriority = holder.getEffectivePriority(); ++ if (neighbourPriority.isHigherPriority(max)) { ++ max = neighbourPriority; ++ } ++ } ++ ++ final PrioritisedExecutor.Priority current = this.getEffectivePriority(); ++ this.neighbourRequestedPriority = max; ++ final PrioritisedExecutor.Priority next = this.getEffectivePriority(); ++ ++ if (current == next) { ++ return; ++ } ++ ++ // our effective priority has changed, so change our task ++ if (this.generationTask != null) { ++ this.generationTask.setPriority(next); ++ } ++ ++ // now propagate this to our neighbours ++ this.recalculateNeighbourPriorities(); ++ } ++ ++ public void recalculateNeighbourPriorities() { ++ for (final NewChunkHolder holder : this.neighboursBlockingGenTask) { ++ holder.recalculateNeighbourRequestedPriority(); ++ } ++ } ++ ++ // must hold scheduling lock ++ public void raisePriority(final PrioritisedExecutor.Priority priority) { ++ if (this.priority != null && this.priority.isHigherOrEqualPriority(priority)) { ++ return; ++ } ++ this.setPriority(priority); ++ } ++ ++ private void lockPriority() { ++ this.priority = PrioritisedExecutor.Priority.NORMAL; ++ this.priorityLocked = true; ++ } ++ ++ // must hold scheduling lock ++ public void setPriority(final PrioritisedExecutor.Priority priority) { ++ if (this.priorityLocked) { ++ return; ++ } ++ final PrioritisedExecutor.Priority old = this.getEffectivePriority(); ++ this.priority = priority; ++ final PrioritisedExecutor.Priority newPriority = this.getEffectivePriority(); ++ ++ if (old != newPriority) { ++ if (this.generationTask != null) { ++ this.generationTask.setPriority(newPriority); ++ } ++ } ++ ++ this.recalculateNeighbourPriorities(); ++ } ++ ++ // must hold scheduling lock ++ public void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ if (this.priority != null && this.priority.isLowerOrEqualPriority(priority)) { ++ return; ++ } ++ this.setPriority(priority); ++ } ++ ++ // error handling state ++ private ChunkStatus failedGenStatus; ++ private Throwable genTaskException; ++ private Thread genTaskFailedThread; ++ ++ private boolean failedLightUpdate; ++ ++ public void failedLightUpdate() { ++ this.failedLightUpdate = true; ++ } ++ ++ public boolean hasFailedGeneration() { ++ return this.genTaskException != null; ++ } ++ ++ // ticket level state ++ private int oldTicketLevel = ChunkLevel.MAX_LEVEL + 1; ++ private int currentTicketLevel = ChunkLevel.MAX_LEVEL + 1; ++ ++ public int getTicketLevel() { ++ return this.currentTicketLevel; ++ } ++ ++ public final ChunkHolder vanillaChunkHolder; ++ ++ public NewChunkHolder(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkTaskScheduler scheduler) { ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.scheduler = scheduler; ++ this.vanillaChunkHolder = new ChunkHolder(new ChunkPos(chunkX, chunkZ), world, world.getLightEngine(), world.chunkSource.chunkMap, this); ++ } ++ ++ protected ImposterProtoChunk wrappedChunkForNeighbour; ++ ++ // holds scheduling lock ++ public ChunkAccess getChunkForNeighbourAccess() { ++ // Vanilla overrides the status futures with an imposter chunk to prevent writes to full chunks ++ // But we don't store per-status futures, so we need this hack ++ if (this.wrappedChunkForNeighbour != null) { ++ return this.wrappedChunkForNeighbour; ++ } ++ final ChunkAccess ret = this.currentChunk; ++ return ret instanceof LevelChunk fullChunk ? this.wrappedChunkForNeighbour = new ImposterProtoChunk(fullChunk, false) : ret; ++ } ++ ++ public ChunkAccess getCurrentChunk() { ++ return this.currentChunk; ++ } ++ ++ int getCurrentTicketLevel() { ++ return this.currentTicketLevel; ++ } ++ ++ void updateTicketLevel(final int toLevel) { ++ this.currentTicketLevel = toLevel; ++ } ++ ++ private int totalNeighboursUsingThisChunk = 0; ++ ++ // holds schedule lock ++ public void addNeighbourUsingChunk() { ++ final int now = ++this.totalNeighboursUsingThisChunk; ++ ++ if (now == 1) { ++ this.checkUnload(); ++ } ++ } ++ ++ // holds schedule lock ++ public void removeNeighbourUsingChunk() { ++ final int now = --this.totalNeighboursUsingThisChunk; ++ ++ if (now == 0) { ++ this.checkUnload(); ++ } ++ ++ if (now < 0) { ++ throw new IllegalStateException("Neighbours using this chunk cannot be negative"); ++ } ++ } ++ ++ // must hold scheduling lock ++ // returns string reason for why chunk should remain loaded, null otherwise ++ public final String isSafeToUnload() { ++ // is ticket level below threshold? ++ if (this.oldTicketLevel <= ChunkHolderManager.MAX_TICKET_LEVEL) { ++ return "ticket_level"; ++ } ++ ++ // are we being used by another chunk for generation? ++ if (this.totalNeighboursUsingThisChunk != 0) { ++ return "neighbours_generating"; ++ } ++ ++ // are we going to be used by another chunk for generation? ++ if (!this.neighboursWaitingForUs.isEmpty()) { ++ return "neighbours_waiting"; ++ } ++ ++ // chunk must be marked inaccessible (i.e unloaded to plugins) ++ if (this.getChunkStatus() != FullChunkStatus.INACCESSIBLE) { ++ return "fullchunkstatus"; ++ } ++ ++ // are we currently generating anything, or have requested generation? ++ if (this.generationTask != null) { ++ return "generating"; ++ } ++ if (this.requestedGenStatus != null) { ++ return "requested_generation"; ++ } ++ ++ // entity data requested? ++ if (this.entityDataLoadTask != null) { ++ return "entity_data_requested"; ++ } ++ ++ // poi data requested? ++ if (this.poiDataLoadTask != null) { ++ return "poi_data_requested"; ++ } ++ ++ // are we pending serialization? ++ if (this.entityDataUnload != null) { ++ return "entity_serialization"; ++ } ++ if (this.poiDataUnload != null) { ++ return "poi_serialization"; ++ } ++ if (this.chunkDataUnload != null) { ++ return "chunk_serialization"; ++ } ++ ++ // Note: light tasks do not need a check, as they add a ticket. ++ ++ // nothing is using this chunk, so it should be unloaded ++ return null; ++ } ++ ++ /** Unloaded from chunk map */ ++ boolean killed; ++ ++ // must hold scheduling lock ++ private void checkUnload() { ++ if (this.killed) { ++ return; ++ } ++ if (this.isSafeToUnload() == null) { ++ // ensure in unload queue ++ this.scheduler.chunkHolderManager.unloadQueue.addChunk(this.chunkX, this.chunkZ); ++ } else { ++ // ensure not in unload queue ++ this.scheduler.chunkHolderManager.unloadQueue.removeChunk(this.chunkX, this.chunkZ); ++ } ++ } ++ ++ static final record UnloadState(NewChunkHolder holder, ChunkAccess chunk, ChunkEntitySlices entityChunk, PoiChunk poiChunk) {}; ++ ++ // note: these are completed with null to indicate that no write occurred ++ // they are also completed with null to indicate a null write occurred ++ private UnloadTask chunkDataUnload; ++ private UnloadTask entityDataUnload; ++ private UnloadTask poiDataUnload; ++ ++ public static final record UnloadTask(Completable completable, DelayedPrioritisedTask task) {} ++ ++ public UnloadTask getUnloadTask(final RegionFileIOThread.RegionFileType type) { ++ switch (type) { ++ case CHUNK_DATA: ++ return this.chunkDataUnload; ++ case ENTITY_DATA: ++ return this.entityDataUnload; ++ case POI_DATA: ++ return this.poiDataUnload; ++ default: ++ throw new IllegalStateException("Unknown regionfile type " + type); ++ } ++ } ++ ++ private UnloadState unloadState; ++ ++ // holds schedule lock ++ UnloadState unloadStage1() { ++ // because we hold the scheduling lock, we cannot actually unload anything ++ // so we need to null this chunk's state ++ ChunkAccess chunk = this.currentChunk; ++ ChunkEntitySlices entityChunk = this.entityChunk; ++ PoiChunk poiChunk = this.poiChunk; ++ // chunk state ++ this.currentChunk = null; ++ this.currentGenStatus = null; ++ this.wrappedChunkForNeighbour = null; ++ this.lastChunkCompletion = null; ++ // entity chunk state ++ this.entityChunk = null; ++ this.pendingEntityChunk = null; ++ ++ // poi chunk state ++ this.poiChunk = null; ++ ++ // priority state ++ this.priorityLocked = false; ++ ++ if (chunk != null) { ++ this.chunkDataUnload = new UnloadTask(new Completable<>(), new DelayedPrioritisedTask(PrioritisedExecutor.Priority.NORMAL)); ++ } ++ if (poiChunk != null) { ++ this.poiDataUnload = new UnloadTask(new Completable<>(), null); ++ } ++ if (entityChunk != null) { ++ this.entityDataUnload = new UnloadTask(new Completable<>(), null); ++ } ++ ++ return this.unloadState = (chunk != null || entityChunk != null || poiChunk != null) ? new UnloadState(this, chunk, entityChunk, poiChunk) : null; ++ } ++ ++ // data is null if failed or does not need to be saved ++ void completeAsyncChunkDataSave(final CompoundTag data) { ++ if (data != null) { ++ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, data, RegionFileIOThread.RegionFileType.CHUNK_DATA); ++ } ++ this.chunkDataUnload.completable().complete(data); ++ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ // can only write to these fields while holding the schedule lock ++ this.chunkDataUnload = null; ++ this.checkUnload(); ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ } ++ ++ void unloadStage2(final UnloadState state) { ++ this.unloadState = null; ++ final ChunkAccess chunk = state.chunk(); ++ final ChunkEntitySlices entityChunk = state.entityChunk(); ++ final PoiChunk poiChunk = state.poiChunk(); ++ ++ final boolean shouldLevelChunkNotSave = (chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave); ++ ++ // unload chunk data ++ if (chunk != null) { ++ if (chunk instanceof LevelChunk levelChunk) { ++ levelChunk.setLoaded(false); ++ } ++ ++ if (!shouldLevelChunkNotSave) { ++ this.saveChunk(chunk, true); ++ } else { ++ this.completeAsyncChunkDataSave(null); ++ } ++ ++ if (chunk instanceof LevelChunk levelChunk) { ++ this.world.unload(levelChunk); ++ } ++ } ++ ++ // unload entity data ++ if (entityChunk != null) { ++ this.saveEntities(entityChunk, true); ++ // yes this is a hack to pass the compound tag through... ++ final CompoundTag lastEntityUnload = this.lastEntityUnload; ++ this.lastEntityUnload = null; ++ ++ if (entityChunk.unload()) { ++ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ entityChunk.setTransient(true); ++ this.entityChunk = entityChunk; ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ } else { ++ this.world.getEntityLookup().entitySectionUnload(this.chunkX, this.chunkZ); ++ } ++ // we need to delay the callback until after determining transience, otherwise a potential loader could ++ // set entityChunk before we do ++ this.entityDataUnload.completable().complete(lastEntityUnload); ++ } ++ ++ // unload poi data ++ if (poiChunk != null) { ++ if (poiChunk.isDirty() && !shouldLevelChunkNotSave) { ++ this.savePOI(poiChunk, true); ++ } else { ++ this.poiDataUnload.completable().complete(null); ++ } ++ ++ if (poiChunk.isLoaded()) { ++ this.world.getPoiManager().onUnload(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ)); ++ } ++ } ++ } ++ ++ boolean unloadStage3() { ++ // can only write to these while holding the schedule lock, and we instantly complete them in stage2 ++ this.poiDataUnload = null; ++ this.entityDataUnload = null; ++ ++ // we need to check if anything has been loaded in the meantime (or if we have transient entities) ++ if (this.entityChunk != null || this.poiChunk != null || this.currentChunk != null) { ++ return false; ++ } ++ ++ return this.isSafeToUnload() == null; ++ } ++ ++ private void cancelGenTask() { ++ if (this.generationTask != null) { ++ this.generationTask.cancel(); ++ } else { ++ // otherwise, we are blocking on neighbours, so remove them ++ if (!this.neighboursBlockingGenTask.isEmpty()) { ++ for (final NewChunkHolder neighbour : this.neighboursBlockingGenTask) { ++ if (neighbour.neighboursWaitingForUs.remove(this) == null) { ++ throw new IllegalStateException("Corrupt state"); ++ } ++ if (neighbour.neighboursWaitingForUs.isEmpty()) { ++ neighbour.checkUnload(); ++ } ++ } ++ this.neighboursBlockingGenTask.clear(); ++ this.checkUnload(); ++ } ++ } ++ } ++ ++ // holds: ticket level update lock ++ // holds: schedule lock ++ public void processTicketLevelUpdate(final List scheduledTasks, final List changedLoadStatus) { ++ final int oldLevel = this.oldTicketLevel; ++ final int newLevel = this.currentTicketLevel; ++ ++ if (oldLevel == newLevel) { ++ return; ++ } ++ ++ this.oldTicketLevel = newLevel; ++ ++ final FullChunkStatus oldState = ChunkLevel.fullStatus(oldLevel); ++ final FullChunkStatus newState = ChunkLevel.fullStatus(newLevel); ++ final boolean oldUnloaded = oldLevel > ChunkHolderManager.MAX_TICKET_LEVEL; ++ final boolean newUnloaded = newLevel > ChunkHolderManager.MAX_TICKET_LEVEL; ++ ++ final ChunkStatus maxGenerationStatusOld = ChunkLevel.generationStatus(oldLevel); ++ final ChunkStatus maxGenerationStatusNew = ChunkLevel.generationStatus(newLevel); ++ ++ // check for cancellations from downgrading ticket level ++ if (this.requestedGenStatus != null && !newState.isOrAfter(FullChunkStatus.FULL) && newLevel > oldLevel) { ++ // note: cancel() may invoke onChunkGenComplete synchronously here ++ if (newUnloaded) { ++ // need to cancel all tasks ++ // note: requested status must be set to null here before cancellation, to indicate to the ++ // completion logic that we do not want rescheduling to occur ++ this.requestedGenStatus = null; ++ this.cancelGenTask(); ++ } else { ++ final ChunkStatus toCancel = maxGenerationStatusNew.getNextStatus(); ++ final ChunkStatus currentRequestedStatus = this.requestedGenStatus; ++ ++ if (currentRequestedStatus.isOrAfter(toCancel)) { ++ // we do have to cancel something here ++ // clamp requested status to the maximum ++ if (this.currentGenStatus != null && this.currentGenStatus.isOrAfter(maxGenerationStatusNew)) { ++ // already generated to status, so we must cancel ++ this.requestedGenStatus = null; ++ this.cancelGenTask(); ++ } else { ++ // not generated to status, so we may have to cancel ++ // note: gen task is always 1 status above current gen status if not null ++ this.requestedGenStatus = maxGenerationStatusNew; ++ if (this.generationTaskStatus != null && this.generationTaskStatus.isOrAfter(toCancel)) { ++ // TOOD is this even possible? i don't think so ++ throw new IllegalStateException("?????"); ++ } ++ } ++ } ++ } ++ } ++ ++ if (newState != oldState) { ++ if (newState.isOrAfter(oldState)) { ++ // status upgrade ++ if (!oldState.isOrAfter(FullChunkStatus.FULL) && newState.isOrAfter(FullChunkStatus.FULL)) { ++ // may need to schedule full load ++ if (this.currentGenStatus != ChunkStatus.FULL) { ++ if (this.requestedGenStatus != null) { ++ this.requestedGenStatus = ChunkStatus.FULL; ++ } else { ++ this.scheduler.schedule( ++ this.chunkX, this.chunkZ, ChunkStatus.FULL, this, scheduledTasks ++ ); ++ } ++ } else { ++ // now we are fully loaded ++ this.queueBorderFullStatus(true, changedLoadStatus); ++ } ++ } ++ } else { ++ // status downgrade ++ if (!newState.isOrAfter(FullChunkStatus.ENTITY_TICKING) && oldState.isOrAfter(FullChunkStatus.ENTITY_TICKING)) { ++ this.completeFullStatusConsumers(FullChunkStatus.ENTITY_TICKING, null); ++ } ++ ++ if (!newState.isOrAfter(FullChunkStatus.BLOCK_TICKING) && oldState.isOrAfter(FullChunkStatus.BLOCK_TICKING)) { ++ this.completeFullStatusConsumers(FullChunkStatus.BLOCK_TICKING, null); ++ } ++ ++ if (!newState.isOrAfter(FullChunkStatus.FULL) && oldState.isOrAfter(FullChunkStatus.FULL)) { ++ this.completeFullStatusConsumers(FullChunkStatus.FULL, null); ++ } ++ } ++ } ++ ++ if (oldState != newState) { ++ if (this.onTicketUpdate(oldState, newState)) { ++ changedLoadStatus.add(this); ++ } ++ } ++ ++ if (oldUnloaded != newUnloaded) { ++ this.checkUnload(); ++ } ++ } ++ ++ /* ++ For full chunks, vanilla just loads chunks around it up to FEATURES, 1 radius ++ ++ For ticking chunks, it updates the persistent entity manager (soon to be completely nuked by EntitySliceManager, which ++ will also need to be updated but with far less implications) ++ It also shoves the scheduled block ticks into the tick scheduler ++ ++ For entity ticking chunks, updates the entity manager (see above) ++ */ ++ ++ static final int NEIGHBOUR_RADIUS = 2; ++ private long fullNeighbourChunksLoadedBitset; ++ ++ private static int getFullNeighbourIndex(final int relativeX, final int relativeZ) { ++ // index = (relativeX + NEIGHBOUR_CACHE_RADIUS) + (relativeZ + NEIGHBOUR_CACHE_RADIUS) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1) ++ // optimised variant of the above by moving some of the ops to compile time ++ return relativeX + (relativeZ * (NEIGHBOUR_RADIUS * 2 + 1)) + (NEIGHBOUR_RADIUS + NEIGHBOUR_RADIUS * ((NEIGHBOUR_RADIUS * 2 + 1))); ++ } ++ public final boolean isNeighbourFullLoaded(final int relativeX, final int relativeZ) { ++ return (this.fullNeighbourChunksLoadedBitset & (1L << getFullNeighbourIndex(relativeX, relativeZ))) != 0; ++ } ++ ++ // returns true if this chunk changed full status ++ public final boolean setNeighbourFullLoaded(final int relativeX, final int relativeZ) { ++ final long before = this.fullNeighbourChunksLoadedBitset; ++ final int index = getFullNeighbourIndex(relativeX, relativeZ); ++ this.fullNeighbourChunksLoadedBitset |= (1L << index); ++ return this.onNeighbourChange(before, this.fullNeighbourChunksLoadedBitset); ++ } ++ ++ // returns true if this chunk changed full status ++ public final boolean setNeighbourFullUnloaded(final int relativeX, final int relativeZ) { ++ final long before = this.fullNeighbourChunksLoadedBitset; ++ final int index = getFullNeighbourIndex(relativeX, relativeZ); ++ this.fullNeighbourChunksLoadedBitset &= ~(1L << index); ++ return this.onNeighbourChange(before, this.fullNeighbourChunksLoadedBitset); ++ } ++ ++ public static boolean areNeighboursFullLoaded(final long bitset, final int radius) { ++ // index = relativeX + (relativeZ * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)) + (NEIGHBOUR_CACHE_RADIUS + NEIGHBOUR_CACHE_RADIUS * ((NEIGHBOUR_CACHE_RADIUS * 2 + 1))) ++ switch (radius) { ++ case 0: { ++ return (bitset & (1L << getFullNeighbourIndex(0, 0))) != 0L; ++ } ++ case 1: { ++ long mask = 0L; ++ for (int dx = -1; dx <= 1; ++dx) { ++ for (int dz = -1; dz <= 1; ++dz) { ++ mask |= (1L << getFullNeighbourIndex(dx, dz)); ++ } ++ } ++ return (bitset & mask) == mask; ++ } ++ case 2: { ++ long mask = 0L; ++ for (int dx = -2; dx <= 2; ++dx) { ++ for (int dz = -2; dz <= 2; ++dz) { ++ mask |= (1L << getFullNeighbourIndex(dx, dz)); ++ } ++ } ++ return (bitset & mask) == mask; ++ } ++ ++ default: { ++ throw new IllegalArgumentException("Radius not recognized: " + radius); ++ } ++ } ++ } ++ ++ // upper 16 bits are pending status, lower 16 bits are current status ++ private volatile long chunkStatus; ++ private static final long PENDING_STATUS_MASK = Long.MIN_VALUE >> 31; ++ private static final FullChunkStatus[] CHUNK_STATUS_BY_ID = FullChunkStatus.values(); ++ private static final VarHandle CHUNK_STATUS_HANDLE = ConcurrentUtil.getVarHandle(NewChunkHolder.class, "chunkStatus", long.class); ++ ++ public static FullChunkStatus getCurrentChunkStatus(final long encoded) { ++ return CHUNK_STATUS_BY_ID[(int)encoded]; ++ } ++ ++ public static FullChunkStatus getPendingChunkStatus(final long encoded) { ++ return CHUNK_STATUS_BY_ID[(int)(encoded >>> 32)]; ++ } ++ ++ public FullChunkStatus getChunkStatus() { ++ return getCurrentChunkStatus(((long)CHUNK_STATUS_HANDLE.getVolatile((NewChunkHolder)this))); ++ } ++ ++ public boolean isEntityTickingReady() { ++ return this.getChunkStatus().isOrAfter(FullChunkStatus.ENTITY_TICKING); ++ } ++ ++ public boolean isTickingReady() { ++ return this.getChunkStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING); ++ } ++ ++ public boolean isFullChunkReady() { ++ return this.getChunkStatus().isOrAfter(FullChunkStatus.FULL); ++ } ++ ++ private static FullChunkStatus getStatusForBitset(final long bitset) { ++ if (areNeighboursFullLoaded(bitset, 2)) { ++ return FullChunkStatus.ENTITY_TICKING; ++ } else if (areNeighboursFullLoaded(bitset, 1)) { ++ return FullChunkStatus.BLOCK_TICKING; ++ } else if (areNeighboursFullLoaded(bitset, 0)) { ++ return FullChunkStatus.FULL; ++ } else { ++ return FullChunkStatus.INACCESSIBLE; ++ } ++ } ++ ++ // note: only while updating ticket level, so holds ticket update lock + scheduling lock ++ protected final boolean onTicketUpdate(final FullChunkStatus oldState, final FullChunkStatus newState) { ++ if (oldState == newState) { ++ return false; ++ } ++ ++ // preserve border request after full status complete, as it does not set anything in the bitset ++ FullChunkStatus byNeighbours = getStatusForBitset(this.fullNeighbourChunksLoadedBitset); ++ if (byNeighbours == FullChunkStatus.INACCESSIBLE && newState.isOrAfter(FullChunkStatus.FULL) && this.currentGenStatus == ChunkStatus.FULL) { ++ byNeighbours = FullChunkStatus.FULL; ++ } ++ ++ final FullChunkStatus toSet; ++ ++ if (newState.isOrAfter(byNeighbours)) { ++ // must clamp to neighbours level, even though we have the ticket level ++ toSet = byNeighbours; ++ } else { ++ // must clamp to ticket level, even though we have the neighbours ++ toSet = newState; ++ } ++ ++ long curr = (long)CHUNK_STATUS_HANDLE.getVolatile((NewChunkHolder)this); ++ ++ if (curr == ((long)toSet.ordinal() | ((long)toSet.ordinal() << 32))) { ++ // nothing to do ++ return false; ++ } ++ ++ int failures = 0; ++ for (;;) { ++ final long update = (curr & ~PENDING_STATUS_MASK) | ((long)toSet.ordinal() << 32); ++ if (curr == (curr = (long)CHUNK_STATUS_HANDLE.compareAndExchange((NewChunkHolder)this, curr, update))) { ++ return true; ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ protected final boolean onNeighbourChange(final long bitsetBefore, final long bitsetAfter) { ++ FullChunkStatus oldState = getStatusForBitset(bitsetBefore); ++ FullChunkStatus newState = getStatusForBitset(bitsetAfter); ++ final FullChunkStatus currStateTicketLevel = ChunkLevel.fullStatus(this.oldTicketLevel); ++ if (oldState.isOrAfter(currStateTicketLevel)) { ++ oldState = currStateTicketLevel; ++ } ++ if (newState.isOrAfter(currStateTicketLevel)) { ++ newState = currStateTicketLevel; ++ } ++ // preserve border request after full status complete, as it does not set anything in the bitset ++ if (newState == FullChunkStatus.INACCESSIBLE && currStateTicketLevel.isOrAfter(FullChunkStatus.FULL) && this.currentGenStatus == ChunkStatus.FULL) { ++ newState = FullChunkStatus.FULL; ++ } ++ ++ if (oldState == newState) { ++ return false; ++ } ++ ++ int failures = 0; ++ for (long curr = (long)CHUNK_STATUS_HANDLE.getVolatile((NewChunkHolder)this);;) { ++ final long update = (curr & ~PENDING_STATUS_MASK) | ((long)newState.ordinal() << 32); ++ if (curr == (curr = (long)CHUNK_STATUS_HANDLE.compareAndExchange((NewChunkHolder)this, curr, update))) { ++ return true; ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ private boolean queueBorderFullStatus(final boolean loaded, final List changedFullStatus) { ++ final FullChunkStatus toStatus = loaded ? FullChunkStatus.FULL : FullChunkStatus.INACCESSIBLE; ++ ++ int failures = 0; ++ for (long curr = (long)CHUNK_STATUS_HANDLE.getVolatile((NewChunkHolder)this);;) { ++ final FullChunkStatus currPending = getPendingChunkStatus(curr); ++ if (loaded && currPending != FullChunkStatus.INACCESSIBLE) { ++ throw new IllegalStateException("Expected " + FullChunkStatus.INACCESSIBLE + " for pending, but got " + currPending); ++ } ++ ++ final long update = (curr & ~PENDING_STATUS_MASK) | ((long)toStatus.ordinal() << 32); ++ if (curr == (curr = (long)CHUNK_STATUS_HANDLE.compareAndExchange((NewChunkHolder)this, curr, update))) { ++ if ((int)(update) != (int)(update >>> 32)) { ++ changedFullStatus.add(this); ++ return true; ++ } ++ return false; ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ // only call on main thread, must hold ticket level and scheduling lock ++ private void onFullChunkLoadChange(final boolean loaded, final List changedFullStatus) { ++ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ, NEIGHBOUR_RADIUS); ++ try { ++ for (int dz = -NEIGHBOUR_RADIUS; dz <= NEIGHBOUR_RADIUS; ++dz) { ++ for (int dx = -NEIGHBOUR_RADIUS; dx <= NEIGHBOUR_RADIUS; ++dx) { ++ final NewChunkHolder holder = (dx | dz) == 0 ? this : this.scheduler.chunkHolderManager.getChunkHolder(dx + this.chunkX, dz + this.chunkZ); ++ if (loaded) { ++ if (holder.setNeighbourFullLoaded(-dx, -dz)) { ++ changedFullStatus.add(holder); ++ } ++ } else { ++ if (holder != null && holder.setNeighbourFullUnloaded(-dx, -dz)) { ++ changedFullStatus.add(holder); ++ } ++ } ++ } ++ } ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ } ++ ++ private FullChunkStatus updateCurrentState(final FullChunkStatus to) { ++ int failures = 0; ++ for (long curr = (long)CHUNK_STATUS_HANDLE.getVolatile((NewChunkHolder)this);;) { ++ final long update = (curr & PENDING_STATUS_MASK) | (long)to.ordinal(); ++ if (curr == (curr = (long)CHUNK_STATUS_HANDLE.compareAndExchange((NewChunkHolder)this, curr, update))) { ++ return getPendingChunkStatus(curr); ++ } ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ private void changeEntityChunkStatus(final FullChunkStatus toStatus) { ++ this.world.getEntityLookup().chunkStatusChange(this.chunkX, this.chunkZ, toStatus); ++ } ++ ++ private boolean processingFullStatus = false; ++ ++ // only to be called on the main thread, no locks need to be held ++ public boolean handleFullStatusChange(final List changedFullStatus) { ++ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot update full status thread off-main"); ++ ++ boolean ret = false; ++ ++ if (this.processingFullStatus) { ++ // we cannot process updates recursively ++ return ret; ++ } ++ ++ // note: use opaque reads for chunk status read since we need it to be atomic ++ ++ // test if anything changed ++ long statusCheck = (long)CHUNK_STATUS_HANDLE.getOpaque((NewChunkHolder)this); ++ if ((int)statusCheck == (int)(statusCheck >>> 32)) { ++ // nothing changed ++ return ret; ++ } ++ ++ final ChunkTaskScheduler scheduler = this.scheduler; ++ final ChunkHolderManager holderManager = scheduler.chunkHolderManager; ++ final int ticketKeep; ++ final Long ticketId = Long.valueOf(holderManager.getNextStatusUpgradeId()); ++ final ReentrantAreaLock.Node ticketLock = holderManager.ticketLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ ticketKeep = this.currentTicketLevel; ++ statusCheck = (long)CHUNK_STATUS_HANDLE.getOpaque((NewChunkHolder)this); ++ // handle race condition where ticket level and target status is updated concurrently ++ if ((int)statusCheck == (int)(statusCheck >>> 32)) { ++ // nothing changed ++ return ret; ++ } ++ holderManager.addTicketAtLevel(TicketType.STATUS_UPGRADE, CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ), ticketKeep, ticketId, false); ++ } finally { ++ holderManager.ticketLockArea.unlock(ticketLock); ++ } ++ ++ this.processingFullStatus = true; ++ try { ++ for (;;) { ++ final long currStateEncoded = (long)CHUNK_STATUS_HANDLE.getOpaque((NewChunkHolder)this); ++ final FullChunkStatus currState = getCurrentChunkStatus(currStateEncoded); ++ FullChunkStatus nextState = getPendingChunkStatus(currStateEncoded); ++ if (currState == nextState) { ++ if (nextState == FullChunkStatus.INACCESSIBLE) { ++ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); ++ try { ++ this.checkUnload(); ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ } ++ break; ++ } ++ ++ // chunks cannot downgrade state while status is pending a change ++ final LevelChunk chunk = (LevelChunk)this.currentChunk; ++ ++ // Note: we assume that only load/unload contain plugin logic ++ // plugin logic is anything stupid enough to possibly change the chunk status while it is already ++ // being changed (i.e during load it is possible it will try to set to full ticking) ++ // in order to allow this change, we also need this plugin logic to be contained strictly after all ++ // of the chunk system load callbacks are invoked ++ if (nextState.isOrAfter(currState)) { ++ // state upgrade ++ if (!currState.isOrAfter(FullChunkStatus.FULL) && nextState.isOrAfter(FullChunkStatus.FULL)) { ++ nextState = this.updateCurrentState(FullChunkStatus.FULL); ++ holderManager.ensureInAutosave(this); ++ chunk.pushChunkIntoLoadedMap(); ++ this.changeEntityChunkStatus(FullChunkStatus.FULL); ++ chunk.onChunkLoad(this); ++ this.onFullChunkLoadChange(true, changedFullStatus); ++ this.completeFullStatusConsumers(FullChunkStatus.FULL, chunk); ++ } ++ ++ if (!currState.isOrAfter(FullChunkStatus.BLOCK_TICKING) && nextState.isOrAfter(FullChunkStatus.BLOCK_TICKING)) { ++ nextState = this.updateCurrentState(FullChunkStatus.BLOCK_TICKING); ++ this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING); ++ chunk.onChunkTicking(this); ++ this.completeFullStatusConsumers(FullChunkStatus.BLOCK_TICKING, chunk); ++ } ++ ++ if (!currState.isOrAfter(FullChunkStatus.ENTITY_TICKING) && nextState.isOrAfter(FullChunkStatus.ENTITY_TICKING)) { ++ nextState = this.updateCurrentState(FullChunkStatus.ENTITY_TICKING); ++ this.changeEntityChunkStatus(FullChunkStatus.ENTITY_TICKING); ++ chunk.onChunkEntityTicking(this); ++ this.completeFullStatusConsumers(FullChunkStatus.ENTITY_TICKING, chunk); ++ } ++ } else { ++ if (currState.isOrAfter(FullChunkStatus.ENTITY_TICKING) && !nextState.isOrAfter(FullChunkStatus.ENTITY_TICKING)) { ++ this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING); ++ chunk.onChunkNotEntityTicking(this); ++ nextState = this.updateCurrentState(FullChunkStatus.BLOCK_TICKING); ++ } ++ ++ if (currState.isOrAfter(FullChunkStatus.BLOCK_TICKING) && !nextState.isOrAfter(FullChunkStatus.BLOCK_TICKING)) { ++ this.changeEntityChunkStatus(FullChunkStatus.FULL); ++ chunk.onChunkNotTicking(this); ++ nextState = this.updateCurrentState(FullChunkStatus.FULL); ++ } ++ ++ if (currState.isOrAfter(FullChunkStatus.FULL) && !nextState.isOrAfter(FullChunkStatus.FULL)) { ++ this.onFullChunkLoadChange(false, changedFullStatus); ++ this.changeEntityChunkStatus(FullChunkStatus.INACCESSIBLE); ++ chunk.onChunkUnload(this); ++ nextState = this.updateCurrentState(FullChunkStatus.INACCESSIBLE); ++ } ++ } ++ ++ ret = true; ++ } ++ } finally { ++ this.processingFullStatus = false; ++ holderManager.removeTicketAtLevel(TicketType.STATUS_UPGRADE, this.chunkX, this.chunkZ, ticketKeep, ticketId); ++ } ++ ++ return ret; ++ } ++ ++ // note: must hold scheduling lock ++ // rets true if the current requested gen status is not null (effectively, whether further scheduling is not needed) ++ boolean upgradeGenTarget(final ChunkStatus toStatus) { ++ if (toStatus == null) { ++ throw new NullPointerException("toStatus cannot be null"); ++ } ++ if (this.requestedGenStatus == null && this.generationTask == null) { ++ return false; ++ } ++ if (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(toStatus)) { ++ this.requestedGenStatus = toStatus; ++ } ++ return true; ++ } ++ ++ public void setGenerationTarget(final ChunkStatus toStatus) { ++ this.requestedGenStatus = toStatus; ++ } ++ ++ public boolean hasGenerationTask() { ++ return this.generationTask != null; ++ } ++ ++ public ChunkStatus getCurrentGenStatus() { ++ return this.currentGenStatus; ++ } ++ ++ public ChunkStatus getRequestedGenStatus() { ++ return this.requestedGenStatus; ++ } ++ ++ private final Reference2ObjectOpenHashMap>> statusWaiters = new Reference2ObjectOpenHashMap<>(); ++ ++ void addStatusConsumer(final ChunkStatus status, final Consumer consumer) { ++ this.statusWaiters.computeIfAbsent(status, (final ChunkStatus keyInMap) -> { ++ return new ArrayList<>(4); ++ }).add(consumer); ++ } ++ ++ private void completeStatusConsumers(ChunkStatus status, final ChunkAccess chunk) { ++ // need to tell future statuses to complete if cancelled ++ do { ++ this.completeStatusConsumers0(status, chunk); ++ } while (chunk == null && status != (status = status.getNextStatus())); ++ } ++ ++ private void completeStatusConsumers0(final ChunkStatus status, final ChunkAccess chunk) { ++ final List> consumers; ++ consumers = this.statusWaiters.remove(status); ++ ++ if (consumers == null) { ++ return; ++ } ++ ++ // must be scheduled to main, we do not trust the callback to not do anything stupid ++ this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { ++ for (final Consumer consumer : consumers) { ++ try { ++ consumer.accept(chunk); ++ } catch (final ThreadDeath thr) { ++ throw thr; ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to process chunk status callback", thr); ++ } ++ } ++ }, PrioritisedExecutor.Priority.HIGHEST); ++ } ++ ++ private final Reference2ObjectOpenHashMap>> fullStatusWaiters = new Reference2ObjectOpenHashMap<>(); ++ ++ void addFullStatusConsumer(final FullChunkStatus status, final Consumer consumer) { ++ this.fullStatusWaiters.computeIfAbsent(status, (final FullChunkStatus keyInMap) -> { ++ return new ArrayList<>(4); ++ }).add(consumer); ++ } ++ ++ private void completeFullStatusConsumers(FullChunkStatus status, final LevelChunk chunk) { ++ // need to tell future statuses to complete if cancelled ++ final FullChunkStatus max = CHUNK_STATUS_BY_ID[CHUNK_STATUS_BY_ID.length - 1]; ++ ++ for (;;) { ++ this.completeFullStatusConsumers0(status, chunk); ++ if (chunk != null || status == max) { ++ break; ++ } ++ status = CHUNK_STATUS_BY_ID[status.ordinal() + 1]; ++ } ++ } ++ ++ private void completeFullStatusConsumers0(final FullChunkStatus status, final LevelChunk chunk) { ++ final List> consumers; ++ consumers = this.fullStatusWaiters.remove(status); ++ ++ if (consumers == null) { ++ return; ++ } ++ ++ // must be scheduled to main, we do not trust the callback to not do anything stupid ++ this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { ++ for (final Consumer consumer : consumers) { ++ try { ++ consumer.accept(chunk); ++ } catch (final ThreadDeath thr) { ++ throw thr; ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to process chunk status callback", thr); ++ } ++ } ++ }, PrioritisedExecutor.Priority.HIGHEST); ++ } ++ ++ // note: must hold scheduling lock ++ private void onChunkGenComplete(final ChunkAccess newChunk, final ChunkStatus newStatus, ++ final List scheduleList, final List changedLoadStatus) { ++ if (!this.neighboursBlockingGenTask.isEmpty()) { ++ throw new IllegalStateException("Cannot have neighbours blocking this gen task"); ++ } ++ if (newChunk != null || (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(newStatus))) { ++ this.completeStatusConsumers(newStatus, newChunk); ++ } ++ // done now, clear state (must be done before scheduling new tasks) ++ this.generationTask = null; ++ this.generationTaskStatus = null; ++ if (newChunk == null) { ++ // task was cancelled ++ // should be careful as this could be called while holding the schedule lock and/or inside the ++ // ticket level update ++ // while a task may be cancelled, it is possible for it to be later re-scheduled ++ // however, because generationTask is only set to null on _completion_, the scheduler leaves ++ // the rescheduling logic to us here ++ final ChunkStatus requestedGenStatus = this.requestedGenStatus; ++ this.requestedGenStatus = null; ++ if (requestedGenStatus != null) { ++ // it looks like it has been requested, so we must reschedule ++ if (!this.neighboursWaitingForUs.isEmpty()) { ++ for (final Iterator> iterator = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final Reference2ObjectMap.Entry entry = iterator.next(); ++ ++ final NewChunkHolder chunkHolder = entry.getKey(); ++ final ChunkStatus toStatus = entry.getValue(); ++ ++ if (!requestedGenStatus.isOrAfter(toStatus)) { ++ // if we were cancelled, we are responsible for removing the waiter ++ if (!chunkHolder.neighboursBlockingGenTask.remove(this)) { ++ throw new IllegalStateException("Corrupt state"); ++ } ++ if (chunkHolder.neighboursBlockingGenTask.isEmpty()) { ++ chunkHolder.checkUnload(); ++ } ++ iterator.remove(); ++ continue; ++ } ++ } ++ } ++ ++ // note: only after generationTask -> null, generationTaskStatus -> null, and requestedGenStatus -> null ++ this.scheduler.schedule( ++ this.chunkX, this.chunkZ, requestedGenStatus, this, scheduleList ++ ); ++ ++ // return, can't do anything further ++ return; ++ } ++ ++ if (!this.neighboursWaitingForUs.isEmpty()) { ++ for (final NewChunkHolder chunkHolder : this.neighboursWaitingForUs.keySet()) { ++ if (!chunkHolder.neighboursBlockingGenTask.remove(this)) { ++ throw new IllegalStateException("Corrupt state"); ++ } ++ if (chunkHolder.neighboursBlockingGenTask.isEmpty()) { ++ chunkHolder.checkUnload(); ++ } ++ } ++ this.neighboursWaitingForUs.clear(); ++ } ++ // reset priority, we have nothing left to generate to ++ this.setPriority(PrioritisedExecutor.Priority.NORMAL); ++ this.checkUnload(); ++ return; ++ } ++ ++ this.currentChunk = newChunk; ++ this.currentGenStatus = newStatus; ++ this.lastChunkCompletion = new ChunkCompletion(newChunk, newStatus); ++ ++ final ChunkStatus requestedGenStatus = this.requestedGenStatus; ++ ++ List needsScheduling = null; ++ boolean recalculatePriority = false; ++ for (final Iterator> iterator ++ = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final Reference2ObjectMap.Entry entry = iterator.next(); ++ final NewChunkHolder neighbour = entry.getKey(); ++ final ChunkStatus requiredStatus = entry.getValue(); ++ ++ if (!newStatus.isOrAfter(requiredStatus)) { ++ if (requestedGenStatus == null || !requestedGenStatus.isOrAfter(requiredStatus)) { ++ // if we're cancelled, still need to clear this map ++ if (!neighbour.neighboursBlockingGenTask.remove(this)) { ++ throw new IllegalStateException("Neighbour is not waiting for us?"); ++ } ++ if (neighbour.neighboursBlockingGenTask.isEmpty()) { ++ neighbour.checkUnload(); ++ } ++ ++ iterator.remove(); ++ } ++ continue; ++ } ++ ++ // doesn't matter what isCancelled is here, we need to schedule if we can ++ ++ recalculatePriority = true; ++ if (!neighbour.neighboursBlockingGenTask.remove(this)) { ++ throw new IllegalStateException("Neighbour is not waiting for us?"); ++ } ++ ++ if (neighbour.neighboursBlockingGenTask.isEmpty()) { ++ if (neighbour.requestedGenStatus != null) { ++ if (needsScheduling == null) { ++ needsScheduling = new ArrayList<>(); ++ } ++ needsScheduling.add(neighbour); ++ } else { ++ neighbour.checkUnload(); ++ } ++ } ++ ++ // remove last; access to entry will throw if removed ++ iterator.remove(); ++ } ++ ++ if (newStatus == ChunkStatus.FULL) { ++ this.lockPriority(); ++ // must use oldTicketLevel, we hold the schedule lock but not the ticket level lock ++ // however, schedule lock needs to be held for ticket level callback, so we're fine here ++ if (ChunkLevel.fullStatus(this.oldTicketLevel).isOrAfter(FullChunkStatus.FULL)) { ++ this.queueBorderFullStatus(true, changedLoadStatus); ++ } ++ } ++ ++ if (recalculatePriority) { ++ this.recalculateNeighbourRequestedPriority(); ++ } ++ ++ if (requestedGenStatus != null && !newStatus.isOrAfter(requestedGenStatus)) { ++ this.scheduleNeighbours(needsScheduling, scheduleList); ++ ++ // we need to schedule more tasks now ++ this.scheduler.schedule( ++ this.chunkX, this.chunkZ, requestedGenStatus, this, scheduleList ++ ); ++ } else { ++ // we're done now ++ if (requestedGenStatus != null) { ++ this.requestedGenStatus = null; ++ } ++ // reached final stage, so stop scheduling now ++ this.setPriority(PrioritisedExecutor.Priority.NORMAL); ++ this.checkUnload(); ++ ++ this.scheduleNeighbours(needsScheduling, scheduleList); ++ } ++ } ++ ++ private void scheduleNeighbours(final List needsScheduling, final List scheduleList) { ++ if (needsScheduling != null) { ++ for (int i = 0, len = needsScheduling.size(); i < len; ++i) { ++ final NewChunkHolder neighbour = needsScheduling.get(i); ++ ++ this.scheduler.schedule( ++ neighbour.chunkX, neighbour.chunkZ, neighbour.requestedGenStatus, neighbour, scheduleList ++ ); ++ } ++ } ++ } ++ ++ public void setGenerationTask(final ChunkProgressionTask generationTask, final ChunkStatus taskStatus, ++ final List neighbours) { ++ if (this.generationTask != null || (this.currentGenStatus != null && this.currentGenStatus.isOrAfter(taskStatus))) { ++ throw new IllegalStateException("Currently generating or provided task is trying to generate to a level we are already at!"); ++ } ++ if (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(taskStatus)) { ++ throw new IllegalStateException("Cannot schedule generation task when not requested"); ++ } ++ this.generationTask = generationTask; ++ this.generationTaskStatus = taskStatus; ++ ++ for (int i = 0, len = neighbours.size(); i < len; ++i) { ++ neighbours.get(i).addNeighbourUsingChunk(); ++ } ++ ++ this.checkUnload(); ++ ++ generationTask.onComplete((final ChunkAccess access, final Throwable thr) -> { ++ if (generationTask != this.generationTask) { ++ throw new IllegalStateException( ++ "Cannot complete generation task '" + generationTask + "' because we are waiting on '" + this.generationTask + "' instead!" ++ ); ++ } ++ if (thr != null) { ++ if (this.genTaskException != null) { ++ // first one is probably the TRUE problem ++ return; ++ } ++ // don't set generation task to null, so that scheduling will not attempt to create another task and it ++ // will automatically block any further scheduling usage of this chunk as it will wait forever for a failed ++ // task to complete ++ this.genTaskException = thr; ++ this.failedGenStatus = taskStatus; ++ this.genTaskFailedThread = Thread.currentThread(); ++ ++ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( ++ "Generation task", ChunkTaskScheduler.stringIfNull(generationTask), ++ "Task to status", ChunkTaskScheduler.stringIfNull(taskStatus) ++ ), thr); ++ return; ++ } ++ ++ final boolean scheduleTasks; ++ List tasks = ChunkHolderManager.getCurrentTicketUpdateScheduling(); ++ if (tasks == null) { ++ scheduleTasks = true; ++ tasks = new ArrayList<>(); ++ } else { ++ scheduleTasks = false; ++ // we are currently updating ticket levels, so we already hold the schedule lock ++ // this means we have to leave the ticket level update to handle the scheduling ++ } ++ final List changedLoadStatus = new ArrayList<>(); ++ // theoretically, we could schedule a chunk at the max radius which performs another max radius access. So we need to double the radius. ++ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ, 2 * ChunkTaskScheduler.getMaxAccessRadius()); ++ try { ++ for (int i = 0, len = neighbours.size(); i < len; ++i) { ++ neighbours.get(i).removeNeighbourUsingChunk(); ++ } ++ this.onChunkGenComplete(access, taskStatus, tasks, changedLoadStatus); ++ } finally { ++ this.scheduler.schedulingLockArea.unlock(schedulingLock); ++ } ++ this.scheduler.chunkHolderManager.addChangedStatuses(changedLoadStatus); ++ ++ if (scheduleTasks) { ++ // can't hold the lock while scheduling, so we have to build the tasks and then schedule after ++ for (int i = 0, len = tasks.size(); i < len; ++i) { ++ tasks.get(i).schedule(); ++ } ++ } ++ }); ++ } ++ ++ public PoiChunk getPoiChunk() { ++ return this.poiChunk; ++ } ++ ++ public ChunkEntitySlices getEntityChunk() { ++ return this.entityChunk; ++ } ++ ++ public long lastAutoSave; ++ ++ public static final record SaveStat(boolean savedChunk, boolean savedEntityChunk, boolean savedPoiChunk) {} ++ ++ public SaveStat save(final boolean shutdown, final boolean unloading) { ++ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot save data off-main"); ++ ++ ChunkAccess chunk = this.getCurrentChunk(); ++ PoiChunk poi = this.getPoiChunk(); ++ ChunkEntitySlices entities = this.getEntityChunk(); ++ boolean executedUnloadTask = false; ++ ++ if (shutdown) { ++ // make sure that the async unloads complete ++ if (this.unloadState != null) { ++ // must have errored during unload ++ chunk = this.unloadState.chunk(); ++ poi = this.unloadState.poiChunk(); ++ entities = this.unloadState.entityChunk(); ++ } ++ final UnloadTask chunkUnloadTask = this.chunkDataUnload; ++ final DelayedPrioritisedTask chunkDataUnloadTask = chunkUnloadTask == null ? null : chunkUnloadTask.task(); ++ if (chunkDataUnloadTask != null) { ++ final PrioritisedExecutor.PrioritisedTask unloadTask = chunkDataUnloadTask.getTask(); ++ if (unloadTask != null) { ++ executedUnloadTask = unloadTask.execute(); ++ } ++ } ++ } ++ ++ boolean canSaveChunk = !(chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave) && ++ (chunk != null && ((shutdown || chunk instanceof LevelChunk) && chunk.isUnsaved())); ++ boolean canSavePOI = !(chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave) && (poi != null && poi.isDirty()); ++ boolean canSaveEntities = entities != null; ++ ++ try (co.aikar.timings.Timing ignored = this.world.timings.chunkSave.startTiming()) { // Paper ++ if (canSaveChunk) { ++ canSaveChunk = this.saveChunk(chunk, unloading); ++ } ++ if (canSavePOI) { ++ canSavePOI = this.savePOI(poi, unloading); ++ } ++ if (canSaveEntities) { ++ // on shutdown, we need to force transient entity chunks to save ++ canSaveEntities = this.saveEntities(entities, unloading || shutdown); ++ if (unloading || shutdown) { ++ this.lastEntityUnload = null; ++ } ++ } ++ } ++ ++ return executedUnloadTask | canSaveChunk | canSaveEntities | canSavePOI ? new SaveStat(executedUnloadTask || canSaveChunk, canSaveEntities, canSavePOI): null; ++ } ++ ++ static final class AsyncChunkSerializeTask implements Runnable { ++ ++ private final ServerLevel world; ++ private final ChunkAccess chunk; ++ private final ChunkSerializer.AsyncSaveData asyncSaveData; ++ private final NewChunkHolder toComplete; ++ ++ public AsyncChunkSerializeTask(final ServerLevel world, final ChunkAccess chunk, final ChunkSerializer.AsyncSaveData asyncSaveData, ++ final NewChunkHolder toComplete) { ++ this.world = world; ++ this.chunk = chunk; ++ this.asyncSaveData = asyncSaveData; ++ this.toComplete = toComplete; ++ } ++ ++ @Override ++ public void run() { ++ final CompoundTag toSerialize; ++ try { ++ toSerialize = ChunkSerializer.saveChunk(this.world, this.chunk, this.asyncSaveData); ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable throwable) { ++ LOGGER.error("Failed to asynchronously save chunk " + this.chunk.getPos() + " for world '" + this.world.getWorld().getName() + "', falling back to synchronous save", throwable); ++ this.world.chunkTaskScheduler.scheduleChunkTask(this.chunk.locX, this.chunk.locZ, () -> { ++ final CompoundTag synchronousSave; ++ try { ++ synchronousSave = ChunkSerializer.saveChunk(AsyncChunkSerializeTask.this.world, AsyncChunkSerializeTask.this.chunk, AsyncChunkSerializeTask.this.asyncSaveData); ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable throwable2) { ++ LOGGER.error("Failed to synchronously save chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + AsyncChunkSerializeTask.this.world.getWorld().getName() + "', chunk data will be lost", throwable2); ++ AsyncChunkSerializeTask.this.toComplete.completeAsyncChunkDataSave(null); ++ return; ++ } ++ ++ AsyncChunkSerializeTask.this.toComplete.completeAsyncChunkDataSave(synchronousSave); ++ LOGGER.info("Successfully serialized chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + AsyncChunkSerializeTask.this.world.getWorld().getName() + "' synchronously"); ++ ++ }, PrioritisedExecutor.Priority.HIGHEST); ++ return; ++ } ++ this.toComplete.completeAsyncChunkDataSave(toSerialize); ++ } ++ ++ @Override ++ public String toString() { ++ return "AsyncChunkSerializeTask{" + ++ "chunk={pos=" + this.chunk.getPos() + ",world=\"" + this.world.getWorld().getName() + "\"}" + ++ "}"; ++ } ++ } ++ ++ private boolean saveChunk(final ChunkAccess chunk, final boolean unloading) { ++ if (!chunk.isUnsaved()) { ++ if (unloading) { ++ this.completeAsyncChunkDataSave(null); ++ } ++ return false; ++ } ++ boolean completing = false; ++ try { ++ if (unloading) { ++ try { ++ final ChunkSerializer.AsyncSaveData asyncSaveData = ChunkSerializer.getAsyncSaveData(this.world, chunk); ++ ++ final PrioritisedExecutor.PrioritisedTask task = this.scheduler.loadExecutor.createTask(new AsyncChunkSerializeTask(this.world, chunk, asyncSaveData, this)); ++ ++ this.chunkDataUnload.task().setTask(task); ++ ++ task.queue(); ++ ++ chunk.setUnsaved(false); ++ ++ return true; ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to prepare async chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "', falling back to synchronous save", thr); ++ // fall through to synchronous save ++ } ++ } ++ ++ final CompoundTag save = ChunkSerializer.saveChunk(this.world, chunk, null); ++ ++ if (unloading) { ++ completing = true; ++ this.completeAsyncChunkDataSave(save); ++ LOGGER.info("Successfully serialized chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "' synchronously"); ++ } else { ++ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.CHUNK_DATA); ++ } ++ chunk.setUnsaved(false); ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'"); ++ if (unloading && !completing) { ++ this.completeAsyncChunkDataSave(null); ++ } ++ } ++ ++ return true; ++ } ++ ++ private boolean lastEntitySaveNull; ++ private CompoundTag lastEntityUnload; ++ private boolean saveEntities(final ChunkEntitySlices entities, final boolean unloading) { ++ try { ++ CompoundTag mergeFrom = null; ++ if (entities.isTransient()) { ++ if (!unloading) { ++ // if we're a transient chunk, we cannot save until unloading because otherwise a double save will ++ // result in double adding the entities ++ return false; ++ } ++ try { ++ mergeFrom = RegionFileIOThread.loadData(this.world, this.chunkX, this.chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, PrioritisedExecutor.Priority.BLOCKING); ++ } catch (final Exception ex) { ++ LOGGER.error("Cannot merge transient entities for chunk (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "', data on disk will be replaced", ex); ++ } ++ } ++ ++ final CompoundTag save = entities.save(); ++ if (mergeFrom != null) { ++ if (save == null) { ++ // don't override the data on disk with nothing ++ return false; ++ } else { ++ EntityStorage.copyEntities(mergeFrom, save); ++ } ++ } ++ if (save == null && this.lastEntitySaveNull) { ++ return false; ++ } ++ ++ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.ENTITY_DATA); ++ this.lastEntitySaveNull = save == null; ++ if (unloading) { ++ this.lastEntityUnload = save; ++ } ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to save entity data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'"); ++ } ++ ++ return true; ++ } ++ ++ private boolean lastPoiSaveNull; ++ private boolean savePOI(final PoiChunk poi, final boolean unloading) { ++ try { ++ final CompoundTag save = poi.save(); ++ poi.setDirty(false); ++ if (save == null && this.lastPoiSaveNull) { ++ if (unloading) { ++ this.poiDataUnload.completable().complete(null); ++ } ++ return false; ++ } ++ ++ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.POI_DATA); ++ this.lastPoiSaveNull = save == null; ++ if (unloading) { ++ this.poiDataUnload.completable().complete(save); ++ } ++ } catch (final ThreadDeath death) { ++ throw death; ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to save poi data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'"); ++ } ++ ++ return true; ++ } ++ ++ @Override ++ public String toString() { ++ final ChunkCompletion lastCompletion = this.lastChunkCompletion; ++ final ChunkEntitySlices entityChunk = this.entityChunk; ++ final long chunkStatus = this.chunkStatus; ++ final int fullChunkStatus = (int)chunkStatus; ++ final int pendingChunkStatus = (int)(chunkStatus >>> 32); ++ final FullChunkStatus currentFullStatus = fullChunkStatus < 0 || fullChunkStatus >= CHUNK_STATUS_BY_ID.length ? null : CHUNK_STATUS_BY_ID[fullChunkStatus]; ++ final FullChunkStatus pendingFullStatus = pendingChunkStatus < 0 || pendingChunkStatus >= CHUNK_STATUS_BY_ID.length ? null : CHUNK_STATUS_BY_ID[pendingChunkStatus]; ++ return "NewChunkHolder{" + ++ "world=" + this.world.getWorld().getName() + ++ ", chunkX=" + this.chunkX + ++ ", chunkZ=" + this.chunkZ + ++ ", entityChunkFromDisk=" + (entityChunk != null && !entityChunk.isTransient()) + ++ ", lastChunkCompletion={chunk_class=" + (lastCompletion == null || lastCompletion.chunk() == null ? "null" : lastCompletion.chunk().getClass().getName()) + ",status=" + (lastCompletion == null ? "null" : lastCompletion.genStatus()) + "}" + ++ ", currentGenStatus=" + this.currentGenStatus + ++ ", requestedGenStatus=" + this.requestedGenStatus + ++ ", generationTask=" + this.generationTask + ++ ", generationTaskStatus=" + this.generationTaskStatus + ++ ", priority=" + this.priority + ++ ", priorityLocked=" + this.priorityLocked + ++ ", neighbourRequestedPriority=" + this.neighbourRequestedPriority + ++ ", effective_priority=" + this.getEffectivePriority() + ++ ", oldTicketLevel=" + this.oldTicketLevel + ++ ", currentTicketLevel=" + this.currentTicketLevel + ++ ", totalNeighboursUsingThisChunk=" + this.totalNeighboursUsingThisChunk + ++ ", fullNeighbourChunksLoadedBitset=" + this.fullNeighbourChunksLoadedBitset + ++ ", chunkStatusRaw=" + chunkStatus + ++ ", currentChunkStatus=" + currentFullStatus + ++ ", pendingChunkStatus=" + pendingFullStatus + ++ ", is_unload_safe=" + this.isSafeToUnload() + ++ ", killed=" + this.killed + ++ '}'; ++ } ++ ++ private static JsonElement serializeCompletable(final Completable completable) { ++ if (completable == null) { ++ return new JsonPrimitive("null"); ++ } ++ ++ final JsonObject ret = new JsonObject(); ++ final boolean isCompleted = completable.isCompleted(); ++ ret.addProperty("completed", Boolean.valueOf(isCompleted)); ++ ++ if (isCompleted) { ++ ret.addProperty("completed_exceptionally", Boolean.valueOf(completable.getThrowable() != null)); ++ } ++ ++ return ret; ++ } ++ ++ // holds ticket and scheduling lock ++ public JsonObject getDebugJson() { ++ final JsonObject ret = new JsonObject(); ++ ++ final ChunkCompletion lastCompletion = this.lastChunkCompletion; ++ final ChunkEntitySlices slices = this.entityChunk; ++ final PoiChunk poiChunk = this.poiChunk; ++ ++ ret.addProperty("chunkX", Integer.valueOf(this.chunkX)); ++ ret.addProperty("chunkZ", Integer.valueOf(this.chunkZ)); ++ ret.addProperty("entity_chunk", slices == null ? "null" : "transient=" + slices.isTransient()); ++ ret.addProperty("poi_chunk", "null=" + (poiChunk == null)); ++ ret.addProperty("completed_chunk_class", lastCompletion == null ? "null" : lastCompletion.chunk().getClass().getName()); ++ ret.addProperty("completed_gen_status", lastCompletion == null ? "null" : lastCompletion.genStatus().toString()); ++ ret.addProperty("priority", Objects.toString(this.priority)); ++ ret.addProperty("neighbour_requested_priority", Objects.toString(this.neighbourRequestedPriority)); ++ ret.addProperty("generation_task", Objects.toString(this.generationTask)); ++ ret.addProperty("is_safe_unload", Objects.toString(this.isSafeToUnload())); ++ ret.addProperty("old_ticket_level", Integer.valueOf(this.oldTicketLevel)); ++ ret.addProperty("current_ticket_level", Integer.valueOf(this.currentTicketLevel)); ++ ret.addProperty("neighbours_using_chunk", Integer.valueOf(this.totalNeighboursUsingThisChunk)); ++ ++ final JsonObject neighbourWaitState = new JsonObject(); ++ ret.add("neighbour_state", neighbourWaitState); ++ ++ final JsonArray blockingGenNeighbours = new JsonArray(); ++ neighbourWaitState.add("blocking_gen_task", blockingGenNeighbours); ++ for (final NewChunkHolder blockingGenNeighbour : this.neighboursBlockingGenTask) { ++ final JsonObject neighbour = new JsonObject(); ++ blockingGenNeighbours.add(neighbour); ++ ++ neighbour.addProperty("chunkX", Integer.valueOf(blockingGenNeighbour.chunkX)); ++ neighbour.addProperty("chunkZ", Integer.valueOf(blockingGenNeighbour.chunkZ)); ++ } ++ ++ final JsonArray neighboursWaitingForUs = new JsonArray(); ++ neighbourWaitState.add("neighbours_waiting_on_us", neighboursWaitingForUs); ++ for (final Reference2ObjectMap.Entry entry : this.neighboursWaitingForUs.reference2ObjectEntrySet()) { ++ final NewChunkHolder holder = entry.getKey(); ++ final ChunkStatus status = entry.getValue(); ++ ++ final JsonObject neighbour = new JsonObject(); ++ neighboursWaitingForUs.add(neighbour); ++ ++ ++ neighbour.addProperty("chunkX", Integer.valueOf(holder.chunkX)); ++ neighbour.addProperty("chunkZ", Integer.valueOf(holder.chunkZ)); ++ neighbour.addProperty("waiting_for", Objects.toString(status)); ++ } ++ ++ ret.addProperty("fullchunkstatus", Objects.toString(this.getChunkStatus())); ++ ret.addProperty("fullchunkstatus_raw", Long.valueOf(this.chunkStatus)); ++ ret.addProperty("generation_task", Objects.toString(this.generationTask)); ++ ret.addProperty("requested_generation", Objects.toString(this.requestedGenStatus)); ++ ret.addProperty("has_entity_load_task", Boolean.valueOf(this.entityDataLoadTask != null)); ++ ret.addProperty("has_poi_load_task", Boolean.valueOf(this.poiDataLoadTask != null)); ++ ++ final UnloadTask entityDataUnload = this.entityDataUnload; ++ final UnloadTask poiDataUnload = this.poiDataUnload; ++ final UnloadTask chunkDataUnload = this.chunkDataUnload; ++ ++ ret.add("entity_unload_completable", serializeCompletable(entityDataUnload == null ? null : entityDataUnload.completable())); ++ ret.add("poi_unload_completable", serializeCompletable(poiDataUnload == null ? null : poiDataUnload.completable())); ++ ret.add("chunk_unload_completable", serializeCompletable(chunkDataUnload == null ? null : chunkDataUnload.completable())); ++ ++ final DelayedPrioritisedTask unloadTask = chunkDataUnload == null ? null : chunkDataUnload.task(); ++ if (unloadTask == null) { ++ ret.addProperty("unload_task_priority", "null"); ++ ret.addProperty("unload_task_priority_raw", "null"); ++ } else { ++ ret.addProperty("unload_task_priority", Objects.toString(unloadTask.getPriority())); ++ ret.addProperty("unload_task_priority_raw", Integer.valueOf(unloadTask.getPriorityInternal())); ++ } ++ ++ ret.addProperty("killed", Boolean.valueOf(this.killed)); ++ ++ return ret; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/PriorityHolder.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/PriorityHolder.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b4c56bf12dc8dd17452210ece4fd67411cc6b2fd +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/PriorityHolder.java +@@ -0,0 +1,215 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import java.lang.invoke.VarHandle; ++ ++public abstract class PriorityHolder { ++ ++ protected volatile int priority; ++ protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(PriorityHolder.class, "priority", int.class); ++ ++ protected static final int PRIORITY_SCHEDULED = Integer.MIN_VALUE >>> 0; ++ protected static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 1; ++ ++ protected final int getPriorityVolatile() { ++ return (int)PRIORITY_HANDLE.getVolatile((PriorityHolder)this); ++ } ++ ++ protected final int compareAndExchangePriorityVolatile(final int expect, final int update) { ++ return (int)PRIORITY_HANDLE.compareAndExchange((PriorityHolder)this, (int)expect, (int)update); ++ } ++ ++ protected final int getAndOrPriorityVolatile(final int val) { ++ return (int)PRIORITY_HANDLE.getAndBitwiseOr((PriorityHolder)this, (int)val); ++ } ++ ++ protected final void setPriorityPlain(final int val) { ++ PRIORITY_HANDLE.set((PriorityHolder)this, (int)val); ++ } ++ ++ protected PriorityHolder(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ this.setPriorityPlain(priority.priority); ++ } ++ ++ // used only for debug json ++ public boolean isScheduled() { ++ return (this.getPriorityVolatile() & PRIORITY_SCHEDULED) != 0; ++ } ++ ++ // returns false if cancelled ++ protected boolean markExecuting() { ++ return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0; ++ } ++ ++ protected boolean isMarkedExecuted() { ++ return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0; ++ } ++ ++ public void cancel() { ++ if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) { ++ // cancelled already ++ return; ++ } ++ this.cancelScheduled(); ++ } ++ ++ public void schedule() { ++ int priority = this.getPriorityVolatile(); ++ ++ if ((priority & PRIORITY_SCHEDULED) != 0) { ++ throw new IllegalStateException("schedule() called twice"); ++ } ++ ++ if ((priority & PRIORITY_EXECUTED) != 0) { ++ // cancelled ++ return; ++ } ++ ++ this.scheduleTask(PrioritisedExecutor.Priority.getPriority(priority)); ++ ++ int failures = 0; ++ for (;;) { ++ if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | PRIORITY_SCHEDULED))) { ++ return; ++ } ++ ++ if ((priority & PRIORITY_SCHEDULED) != 0) { ++ throw new IllegalStateException("schedule() called twice"); ++ } ++ ++ if ((priority & PRIORITY_EXECUTED) != 0) { ++ // cancelled or executed ++ return; ++ } ++ ++ this.setPriorityScheduled(PrioritisedExecutor.Priority.getPriority(priority)); ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public final PrioritisedExecutor.Priority getPriority() { ++ final int ret = this.getPriorityVolatile(); ++ if ((ret & PRIORITY_EXECUTED) != 0) { ++ return PrioritisedExecutor.Priority.COMPLETING; ++ } ++ if ((ret & PRIORITY_SCHEDULED) != 0) { ++ return this.getScheduledPriority(); ++ } ++ return PrioritisedExecutor.Priority.getPriority(ret); ++ } ++ ++ public final void lowerPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_EXECUTED) != 0) { ++ return; ++ } ++ ++ if ((curr & PRIORITY_SCHEDULED) != 0) { ++ this.lowerPriorityScheduled(priority); ++ return; ++ } ++ ++ if (!priority.isLowerPriority(curr)) { ++ return; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public final void setPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_EXECUTED) != 0) { ++ return; ++ } ++ ++ if ((curr & PRIORITY_SCHEDULED) != 0) { ++ this.setPriorityScheduled(priority); ++ return; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ public final void raisePriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ int failures = 0; ++ for (int curr = this.getPriorityVolatile();;) { ++ if ((curr & PRIORITY_EXECUTED) != 0) { ++ return; ++ } ++ ++ if ((curr & PRIORITY_SCHEDULED) != 0) { ++ this.raisePriorityScheduled(priority); ++ return; ++ } ++ ++ if (!priority.isHigherPriority(curr)) { ++ return; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { ++ return; ++ } ++ ++ // failed, retry ++ ++ ++failures; ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ } ++ } ++ ++ protected abstract void cancelScheduled(); ++ ++ protected abstract PrioritisedExecutor.Priority getScheduledPriority(); ++ ++ protected abstract void scheduleTask(final PrioritisedExecutor.Priority priority); ++ ++ protected abstract void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority); ++ ++ protected abstract void setPriorityScheduled(final PrioritisedExecutor.Priority priority); ++ ++ protected abstract void raisePriorityScheduled(final PrioritisedExecutor.Priority priority); ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ThreadedTicketLevelPropagator.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ThreadedTicketLevelPropagator.java +new file mode 100644 +index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276f626405d +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ThreadedTicketLevelPropagator.java +@@ -0,0 +1,1477 @@ ++package io.papermc.paper.chunk.system.scheduling; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import it.unimi.dsi.fastutil.HashCommon; ++import it.unimi.dsi.fastutil.longs.Long2ByteLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.shorts.Short2ByteLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.shorts.Short2ByteMap; ++import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; ++import java.lang.invoke.VarHandle; ++import java.util.ArrayDeque; ++import java.util.Arrays; ++import java.util.Iterator; ++import java.util.List; ++import java.util.concurrent.ConcurrentHashMap; ++import java.util.concurrent.locks.LockSupport; ++ ++public abstract class ThreadedTicketLevelPropagator { ++ ++ // sections are 64 in length ++ public static final int SECTION_SHIFT = 6; ++ public static final int SECTION_SIZE = 1 << SECTION_SHIFT; ++ private static final int LEVEL_BITS = SECTION_SHIFT; ++ private static final int LEVEL_COUNT = 1 << LEVEL_BITS; ++ private static final int MIN_SOURCE_LEVEL = 1; ++ // we limit the max source to 62 because the depropagation code _must_ attempt to depropagate ++ // a 1 level to 0; and if a source was 63 then it may cross more than 2 sections in depropagation ++ private static final int MAX_SOURCE_LEVEL = 62; ++ ++ private final UpdateQueue updateQueue; ++ private final ConcurrentHashMap sections = new ConcurrentHashMap<>(); ++ ++ public ThreadedTicketLevelPropagator() { ++ this.updateQueue = new UpdateQueue(); ++ } ++ ++ // must hold ticket lock for: ++ // (posX & ~(SECTION_SIZE - 1), posZ & ~(SECTION_SIZE - 1)) to (posX | (SECTION_SIZE - 1), posZ | (SECTION_SIZE - 1)) ++ public void setSource(final int posX, final int posZ, final int to) { ++ if (to < 1 || to > MAX_SOURCE_LEVEL) { ++ throw new IllegalArgumentException("Source: " + to); ++ } ++ ++ final int sectionX = posX >> SECTION_SHIFT; ++ final int sectionZ = posZ >> SECTION_SHIFT; ++ ++ final Coordinate coordinate = new Coordinate(sectionX, sectionZ); ++ Section section = this.sections.get(coordinate); ++ if (section == null) { ++ if (null != this.sections.putIfAbsent(coordinate, section = new Section(sectionX, sectionZ))) { ++ throw new IllegalStateException("Race condition while creating new section"); ++ } ++ } ++ ++ final int localIdx = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); ++ final short sLocalIdx = (short)localIdx; ++ ++ final short sourceAndLevel = section.levels[localIdx]; ++ final int currentSource = (sourceAndLevel >>> 8) & 0xFF; ++ ++ if (currentSource == to) { ++ // nothing to do ++ // make sure to kill the current update, if any ++ section.queuedSources.replace(sLocalIdx, (byte)to); ++ return; ++ } ++ ++ if (section.queuedSources.put(sLocalIdx, (byte)to) == Section.NO_QUEUED_UPDATE && section.queuedSources.size() == 1) { ++ this.queueSectionUpdate(section); ++ } ++ } ++ ++ // must hold ticket lock for: ++ // (posX & ~(SECTION_SIZE - 1), posZ & ~(SECTION_SIZE - 1)) to (posX | (SECTION_SIZE - 1), posZ | (SECTION_SIZE - 1)) ++ public void removeSource(final int posX, final int posZ) { ++ final int sectionX = posX >> SECTION_SHIFT; ++ final int sectionZ = posZ >> SECTION_SHIFT; ++ ++ final Coordinate coordinate = new Coordinate(sectionX, sectionZ); ++ final Section section = this.sections.get(coordinate); ++ ++ if (section == null) { ++ return; ++ } ++ ++ final int localIdx = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); ++ final short sLocalIdx = (short)localIdx; ++ ++ final int currentSource = (section.levels[localIdx] >>> 8) & 0xFF; ++ ++ if (currentSource == 0) { ++ // we use replace here so that we do not possibly multi-queue a section for an update ++ section.queuedSources.replace(sLocalIdx, (byte)0); ++ return; ++ } ++ ++ if (section.queuedSources.put(sLocalIdx, (byte)0) == Section.NO_QUEUED_UPDATE && section.queuedSources.size() == 1) { ++ this.queueSectionUpdate(section); ++ } ++ } ++ ++ private void queueSectionUpdate(final Section section) { ++ this.updateQueue.append(new UpdateQueue.UpdateQueueNode(section, null)); ++ } ++ ++ public boolean hasPendingUpdates() { ++ return !this.updateQueue.isEmpty(); ++ } ++ ++ // holds ticket lock for every chunk section represented by any position in the key set ++ // updates is modifiable and passed to processSchedulingUpdates after this call ++ protected abstract void processLevelUpdates(final Long2ByteLinkedOpenHashMap updates); ++ ++ // holds ticket lock for every chunk section represented by any position in the key set ++ // holds scheduling lock in max access radius for every position held by the ticket lock ++ // updates is cleared after this call ++ protected abstract void processSchedulingUpdates(final Long2ByteLinkedOpenHashMap updates, final List scheduledTasks, ++ final List changedFullStatus); ++ ++ // must hold ticket lock for every position in the sections in one radius around sectionX,sectionZ ++ public boolean performUpdate(final int sectionX, final int sectionZ, final ReentrantAreaLock schedulingLock, ++ final List scheduledTasks, final List changedFullStatus) { ++ if (!this.hasPendingUpdates()) { ++ return false; ++ } ++ ++ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ)); ++ final Section section = this.sections.get(coordinate); ++ ++ if (section == null || section.queuedSources.isEmpty()) { ++ // no section or no updates ++ return false; ++ } ++ ++ final Propagator propagator = Propagator.acquirePropagator(); ++ final boolean ret = this.performUpdate(section, null, propagator, ++ null, schedulingLock, scheduledTasks, changedFullStatus ++ ); ++ Propagator.returnPropagator(propagator); ++ return ret; ++ } ++ ++ private boolean performUpdate(final Section section, final UpdateQueue.UpdateQueueNode node, final Propagator propagator, ++ final ReentrantAreaLock ticketLock, final ReentrantAreaLock schedulingLock, ++ final List scheduledTasks, final List changedFullStatus) { ++ final int sectionX = section.sectionX; ++ final int sectionZ = section.sectionZ; ++ ++ final int rad1MinX = (sectionX - 1) << SECTION_SHIFT; ++ final int rad1MinZ = (sectionZ - 1) << SECTION_SHIFT; ++ final int rad1MaxX = ((sectionX + 1) << SECTION_SHIFT) | (SECTION_SIZE - 1); ++ final int rad1MaxZ = ((sectionZ + 1) << SECTION_SHIFT) | (SECTION_SIZE - 1); ++ ++ // set up encode offset first as we need to queue level changes _before_ ++ propagator.setupEncodeOffset(sectionX, sectionZ); ++ ++ final int coordinateOffset = propagator.coordinateOffset; ++ ++ final ReentrantAreaLock.Node ticketNode = ticketLock == null ? null : ticketLock.lock(rad1MinX, rad1MinZ, rad1MaxX, rad1MaxZ); ++ final boolean ret; ++ try { ++ // first, check if this update was stolen ++ if (section != this.sections.get(new Coordinate(sectionX, sectionZ))) { ++ // occurs when a stolen update deletes this section ++ // it is possible that another update is scheduled, but that one will have the correct section ++ if (node != null) { ++ this.updateQueue.remove(node); ++ } ++ return false; ++ } ++ ++ final int oldSourceSize = section.sources.size(); ++ ++ // process pending sources ++ for (final Iterator iterator = section.queuedSources.short2ByteEntrySet().fastIterator(); iterator.hasNext();) { ++ final Short2ByteMap.Entry entry = iterator.next(); ++ final int pos = (int)entry.getShortKey(); ++ final int posX = (pos & (SECTION_SIZE - 1)) | (sectionX << SECTION_SHIFT); ++ final int posZ = ((pos >> SECTION_SHIFT) & (SECTION_SIZE - 1)) | (sectionZ << SECTION_SHIFT); ++ final int newSource = (int)entry.getByteValue(); ++ ++ final short currentEncoded = section.levels[pos]; ++ final int currLevel = currentEncoded & 0xFF; ++ final int prevSource = (currentEncoded >>> 8) & 0xFF; ++ ++ if (prevSource == newSource) { ++ // nothing changed ++ continue; ++ } ++ ++ if ((prevSource < currLevel && newSource <= currLevel) || newSource == currLevel) { ++ // just update the source, don't need to propagate change ++ section.levels[pos] = (short)(currLevel | (newSource << 8)); ++ // level is unchanged, don't add to changed positions ++ } else { ++ // set current level and current source to new source ++ section.levels[pos] = (short)(newSource | (newSource << 8)); ++ // must add to updated positions in case this is final ++ propagator.updatedPositions.put(Coordinate.key(posX, posZ), (byte)newSource); ++ if (newSource != 0) { ++ // queue increase with new source level ++ propagator.appendToIncreaseQueue( ++ ((long)(posX + (posZ << Propagator.COORDINATE_BITS) + coordinateOffset) & ((1L << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) - 1)) | ++ ((newSource & (LEVEL_COUNT - 1L)) << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) | ++ (Propagator.ALL_DIRECTIONS_BITSET << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS + LEVEL_BITS)) ++ ); ++ } ++ // queue decrease with previous level ++ if (newSource < currLevel) { ++ propagator.appendToDecreaseQueue( ++ ((long)(posX + (posZ << Propagator.COORDINATE_BITS) + coordinateOffset) & ((1L << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) - 1)) | ++ ((currLevel & (LEVEL_COUNT - 1L)) << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) | ++ (Propagator.ALL_DIRECTIONS_BITSET << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS + LEVEL_BITS)) ++ ); ++ } ++ } ++ ++ if (newSource == 0) { ++ // prevSource != newSource, so we are removing this source ++ section.sources.remove((short)pos); ++ } else if (prevSource == 0) { ++ // prevSource != newSource, so we are adding this source ++ section.sources.add((short)pos); ++ } ++ } ++ ++ section.queuedSources.clear(); ++ ++ final int newSourceSize = section.sources.size(); ++ ++ if (oldSourceSize == 0 && newSourceSize != 0) { ++ // need to make sure the sections in 1 radius are initialised ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ if ((dx | dz) == 0) { ++ continue; ++ } ++ final int offX = dx + sectionX; ++ final int offZ = dz + sectionZ; ++ final Coordinate coordinate = new Coordinate(offX, offZ); ++ final Section neighbour = this.sections.computeIfAbsent(coordinate, (final Coordinate keyInMap) -> { ++ return new Section(Coordinate.x(keyInMap.key), Coordinate.z(keyInMap.key)); ++ }); ++ ++ // increase ref count ++ ++neighbour.oneRadNeighboursWithSources; ++ if (neighbour.oneRadNeighboursWithSources <= 0 || neighbour.oneRadNeighboursWithSources > 8) { ++ throw new IllegalStateException(Integer.toString(neighbour.oneRadNeighboursWithSources)); ++ } ++ } ++ } ++ } ++ ++ if (propagator.hasUpdates()) { ++ propagator.setupCaches(this, sectionX, sectionZ, 1); ++ propagator.performDecrease(); ++ // don't need try-finally, as any exception will cause the propagator to not be returned ++ propagator.destroyCaches(); ++ } ++ ++ if (newSourceSize == 0) { ++ final boolean decrementRef = oldSourceSize != 0; ++ // check for section de-init ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ final int offX = dx + sectionX; ++ final int offZ = dz + sectionZ; ++ final Coordinate coordinate = new Coordinate(offX, offZ); ++ final Section neighbour = this.sections.get(coordinate); ++ ++ if (neighbour == null) { ++ if (oldSourceSize == 0 && (dx | dz) != 0) { ++ // since we don't have sources, this section is allowed to null ++ continue; ++ } ++ throw new IllegalStateException("??"); ++ } ++ ++ if (decrementRef && (dx | dz) != 0) { ++ // decrease ref count, but only for neighbours ++ --neighbour.oneRadNeighboursWithSources; ++ } ++ ++ // we need to check the current section for de-init as well ++ if (neighbour.oneRadNeighboursWithSources == 0) { ++ if (neighbour.queuedSources.isEmpty() && neighbour.sources.isEmpty()) { ++ // need to de-init ++ this.sections.remove(coordinate); ++ } // else: neighbour is queued for an update, and it will de-init itself ++ } else if (neighbour.oneRadNeighboursWithSources < 0 || neighbour.oneRadNeighboursWithSources > 8) { ++ throw new IllegalStateException(Integer.toString(neighbour.oneRadNeighboursWithSources)); ++ } ++ } ++ } ++ } ++ ++ ++ ret = !propagator.updatedPositions.isEmpty(); ++ ++ if (ret) { ++ this.processLevelUpdates(propagator.updatedPositions); ++ ++ if (!propagator.updatedPositions.isEmpty()) { ++ // now we can actually update the ticket levels in the chunk holders ++ final int maxScheduleRadius = 2 * ChunkTaskScheduler.getMaxAccessRadius(); ++ ++ // allow the chunkholders to process ticket level updates without needing to acquire the schedule lock every time ++ final ReentrantAreaLock.Node schedulingNode = schedulingLock.lock( ++ rad1MinX - maxScheduleRadius, rad1MinZ - maxScheduleRadius, ++ rad1MaxX + maxScheduleRadius, rad1MaxZ + maxScheduleRadius ++ ); ++ try { ++ this.processSchedulingUpdates(propagator.updatedPositions, scheduledTasks, changedFullStatus); ++ } finally { ++ schedulingLock.unlock(schedulingNode); ++ } ++ } ++ ++ propagator.updatedPositions.clear(); ++ } ++ } finally { ++ if (ticketLock != null) { ++ ticketLock.unlock(ticketNode); ++ } ++ } ++ ++ // finished ++ if (node != null) { ++ this.updateQueue.remove(node); ++ } ++ ++ return ret; ++ } ++ ++ public boolean performUpdates(final ReentrantAreaLock ticketLock, final ReentrantAreaLock schedulingLock, ++ final List scheduledTasks, final List changedFullStatus) { ++ if (this.updateQueue.isEmpty()) { ++ return false; ++ } ++ ++ final long maxOrder = this.updateQueue.getLastOrder(); ++ ++ boolean updated = false; ++ Propagator propagator = null; ++ ++ for (;;) { ++ final UpdateQueue.UpdateQueueNode toUpdate = this.updateQueue.acquireNextToUpdate(maxOrder); ++ if (toUpdate == null) { ++ this.updateQueue.awaitFirst(maxOrder); ++ ++ if (!this.updateQueue.hasRemainingUpdates(maxOrder)) { ++ if (propagator != null) { ++ Propagator.returnPropagator(propagator); ++ } ++ return updated; ++ } ++ ++ continue; ++ } ++ ++ if (propagator == null) { ++ propagator = Propagator.acquirePropagator(); ++ } ++ ++ updated |= this.performUpdate(toUpdate.section, toUpdate, propagator, ticketLock, schedulingLock, scheduledTasks, changedFullStatus); ++ } ++ } ++ ++ private static final class UpdateQueue { ++ ++ private volatile UpdateQueueNode head; ++ private volatile UpdateQueueNode tail; ++ private volatile UpdateQueueNode lastUpdating; ++ ++ protected static final VarHandle HEAD_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "head", UpdateQueueNode.class); ++ protected static final VarHandle TAIL_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "tail", UpdateQueueNode.class); ++ protected static final VarHandle LAST_UPDATING = ConcurrentUtil.getVarHandle(UpdateQueue.class, "lastUpdating", UpdateQueueNode.class); ++ ++ /* head */ ++ ++ protected final void setHeadPlain(final UpdateQueueNode newHead) { ++ HEAD_HANDLE.set(this, newHead); ++ } ++ ++ protected final void setHeadOpaque(final UpdateQueueNode newHead) { ++ HEAD_HANDLE.setOpaque(this, newHead); ++ } ++ ++ protected final UpdateQueueNode getHeadPlain() { ++ return (UpdateQueueNode)HEAD_HANDLE.get(this); ++ } ++ ++ protected final UpdateQueueNode getHeadOpaque() { ++ return (UpdateQueueNode)HEAD_HANDLE.getOpaque(this); ++ } ++ ++ protected final UpdateQueueNode getHeadAcquire() { ++ return (UpdateQueueNode)HEAD_HANDLE.getAcquire(this); ++ } ++ ++ /* tail */ ++ ++ protected final void setTailPlain(final UpdateQueueNode newTail) { ++ TAIL_HANDLE.set(this, newTail); ++ } ++ ++ protected final void setTailOpaque(final UpdateQueueNode newTail) { ++ TAIL_HANDLE.setOpaque(this, newTail); ++ } ++ ++ protected final UpdateQueueNode getTailPlain() { ++ return (UpdateQueueNode)TAIL_HANDLE.get(this); ++ } ++ ++ protected final UpdateQueueNode getTailOpaque() { ++ return (UpdateQueueNode)TAIL_HANDLE.getOpaque(this); ++ } ++ ++ /* lastUpdating */ ++ ++ protected final UpdateQueueNode getLastUpdatingVolatile() { ++ return (UpdateQueueNode)LAST_UPDATING.getVolatile(this); ++ } ++ ++ protected final UpdateQueueNode compareAndExchangeLastUpdatingVolatile(final UpdateQueueNode expect, final UpdateQueueNode update) { ++ return (UpdateQueueNode)LAST_UPDATING.compareAndExchange(this, expect, update); ++ } ++ ++ public UpdateQueue() { ++ final UpdateQueueNode dummy = new UpdateQueueNode(null, null); ++ dummy.order = -1L; ++ dummy.preventAdds(); ++ ++ this.setHeadPlain(dummy); ++ this.setTailPlain(dummy); ++ } ++ ++ public boolean isEmpty() { ++ return this.peek() == null; ++ } ++ ++ public boolean hasRemainingUpdates(final long maxUpdate) { ++ final UpdateQueueNode node = this.peek(); ++ return node != null && node.order <= maxUpdate; ++ } ++ ++ public long getLastOrder() { ++ for (UpdateQueueNode tail = this.getTailOpaque(), curr = tail;;) { ++ final UpdateQueueNode next = curr.getNextVolatile(); ++ if (next == null) { ++ // try to update stale tail ++ if (this.getTailOpaque() == tail && curr != tail) { ++ this.setTailOpaque(curr); ++ } ++ return curr.order; ++ } ++ curr = next; ++ } ++ } ++ ++ public UpdateQueueNode acquireNextToUpdate(final long maxOrder) { ++ int failures = 0; ++ for (UpdateQueueNode prev = this.getLastUpdatingVolatile();;) { ++ UpdateQueueNode next = prev == null ? this.peek() : prev.next; ++ ++ if (next == null || next.order > maxOrder) { ++ return null; ++ } ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (prev == (prev = this.compareAndExchangeLastUpdatingVolatile(prev, next))) { ++ return next; ++ } ++ ++ ++failures; ++ } ++ } ++ ++ public void awaitFirst(final long maxOrder) { ++ final UpdateQueueNode earliest = this.peek(); ++ if (earliest == null || earliest.order > maxOrder) { ++ return; ++ } ++ ++ final Thread currThread = Thread.currentThread(); ++ // we do not use add-blocking because we use the nullability of the section to block ++ // remove() does not begin to poll from the wait queue until the section is null'd, ++ // and so provided we check the nullability before parking there is no ordering of these operations ++ // such that remove() finishes polling from the wait queue while section is not null ++ earliest.add(currThread); ++ ++ // wait until completed ++ while (earliest.getSectionVolatile() != null) { ++ LockSupport.park(); ++ } ++ } ++ ++ public UpdateQueueNode peek() { ++ for (UpdateQueueNode head = this.getHeadOpaque(), curr = head;;) { ++ final UpdateQueueNode next = curr.getNextVolatile(); ++ final Section element = curr.getSectionVolatile(); /* Likely in sync */ ++ ++ if (element != null) { ++ if (this.getHeadOpaque() == head && curr != head) { ++ this.setHeadOpaque(curr); ++ } ++ return curr; ++ } ++ ++ if (next == null) { ++ if (this.getHeadOpaque() == head && curr != head) { ++ this.setHeadOpaque(curr); ++ } ++ return null; ++ } ++ curr = next; ++ } ++ } ++ ++ public void remove(final UpdateQueueNode node) { ++ // mark as removed ++ node.setSectionVolatile(null); ++ ++ // use peek to advance head ++ this.peek(); ++ ++ // unpark any waiters / block the wait queue ++ Thread unpark; ++ while ((unpark = node.poll()) != null) { ++ LockSupport.unpark(unpark); ++ } ++ } ++ ++ public void append(final UpdateQueueNode node) { ++ int failures = 0; ++ ++ for (UpdateQueueNode currTail = this.getTailOpaque(), curr = currTail;;) { ++ /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */ ++ /* It is likely due to a cache miss caused by another write to the next field */ ++ final UpdateQueueNode next = curr.getNextVolatile(); ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (next == null) { ++ node.order = curr.order + 1L; ++ final UpdateQueueNode compared = curr.compareExchangeNextVolatile(null, node); ++ ++ if (compared == null) { ++ /* Added */ ++ /* Avoid CASing on tail more than we need to */ ++ /* CAS to avoid setting an out-of-date tail */ ++ if (this.getTailOpaque() == currTail) { ++ this.setTailOpaque(node); ++ } ++ return; ++ } ++ ++ ++failures; ++ curr = compared; ++ continue; ++ } ++ ++ if (curr == currTail) { ++ /* Tail is likely not up-to-date */ ++ curr = next; ++ } else { ++ /* Try to update to tail */ ++ if (currTail == (currTail = this.getTailOpaque())) { ++ curr = next; ++ } else { ++ curr = currTail; ++ } ++ } ++ } ++ } ++ ++ // each node also represents a set of waiters, represented by the MTQ ++ // if the queue is add-blocked, then the update is complete ++ private static final class UpdateQueueNode extends MultiThreadedQueue { ++ private long order; ++ private Section section; ++ private volatile UpdateQueueNode next; ++ ++ protected static final VarHandle SECTION_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "section", Section.class); ++ protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "next", UpdateQueueNode.class); ++ ++ public UpdateQueueNode(final Section section, final UpdateQueueNode next) { ++ SECTION_HANDLE.set(this, section); ++ NEXT_HANDLE.set(this, next); ++ } ++ ++ /* section */ ++ ++ protected final Section getSectionPlain() { ++ return (Section)SECTION_HANDLE.get(this); ++ } ++ ++ protected final Section getSectionVolatile() { ++ return (Section)SECTION_HANDLE.getVolatile(this); ++ } ++ ++ protected final void setSectionPlain(final Section update) { ++ SECTION_HANDLE.set(this, update); ++ } ++ ++ protected final void setSectionOpaque(final Section update) { ++ SECTION_HANDLE.setOpaque(this, update); ++ } ++ ++ protected final void setSectionVolatile(final Section update) { ++ SECTION_HANDLE.setVolatile(this, update); ++ } ++ ++ protected final Section getAndSetSectionVolatile(final Section update) { ++ return (Section)SECTION_HANDLE.getAndSet(this, update); ++ } ++ ++ protected final Section compareExchangeSectionVolatile(final Section expect, final Section update) { ++ return (Section)SECTION_HANDLE.compareAndExchange(this, expect, update); ++ } ++ ++ /* next */ ++ ++ protected final UpdateQueueNode getNextPlain() { ++ return (UpdateQueueNode)NEXT_HANDLE.get(this); ++ } ++ ++ protected final UpdateQueueNode getNextOpaque() { ++ return (UpdateQueueNode)NEXT_HANDLE.getOpaque(this); ++ } ++ ++ protected final UpdateQueueNode getNextAcquire() { ++ return (UpdateQueueNode)NEXT_HANDLE.getAcquire(this); ++ } ++ ++ protected final UpdateQueueNode getNextVolatile() { ++ return (UpdateQueueNode)NEXT_HANDLE.getVolatile(this); ++ } ++ ++ protected final void setNextPlain(final UpdateQueueNode next) { ++ NEXT_HANDLE.set(this, next); ++ } ++ ++ protected final void setNextVolatile(final UpdateQueueNode next) { ++ NEXT_HANDLE.setVolatile(this, next); ++ } ++ ++ protected final UpdateQueueNode compareExchangeNextVolatile(final UpdateQueueNode expect, final UpdateQueueNode set) { ++ return (UpdateQueueNode)NEXT_HANDLE.compareAndExchange(this, expect, set); ++ } ++ } ++ } ++ ++ private static final class Section { ++ ++ // upper 8 bits: sources, lower 8 bits: level ++ // if we REALLY wanted to get crazy, we could make the increase propagator use MethodHandles#byteArrayViewVarHandle ++ // to read and write the lower 8 bits of this array directly rather than reading, updating the bits, then writing back. ++ private final short[] levels = new short[SECTION_SIZE * SECTION_SIZE]; ++ // set of local positions that represent sources ++ private final ShortOpenHashSet sources = new ShortOpenHashSet(); ++ // map of local index to new source level ++ // the source level _cannot_ be updated in the backing storage immediately since the update ++ private static final byte NO_QUEUED_UPDATE = (byte)-1; ++ private final Short2ByteLinkedOpenHashMap queuedSources = new Short2ByteLinkedOpenHashMap(); ++ { ++ this.queuedSources.defaultReturnValue(NO_QUEUED_UPDATE); ++ } ++ private int oneRadNeighboursWithSources = 0; ++ ++ public final int sectionX; ++ public final int sectionZ; ++ ++ public Section(final int sectionX, final int sectionZ) { ++ this.sectionX = sectionX; ++ this.sectionZ = sectionZ; ++ } ++ ++ public boolean isZero() { ++ for (final short val : this.levels) { ++ if (val != 0) { ++ return false; ++ } ++ } ++ return true; ++ } ++ ++ @Override ++ public String toString() { ++ final StringBuilder ret = new StringBuilder(); ++ ++ for (int x = 0; x < SECTION_SIZE; ++x) { ++ ret.append("levels x=").append(x).append("\n"); ++ for (int z = 0; z < SECTION_SIZE; ++z) { ++ final short v = this.levels[x | (z << SECTION_SHIFT)]; ++ ret.append(v & 0xFF).append("."); ++ } ++ ret.append("\n"); ++ ret.append("sources x=").append(x).append("\n"); ++ for (int z = 0; z < SECTION_SIZE; ++z) { ++ final short v = this.levels[x | (z << SECTION_SHIFT)]; ++ ret.append((v >>> 8) & 0xFF).append("."); ++ } ++ ret.append("\n\n"); ++ } ++ ++ return ret.toString(); ++ } ++ } ++ ++ ++ private static final class Propagator { ++ ++ private static final ArrayDeque CACHED_PROPAGATORS = new ArrayDeque<>(); ++ private static final int MAX_PROPAGATORS = Runtime.getRuntime().availableProcessors() * 2; ++ ++ private static Propagator acquirePropagator() { ++ synchronized (CACHED_PROPAGATORS) { ++ final Propagator ret = CACHED_PROPAGATORS.pollFirst(); ++ if (ret != null) { ++ return ret; ++ } ++ } ++ return new Propagator(); ++ } ++ ++ private static void returnPropagator(final Propagator propagator) { ++ synchronized (CACHED_PROPAGATORS) { ++ if (CACHED_PROPAGATORS.size() < MAX_PROPAGATORS) { ++ CACHED_PROPAGATORS.add(propagator); ++ } ++ } ++ } ++ ++ private static final int SECTION_RADIUS = 2; ++ private static final int SECTION_CACHE_WIDTH = 2 * SECTION_RADIUS + 1; ++ // minimum number of bits to represent [0, SECTION_SIZE * SECTION_CACHE_WIDTH) ++ private static final int COORDINATE_BITS = 9; ++ private static final int COORDINATE_SIZE = 1 << COORDINATE_BITS; ++ static { ++ if ((SECTION_SIZE * SECTION_CACHE_WIDTH) > (1 << COORDINATE_BITS)) { ++ throw new IllegalStateException("Adjust COORDINATE_BITS"); ++ } ++ } ++ // index = x + (z * SECTION_CACHE_WIDTH) ++ // (this requires x >= 0 and z >= 0) ++ private final Section[] sections = new Section[SECTION_CACHE_WIDTH * SECTION_CACHE_WIDTH]; ++ ++ private int encodeOffsetX; ++ private int encodeOffsetZ; ++ ++ private int coordinateOffset; ++ ++ private int encodeSectionOffsetX; ++ private int encodeSectionOffsetZ; ++ ++ private int sectionIndexOffset; ++ ++ public final boolean hasUpdates() { ++ return this.decreaseQueueInitialLength != 0 || this.increaseQueueInitialLength != 0; ++ } ++ ++ protected final void setupEncodeOffset(final int centerSectionX, final int centerSectionZ) { ++ final int maxCoordinate = (SECTION_RADIUS * SECTION_SIZE - 1); ++ // must have that encoded >= 0 ++ // coordinates can range from [-maxCoordinate + centerSection*SECTION_SIZE, maxCoordinate + centerSection*SECTION_SIZE] ++ // we want a range of [0, maxCoordinate*2] ++ // so, 0 = -maxCoordinate + centerSection*SECTION_SIZE + offset ++ this.encodeOffsetX = maxCoordinate - (centerSectionX << SECTION_SHIFT); ++ this.encodeOffsetZ = maxCoordinate - (centerSectionZ << SECTION_SHIFT); ++ ++ // encoded coordinates range from [0, SECTION_SIZE * SECTION_CACHE_WIDTH) ++ // coordinate index = (x + encodeOffsetX) + ((z + encodeOffsetZ) << COORDINATE_BITS) ++ this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << COORDINATE_BITS); ++ ++ // need encoded values to be >= 0 ++ // so, 0 = (-SECTION_RADIUS + centerSectionX) + encodeOffset ++ this.encodeSectionOffsetX = SECTION_RADIUS - centerSectionX; ++ this.encodeSectionOffsetZ = SECTION_RADIUS - centerSectionZ; ++ ++ // section index = (secX + encodeSectionOffsetX) + ((secZ + encodeSectionOffsetZ) * SECTION_CACHE_WIDTH) ++ this.sectionIndexOffset = this.encodeSectionOffsetX + (this.encodeSectionOffsetZ * SECTION_CACHE_WIDTH); ++ } ++ ++ // must hold ticket lock for (centerSectionX,centerSectionZ) in radius rad ++ // must call setupEncodeOffset ++ protected final void setupCaches(final ThreadedTicketLevelPropagator propagator, ++ final int centerSectionX, final int centerSectionZ, ++ final int rad) { ++ for (int dz = -rad; dz <= rad; ++dz) { ++ for (int dx = -rad; dx <= rad; ++dx) { ++ final int sectionX = centerSectionX + dx; ++ final int sectionZ = centerSectionZ + dz; ++ final Coordinate coordinate = new Coordinate(sectionX, sectionZ); ++ final Section section = propagator.sections.get(coordinate); ++ ++ if (section == null) { ++ throw new IllegalStateException("Section at " + coordinate + " should not be null"); ++ } ++ ++ this.setSectionInCache(sectionX, sectionZ, section); ++ } ++ } ++ } ++ ++ protected final void setSectionInCache(final int sectionX, final int sectionZ, final Section section) { ++ this.sections[sectionX + SECTION_CACHE_WIDTH*sectionZ + this.sectionIndexOffset] = section; ++ } ++ ++ protected final Section getSection(final int sectionX, final int sectionZ) { ++ return this.sections[sectionX + SECTION_CACHE_WIDTH*sectionZ + this.sectionIndexOffset]; ++ } ++ ++ protected final int getLevel(final int posX, final int posZ) { ++ final Section section = this.sections[(posX >> SECTION_SHIFT) + SECTION_CACHE_WIDTH*(posZ >> SECTION_SHIFT) + this.sectionIndexOffset]; ++ if (section != null) { ++ return (int)section.levels[(posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT)] & 0xFF; ++ } ++ ++ return 0; ++ } ++ ++ protected final void setLevel(final int posX, final int posZ, final int to) { ++ final Section section = this.sections[(posX >> SECTION_SHIFT) + SECTION_CACHE_WIDTH*(posZ >> SECTION_SHIFT) + this.sectionIndexOffset]; ++ if (section != null) { ++ final int index = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); ++ final short level = section.levels[index]; ++ section.levels[index] = (short)((level & ~0xFF) | (to & 0xFF)); ++ this.updatedPositions.put(Coordinate.key(posX, posZ), (byte)to); ++ } ++ } ++ ++ protected final void destroyCaches() { ++ Arrays.fill(this.sections, null); ++ } ++ ++ // contains: ++ // lower (COORDINATE_BITS(9) + COORDINATE_BITS(9) = 18) bits encoded position: (x | (z << COORDINATE_BITS)) ++ // next LEVEL_BITS (6) bits: propagated level [0, 63] ++ // propagation directions bitset (16 bits): ++ protected static final long ALL_DIRECTIONS_BITSET = ( ++ // z = -1 ++ (1L << ((1 - 1) | ((1 - 1) << 2))) | ++ (1L << ((1 + 0) | ((1 - 1) << 2))) | ++ (1L << ((1 + 1) | ((1 - 1) << 2))) | ++ ++ // z = 0 ++ (1L << ((1 - 1) | ((1 + 0) << 2))) | ++ //(1L << ((1 + 0) | ((1 + 0) << 2))) | // exclude (0,0) ++ (1L << ((1 + 1) | ((1 + 0) << 2))) | ++ ++ // z = 1 ++ (1L << ((1 - 1) | ((1 + 1) << 2))) | ++ (1L << ((1 + 0) | ((1 + 1) << 2))) | ++ (1L << ((1 + 1) | ((1 + 1) << 2))) ++ ); ++ ++ private void ex(int bitset) { ++ for (int i = 0, len = Integer.bitCount(bitset); i < len; ++i) { ++ final int set = Integer.numberOfTrailingZeros(bitset); ++ final int tailingBit = (-bitset) & bitset; ++ // XOR to remove the trailing bit ++ bitset ^= tailingBit; ++ ++ // the encoded value set is (x_val) | (z_val << 2), totaling 4 bits ++ // thus, the bitset is 16 bits wide where each one represents a direction to propagate and the ++ // index of the set bit is the encoded value ++ // the encoded coordinate has 3 valid states: ++ // 0b00 (0) -> -1 ++ // 0b01 (1) -> 0 ++ // 0b10 (2) -> 1 ++ // the decode operation then is val - 1, and the encode operation is val + 1 ++ final int xOff = (set & 3) - 1; ++ final int zOff = ((set >>> 2) & 3) - 1; ++ System.out.println("Encoded: (" + xOff + "," + zOff + ")"); ++ } ++ } ++ ++ private void ch(long bs, int shift) { ++ int bitset = (int)(bs >>> shift); ++ for (int i = 0, len = Integer.bitCount(bitset); i < len; ++i) { ++ final int set = Integer.numberOfTrailingZeros(bitset); ++ final int tailingBit = (-bitset) & bitset; ++ // XOR to remove the trailing bit ++ bitset ^= tailingBit; ++ ++ // the encoded value set is (x_val) | (z_val << 2), totaling 4 bits ++ // thus, the bitset is 16 bits wide where each one represents a direction to propagate and the ++ // index of the set bit is the encoded value ++ // the encoded coordinate has 3 valid states: ++ // 0b00 (0) -> -1 ++ // 0b01 (1) -> 0 ++ // 0b10 (2) -> 1 ++ // the decode operation then is val - 1, and the encode operation is val + 1 ++ final int xOff = (set & 3) - 1; ++ final int zOff = ((set >>> 2) & 3) - 1; ++ if (Math.abs(xOff) > 1 || Math.abs(zOff) > 1 || (xOff | zOff) == 0) { ++ throw new IllegalStateException(); ++ } ++ } ++ } ++ ++ // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading ++ // updates for sources ++ protected static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 1; ++ // 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 >>> 0; ++ ++ protected long[] increaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2]; ++ protected int increaseQueueInitialLength; ++ protected long[] decreaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2]; ++ protected int decreaseQueueInitialLength; ++ ++ protected final Long2ByteLinkedOpenHashMap updatedPositions = new Long2ByteLinkedOpenHashMap(); ++ ++ 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; ++ return; ++ } else { ++ queue[idx] = value; ++ return; ++ } ++ } ++ ++ 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; ++ return; ++ } else { ++ queue[idx] = value; ++ return; ++ } ++ } ++ ++ protected final void performIncrease() { ++ long[] queue = this.increaseQueue; ++ int queueReadIndex = 0; ++ int queueLength = this.increaseQueueInitialLength; ++ this.increaseQueueInitialLength = 0; ++ final int decodeOffsetX = -this.encodeOffsetX; ++ final int decodeOffsetZ = -this.encodeOffsetZ; ++ final int encodeOffset = this.coordinateOffset; ++ final int sectionOffset = this.sectionIndexOffset; ++ ++ final Long2ByteLinkedOpenHashMap updatedPositions = this.updatedPositions; ++ ++ while (queueReadIndex < queueLength) { ++ final long queueValue = queue[queueReadIndex++]; ++ ++ final int posX = ((int)queueValue & (COORDINATE_SIZE - 1)) + decodeOffsetX; ++ final int posZ = (((int)queueValue >>> COORDINATE_BITS) & (COORDINATE_SIZE - 1)) + decodeOffsetZ; ++ final int propagatedLevel = ((int)queueValue >>> (COORDINATE_BITS + COORDINATE_BITS)) & (LEVEL_COUNT - 1); ++ // note: the above code requires coordinate bits * 2 < 32 ++ // bitset is 16 bits ++ int propagateDirectionBitset = (int)(queueValue >>> (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) & ((1 << 16) - 1); ++ ++ if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) { ++ if (this.getLevel(posX, posZ) != propagatedLevel) { ++ // not at the level we expect, so something changed. ++ continue; ++ } ++ } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) { ++ // these are used to restore sources after a propagation decrease ++ this.setLevel(posX, posZ, propagatedLevel); ++ } ++ ++ // this bitset represents the values that we have not propagated to ++ // this bitset lets us determine what directions the neighbours we set should propagate to, in most cases ++ // significantly reducing the total number of ops ++ // since we propagate in a 1 radius, we need a 2 radius bitset to hold all possible values we would possibly need ++ // but if we use only 5x5 bits, then we need to use div/mod to retrieve coordinates from the bitset, so instead ++ // we use an 8x8 bitset and luckily that can be fit into only one long value (64 bits) ++ // to make things easy, we use positions [0, 4] in the bitset, with current position being 2 ++ // index = x | (z << 3) ++ ++ // to start, we eliminate everything 1 radius from the current position as the previous propagator ++ // must guarantee that either we propagate everything in 1 radius or we partially propagate for 1 radius ++ // but the rest not propagated are already handled ++ long currentPropagation = ~( ++ // z = -1 ++ (1L << ((2 - 1) | ((2 - 1) << 3))) | ++ (1L << ((2 + 0) | ((2 - 1) << 3))) | ++ (1L << ((2 + 1) | ((2 - 1) << 3))) | ++ ++ // z = 0 ++ (1L << ((2 - 1) | ((2 + 0) << 3))) | ++ (1L << ((2 + 0) | ((2 + 0) << 3))) | ++ (1L << ((2 + 1) | ((2 + 0) << 3))) | ++ ++ // z = 1 ++ (1L << ((2 - 1) | ((2 + 1) << 3))) | ++ (1L << ((2 + 0) | ((2 + 1) << 3))) | ++ (1L << ((2 + 1) | ((2 + 1) << 3))) ++ ); ++ ++ final int toPropagate = propagatedLevel - 1; ++ ++ // we could use while (propagateDirectionBitset != 0), but it's not a predictable branch. By counting ++ // the bits, the cpu loop predictor should perfectly predict the loop. ++ for (int l = 0, len = Integer.bitCount(propagateDirectionBitset); l < len; ++l) { ++ final int set = Integer.numberOfTrailingZeros(propagateDirectionBitset); ++ final int tailingBit = (-propagateDirectionBitset) & propagateDirectionBitset; ++ propagateDirectionBitset ^= tailingBit; ++ ++ // pDecode is from [0, 2], and 1 must be subtracted to fully decode the offset ++ // it has been split to save some cycles via parallelism ++ final int pDecodeX = (set & 3); ++ final int pDecodeZ = ((set >>> 2) & 3); ++ ++ // re-ordered -1 on the position decode into pos - 1 to occur in parallel with determining pDecodeX ++ final int offX = (posX - 1) + pDecodeX; ++ final int offZ = (posZ - 1) + pDecodeZ; ++ ++ final int sectionIndex = (offX >> SECTION_SHIFT) + ((offZ >> SECTION_SHIFT) * SECTION_CACHE_WIDTH) + sectionOffset; ++ final int localIndex = (offX & (SECTION_SIZE - 1)) | ((offZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); ++ ++ // to retrieve a set of bits from a long value: (n_bitmask << (nstartidx)) & bitset ++ // bitset idx = x | (z << 3) ++ ++ // read three bits, so we need 7L ++ // note that generally: off - pos = (pos - 1) + pDecode - pos = pDecode - 1 ++ // nstartidx1 = x rel -1 for z rel -1 ++ // = (offX - posX - 1 + 2) | ((offZ - posZ - 1 + 2) << 3) ++ // = (pDecodeX - 1 - 1 + 2) | ((pDecodeZ - 1 - 1 + 2) << 3) ++ // = pDecodeX | (pDecodeZ << 3) = start ++ final int start = pDecodeX | (pDecodeZ << 3); ++ final long bitsetLine1 = currentPropagation & (7L << (start)); ++ ++ // nstartidx2 = x rel -1 for z rel 0 = line after line1, so we can just add 8 (row length of bitset) ++ final long bitsetLine2 = currentPropagation & (7L << (start + 8)); ++ ++ // nstartidx2 = x rel -1 for z rel 0 = line after line2, so we can just add 8 (row length of bitset) ++ final long bitsetLine3 = currentPropagation & (7L << (start + (8 + 8))); ++ ++ // remove ("take") lines from bitset ++ currentPropagation ^= (bitsetLine1 | bitsetLine2 | bitsetLine3); ++ ++ // now try to propagate ++ final Section section = this.sections[sectionIndex]; ++ ++ // lower 8 bits are current level, next upper 7 bits are source level, next 1 bit is updated source flag ++ final short currentStoredLevel = section.levels[localIndex]; ++ final int currentLevel = currentStoredLevel & 0xFF; ++ ++ if (currentLevel >= toPropagate) { ++ continue; // already at the level we want ++ } ++ ++ // update level ++ section.levels[localIndex] = (short)((currentStoredLevel & ~0xFF) | (toPropagate & 0xFF)); ++ updatedPositions.putAndMoveToLast(Coordinate.key(offX, offZ), (byte)toPropagate); ++ ++ // queue next ++ if (toPropagate > 1) { ++ // now combine into one bitset to pass to child ++ // the child bitset is 4x4, so we just shift each line by 4 ++ // add the propagation bitset offset to each line to make it easy to OR it into the propagation queue value ++ final long childPropagation = ++ ((bitsetLine1 >>> (start)) << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = -1 ++ ((bitsetLine2 >>> (start + 8)) << (4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = 0 ++ ((bitsetLine3 >>> (start + (8 + 8))) << (4 + 4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); // z = 1 ++ ++ // don't queue update if toPropagate cannot propagate anything to neighbours ++ // (for increase, propagating 0 to neighbours is useless) ++ if (queueLength >= queue.length) { ++ queue = this.resizeIncreaseQueue(); ++ } ++ queue[queueLength++] = ++ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | ++ ((toPropagate & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | ++ childPropagation; //(ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); ++ continue; ++ } ++ continue; ++ } ++ } ++ } ++ ++ protected final void performDecrease() { ++ 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 decodeOffsetZ = -this.encodeOffsetZ; ++ final int encodeOffset = this.coordinateOffset; ++ final int sectionOffset = this.sectionIndexOffset; ++ ++ final Long2ByteLinkedOpenHashMap updatedPositions = this.updatedPositions; ++ ++ while (queueReadIndex < queueLength) { ++ final long queueValue = queue[queueReadIndex++]; ++ ++ final int posX = ((int)queueValue & (COORDINATE_SIZE - 1)) + decodeOffsetX; ++ final int posZ = (((int)queueValue >>> COORDINATE_BITS) & (COORDINATE_SIZE - 1)) + decodeOffsetZ; ++ final int propagatedLevel = ((int)queueValue >>> (COORDINATE_BITS + COORDINATE_BITS)) & (LEVEL_COUNT - 1); ++ // note: the above code requires coordinate bits * 2 < 32 ++ // bitset is 16 bits ++ int propagateDirectionBitset = (int)(queueValue >>> (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) & ((1 << 16) - 1); ++ ++ // this bitset represents the values that we have not propagated to ++ // this bitset lets us determine what directions the neighbours we set should propagate to, in most cases ++ // significantly reducing the total number of ops ++ // since we propagate in a 1 radius, we need a 2 radius bitset to hold all possible values we would possibly need ++ // but if we use only 5x5 bits, then we need to use div/mod to retrieve coordinates from the bitset, so instead ++ // we use an 8x8 bitset and luckily that can be fit into only one long value (64 bits) ++ // to make things easy, we use positions [0, 4] in the bitset, with current position being 2 ++ // index = x | (z << 3) ++ ++ // to start, we eliminate everything 1 radius from the current position as the previous propagator ++ // must guarantee that either we propagate everything in 1 radius or we partially propagate for 1 radius ++ // but the rest not propagated are already handled ++ long currentPropagation = ~( ++ // z = -1 ++ (1L << ((2 - 1) | ((2 - 1) << 3))) | ++ (1L << ((2 + 0) | ((2 - 1) << 3))) | ++ (1L << ((2 + 1) | ((2 - 1) << 3))) | ++ ++ // z = 0 ++ (1L << ((2 - 1) | ((2 + 0) << 3))) | ++ (1L << ((2 + 0) | ((2 + 0) << 3))) | ++ (1L << ((2 + 1) | ((2 + 0) << 3))) | ++ ++ // z = 1 ++ (1L << ((2 - 1) | ((2 + 1) << 3))) | ++ (1L << ((2 + 0) | ((2 + 1) << 3))) | ++ (1L << ((2 + 1) | ((2 + 1) << 3))) ++ ); ++ ++ final int toPropagate = propagatedLevel - 1; ++ ++ // we could use while (propagateDirectionBitset != 0), but it's not a predictable branch. By counting ++ // the bits, the cpu loop predictor should perfectly predict the loop. ++ for (int l = 0, len = Integer.bitCount(propagateDirectionBitset); l < len; ++l) { ++ final int set = Integer.numberOfTrailingZeros(propagateDirectionBitset); ++ final int tailingBit = (-propagateDirectionBitset) & propagateDirectionBitset; ++ propagateDirectionBitset ^= tailingBit; ++ ++ ++ // pDecode is from [0, 2], and 1 must be subtracted to fully decode the offset ++ // it has been split to save some cycles via parallelism ++ final int pDecodeX = (set & 3); ++ final int pDecodeZ = ((set >>> 2) & 3); ++ ++ // re-ordered -1 on the position decode into pos - 1 to occur in parallel with determining pDecodeX ++ final int offX = (posX - 1) + pDecodeX; ++ final int offZ = (posZ - 1) + pDecodeZ; ++ ++ final int sectionIndex = (offX >> SECTION_SHIFT) + ((offZ >> SECTION_SHIFT) * SECTION_CACHE_WIDTH) + sectionOffset; ++ final int localIndex = (offX & (SECTION_SIZE - 1)) | ((offZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); ++ ++ // to retrieve a set of bits from a long value: (n_bitmask << (nstartidx)) & bitset ++ // bitset idx = x | (z << 3) ++ ++ // read three bits, so we need 7L ++ // note that generally: off - pos = (pos - 1) + pDecode - pos = pDecode - 1 ++ // nstartidx1 = x rel -1 for z rel -1 ++ // = (offX - posX - 1 + 2) | ((offZ - posZ - 1 + 2) << 3) ++ // = (pDecodeX - 1 - 1 + 2) | ((pDecodeZ - 1 - 1 + 2) << 3) ++ // = pDecodeX | (pDecodeZ << 3) = start ++ final int start = pDecodeX | (pDecodeZ << 3); ++ final long bitsetLine1 = currentPropagation & (7L << (start)); ++ ++ // nstartidx2 = x rel -1 for z rel 0 = line after line1, so we can just add 8 (row length of bitset) ++ final long bitsetLine2 = currentPropagation & (7L << (start + 8)); ++ ++ // nstartidx2 = x rel -1 for z rel 0 = line after line2, so we can just add 8 (row length of bitset) ++ final long bitsetLine3 = currentPropagation & (7L << (start + (8 + 8))); ++ ++ // now try to propagate ++ final Section section = this.sections[sectionIndex]; ++ ++ // lower 8 bits are current level, next upper 7 bits are source level, next 1 bit is updated source flag ++ final short currentStoredLevel = section.levels[localIndex]; ++ final int currentLevel = currentStoredLevel & 0xFF; ++ final int sourceLevel = (currentStoredLevel >>> 8) & 0xFF; ++ ++ if (currentLevel == 0) { ++ continue; // already at the level we want ++ } ++ ++ if (currentLevel > toPropagate) { ++ // it looks like another source propagated here, so re-propagate it ++ if (increaseQueueLength >= increaseQueue.length) { ++ increaseQueue = this.resizeIncreaseQueue(); ++ } ++ increaseQueue[increaseQueueLength++] = ++ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | ++ ((currentLevel & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | ++ (FLAG_RECHECK_LEVEL | (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS))); ++ continue; ++ } ++ ++ // remove ("take") lines from bitset ++ // can't do this during decrease, TODO WHY? ++ //currentPropagation ^= (bitsetLine1 | bitsetLine2 | bitsetLine3); ++ ++ // update level ++ section.levels[localIndex] = (short)((currentStoredLevel & ~0xFF)); ++ updatedPositions.putAndMoveToLast(Coordinate.key(offX, offZ), (byte)0); ++ ++ if (sourceLevel != 0) { ++ // re-propagate source ++ // note: do not set recheck level, or else the propagation will fail ++ if (increaseQueueLength >= increaseQueue.length) { ++ increaseQueue = this.resizeIncreaseQueue(); ++ } ++ increaseQueue[increaseQueueLength++] = ++ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | ++ ((sourceLevel & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | ++ (FLAG_WRITE_LEVEL | (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS))); ++ } ++ ++ // queue next ++ // note: targetLevel > 0 here, since toPropagate >= currentLevel and currentLevel > 0 ++ // now combine into one bitset to pass to child ++ // the child bitset is 4x4, so we just shift each line by 4 ++ // add the propagation bitset offset to each line to make it easy to OR it into the propagation queue value ++ final long childPropagation = ++ ((bitsetLine1 >>> (start)) << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = -1 ++ ((bitsetLine2 >>> (start + 8)) << (4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = 0 ++ ((bitsetLine3 >>> (start + (8 + 8))) << (4 + 4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); // z = 1 ++ ++ // don't queue update if toPropagate cannot propagate anything to neighbours ++ // (for increase, propagating 0 to neighbours is useless) ++ if (queueLength >= queue.length) { ++ queue = this.resizeDecreaseQueue(); ++ } ++ queue[queueLength++] = ++ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | ++ ((toPropagate & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | ++ (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); //childPropagation; ++ continue; ++ } ++ } ++ ++ // propagate sources we clobbered ++ this.increaseQueueInitialLength = increaseQueueLength; ++ this.performIncrease(); ++ } ++ } ++ ++ private static final class Coordinate implements Comparable { ++ ++ public final long key; ++ ++ public Coordinate(final long key) { ++ this.key = key; ++ } ++ ++ public Coordinate(final int x, final int z) { ++ this.key = key(x, z); ++ } ++ ++ public static long key(final int x, final int z) { ++ return ((long)z << 32) | (x & 0xFFFFFFFFL); ++ } ++ ++ public static int x(final long key) { ++ return (int)key; ++ } ++ ++ public static int z(final long key) { ++ return (int)(key >>> 32); ++ } ++ ++ @Override ++ public int hashCode() { ++ return (int)HashCommon.mix(this.key); ++ } ++ ++ @Override ++ public boolean equals(final Object obj) { ++ if (this == obj) { ++ return true; ++ } ++ ++ if (!(obj instanceof Coordinate other)) { ++ return false; ++ } ++ ++ return this.key == other.key; ++ } ++ ++ // This class is intended for HashMap/ConcurrentHashMap usage, which do treeify bin nodes if the chain ++ // is too large. So we should implement compareTo to help. ++ @Override ++ public int compareTo(final Coordinate other) { ++ return Long.compare(this.key, other.key); ++ } ++ ++ @Override ++ public String toString() { ++ return "[" + x(this.key) + "," + z(this.key) + "]"; ++ } ++ } ++ ++ /* ++ private static final java.util.Random random = new java.util.Random(4L); ++ private static final List> walkers = ++ new java.util.ArrayList<>(); ++ static final int PLAYERS = 0; ++ static final int RAD_BLOCKS = 10000; ++ static final int RAD = RAD_BLOCKS >> 4; ++ static final int RAD_BIG_BLOCKS = 100_000; ++ static final int RAD_BIG = RAD_BIG_BLOCKS >> 4; ++ static final int VD = 4; ++ static final int BIG_PLAYERS = 50; ++ static final double WALK_CHANCE = 0.10; ++ static final double TP_CHANCE = 0.01; ++ static final int TP_BACK_PLAYERS = 200; ++ static final double TP_BACK_CHANCE = 0.25; ++ static final double TP_STEAL_CHANCE = 0.25; ++ private static final List> tpBack = ++ new java.util.ArrayList<>(); ++ ++ public static void main(final String[] args) { ++ final ReentrantAreaLock ticketLock = new ReentrantAreaLock(SECTION_SHIFT); ++ final ReentrantAreaLock schedulingLock = new ReentrantAreaLock(SECTION_SHIFT); ++ final Long2ByteLinkedOpenHashMap levelMap = new Long2ByteLinkedOpenHashMap(); ++ final Long2ByteLinkedOpenHashMap refMap = new Long2ByteLinkedOpenHashMap(); ++ final io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D ref = new io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D((final long coordinate, final byte oldLevel, final byte newLevel) -> { ++ if (newLevel == 0) { ++ refMap.remove(coordinate); ++ } else { ++ refMap.put(coordinate, newLevel); ++ } ++ }); ++ final ThreadedTicketLevelPropagator propagator = new ThreadedTicketLevelPropagator() { ++ @Override ++ protected void processLevelUpdates(Long2ByteLinkedOpenHashMap updates) { ++ for (final long key : updates.keySet()) { ++ final byte val = updates.get(key); ++ if (val == 0) { ++ levelMap.remove(key); ++ } else { ++ levelMap.put(key, val); ++ } ++ } ++ } ++ ++ @Override ++ protected void processSchedulingUpdates(Long2ByteLinkedOpenHashMap updates, List scheduledTasks, List changedFullStatus) {} ++ }; ++ ++ for (;;) { ++ if (walkers.isEmpty() && tpBack.isEmpty()) { ++ for (int i = 0; i < PLAYERS; ++i) { ++ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD; ++ int posX = random.nextInt(-rad, rad + 1); ++ int posZ = random.nextInt(-rad, rad + 1); ++ ++ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) { ++ @Override ++ protected void addCallback(Void parameter, int chunkX, int chunkZ) { ++ int src = 45 - 31 + 1; ++ ref.setSource(chunkX, chunkZ, src); ++ propagator.setSource(chunkX, chunkZ, src); ++ } ++ ++ @Override ++ protected void removeCallback(Void parameter, int chunkX, int chunkZ) { ++ ref.removeSource(chunkX, chunkZ); ++ propagator.removeSource(chunkX, chunkZ); ++ } ++ }; ++ ++ map.add(posX, posZ, VD); ++ ++ walkers.add(map); ++ } ++ for (int i = 0; i < TP_BACK_PLAYERS; ++i) { ++ int rad = RAD_BIG; ++ int posX = random.nextInt(-rad, rad + 1); ++ int posZ = random.nextInt(-rad, rad + 1); ++ ++ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) { ++ @Override ++ protected void addCallback(Void parameter, int chunkX, int chunkZ) { ++ int src = 45 - 31 + 1; ++ ref.setSource(chunkX, chunkZ, src); ++ propagator.setSource(chunkX, chunkZ, src); ++ } ++ ++ @Override ++ protected void removeCallback(Void parameter, int chunkX, int chunkZ) { ++ ref.removeSource(chunkX, chunkZ); ++ propagator.removeSource(chunkX, chunkZ); ++ } ++ }; ++ ++ map.add(posX, posZ, random.nextInt(1, 63)); ++ ++ tpBack.add(map); ++ } ++ } else { ++ for (int i = 0; i < PLAYERS; ++i) { ++ if (random.nextDouble() > WALK_CHANCE) { ++ continue; ++ } ++ ++ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = walkers.get(i); ++ ++ int updateX = random.nextInt(-1, 2); ++ int updateZ = random.nextInt(-1, 2); ++ ++ map.update(map.lastChunkX + updateX, map.lastChunkZ + updateZ, VD); ++ } ++ ++ for (int i = 0; i < PLAYERS; ++i) { ++ if (random.nextDouble() > TP_CHANCE) { ++ continue; ++ } ++ ++ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD; ++ int posX = random.nextInt(-rad, rad + 1); ++ int posZ = random.nextInt(-rad, rad + 1); ++ ++ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = walkers.get(i); ++ ++ map.update(posX, posZ, VD); ++ } ++ ++ for (int i = 0; i < TP_BACK_PLAYERS; ++i) { ++ if (random.nextDouble() > TP_BACK_CHANCE) { ++ continue; ++ } ++ ++ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = tpBack.get(i); ++ ++ map.update(-map.lastChunkX, -map.lastChunkZ, random.nextInt(1, 63)); ++ ++ if (random.nextDouble() > TP_STEAL_CHANCE) { ++ propagator.performUpdate( ++ map.lastChunkX >> SECTION_SHIFT, map.lastChunkZ >> SECTION_SHIFT, schedulingLock, null, null ++ ); ++ propagator.performUpdate( ++ (-map.lastChunkX >> SECTION_SHIFT), (-map.lastChunkZ >> SECTION_SHIFT), schedulingLock, null, null ++ ); ++ } ++ } ++ } ++ ++ ref.propagateUpdates(); ++ propagator.performUpdates(ticketLock, schedulingLock, null, null); ++ ++ if (!refMap.equals(levelMap)) { ++ throw new IllegalStateException("Error!"); ++ } ++ } ++ } ++ */ ++} +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/queue/RadiusAwarePrioritisedExecutor.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/queue/RadiusAwarePrioritisedExecutor.java +new file mode 100644 +index 0000000000000000000000000000000000000000..f7b0e2564ac4bd2db1d2b2bdc230c9f52f8a21b7 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/queue/RadiusAwarePrioritisedExecutor.java +@@ -0,0 +1,667 @@ ++package io.papermc.paper.chunk.system.scheduling.queue; ++ ++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; ++import io.papermc.paper.util.CoordinateUtils; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import java.util.ArrayList; ++import java.util.Comparator; ++import java.util.List; ++import java.util.PriorityQueue; ++ ++public class RadiusAwarePrioritisedExecutor { ++ ++ private static final Comparator DEPENDENCY_NODE_COMPARATOR = (final DependencyNode t1, final DependencyNode t2) -> { ++ return Long.compare(t1.id, t2.id); ++ }; ++ ++ private final DependencyTree[] queues = new DependencyTree[PrioritisedExecutor.Priority.TOTAL_SCHEDULABLE_PRIORITIES]; ++ private static final int NO_TASKS_QUEUED = -1; ++ private int selectedQueue = NO_TASKS_QUEUED; ++ private boolean canQueueTasks = true; ++ ++ public RadiusAwarePrioritisedExecutor(final PrioritisedExecutor executor, final int maxToSchedule) { ++ for (int i = 0; i < this.queues.length; ++i) { ++ this.queues[i] = new DependencyTree(this, executor, maxToSchedule, i); ++ } ++ } ++ ++ private boolean canQueueTasks() { ++ return this.canQueueTasks; ++ } ++ ++ private List treeFinished() { ++ this.canQueueTasks = true; ++ for (int priority = 0; priority < this.queues.length; ++priority) { ++ final DependencyTree queue = this.queues[priority]; ++ if (queue.hasWaitingTasks()) { ++ final List ret = queue.tryPushTasks(); ++ ++ if (ret == null || ret.isEmpty()) { ++ // this happens when the tasks in the wait queue were purged ++ // in this case, the queue was actually empty, we just had to purge it ++ // if we set the selected queue without scheduling any tasks, the queue will never be unselected ++ // as that requires a scheduled task completing... ++ continue; ++ } ++ ++ this.selectedQueue = priority; ++ return ret; ++ } ++ } ++ ++ this.selectedQueue = NO_TASKS_QUEUED; ++ ++ return null; ++ } ++ ++ private List queue(final Task task, final PrioritisedExecutor.Priority priority) { ++ final int priorityId = priority.priority; ++ final DependencyTree queue = this.queues[priorityId]; ++ ++ final DependencyNode node = new DependencyNode(task, queue); ++ ++ if (task.dependencyNode != null) { ++ throw new IllegalStateException(); ++ } ++ task.dependencyNode = node; ++ ++ queue.pushNode(node); ++ ++ if (this.selectedQueue == NO_TASKS_QUEUED) { ++ this.canQueueTasks = true; ++ this.selectedQueue = priorityId; ++ return queue.tryPushTasks(); ++ } ++ ++ if (!this.canQueueTasks) { ++ return null; ++ } ++ ++ if (PrioritisedExecutor.Priority.isHigherPriority(priorityId, this.selectedQueue)) { ++ // prevent the lower priority tree from queueing more tasks ++ this.canQueueTasks = false; ++ return null; ++ } ++ ++ // priorityId != selectedQueue: lower priority, don't care - treeFinished will pick it up ++ return priorityId == this.selectedQueue ? queue.tryPushTasks() : null; ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius, ++ final Runnable run, final PrioritisedExecutor.Priority priority) { ++ if (radius < 0) { ++ throw new IllegalArgumentException("Radius must be > 0: " + radius); ++ } ++ return new Task(this, chunkX, chunkZ, radius, run, priority); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius, ++ final Runnable run) { ++ return this.createTask(chunkX, chunkZ, radius, run, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueTask(final int chunkX, final int chunkZ, final int radius, ++ final Runnable run, final PrioritisedExecutor.Priority priority) { ++ final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run, priority); ++ ++ ret.queue(); ++ ++ return ret; ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueTask(final int chunkX, final int chunkZ, final int radius, ++ final Runnable run) { ++ final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run); ++ ++ ret.queue(); ++ ++ return ret; ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ return new Task(this, 0, 0, -1, run, priority); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run) { ++ return this.createInfiniteRadiusTask(run, PrioritisedExecutor.Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run, final PrioritisedExecutor.Priority priority) { ++ final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, priority); ++ ++ ret.queue(); ++ ++ return ret; ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run) { ++ final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, PrioritisedExecutor.Priority.NORMAL); ++ ++ ret.queue(); ++ ++ return ret; ++ } ++ ++ // all accesses must be synchronised by the radius aware object ++ private static final class DependencyTree { ++ ++ private final RadiusAwarePrioritisedExecutor scheduler; ++ private final PrioritisedExecutor executor; ++ private final int maxToSchedule; ++ private final int treeIndex; ++ ++ private int currentlyExecuting; ++ private long idGenerator; ++ ++ private final PriorityQueue awaiting = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR); ++ ++ private final PriorityQueue infiniteRadius = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR); ++ private boolean isInfiniteRadiusScheduled; ++ ++ private final Long2ReferenceOpenHashMap nodeByPosition = new Long2ReferenceOpenHashMap<>(); ++ ++ public DependencyTree(final RadiusAwarePrioritisedExecutor scheduler, final PrioritisedExecutor executor, ++ final int maxToSchedule, final int treeIndex) { ++ this.scheduler = scheduler; ++ this.executor = executor; ++ this.maxToSchedule = maxToSchedule; ++ this.treeIndex = treeIndex; ++ } ++ ++ public boolean hasWaitingTasks() { ++ return !this.awaiting.isEmpty() || !this.infiniteRadius.isEmpty(); ++ } ++ ++ private long nextId() { ++ return this.idGenerator++; ++ } ++ ++ private boolean isExecutingAnyTasks() { ++ return this.currentlyExecuting != 0; ++ } ++ ++ private void pushNode(final DependencyNode node) { ++ if (!node.task.isFiniteRadius()) { ++ this.infiniteRadius.add(node); ++ return; ++ } ++ ++ // set up dependency for node ++ final Task task = node.task; ++ ++ final int centerX = task.chunkX; ++ final int centerZ = task.chunkZ; ++ final int radius = task.radius; ++ ++ final int minX = centerX - radius; ++ final int maxX = centerX + radius; ++ ++ final int minZ = centerZ - radius; ++ final int maxZ = centerZ + radius; ++ ++ ReferenceOpenHashSet parents = null; ++ for (int currZ = minZ; currZ <= maxZ; ++currZ) { ++ for (int currX = minX; currX <= maxX; ++currX) { ++ final DependencyNode dependency = this.nodeByPosition.put(CoordinateUtils.getChunkKey(currX, currZ), node); ++ if (dependency != null) { ++ if (parents == null) { ++ parents = new ReferenceOpenHashSet<>(); ++ } ++ if (parents.add(dependency)) { ++ // added a dependency, so we need to add as a child to the dependency ++ if (dependency.children == null) { ++ dependency.children = new ArrayList<>(); ++ } ++ dependency.children.add(node); ++ } ++ } ++ } ++ } ++ ++ if (parents == null) { ++ // no dependencies, add straight to awaiting ++ this.awaiting.add(node); ++ } else { ++ node.parents = parents.size(); ++ // we will be added to awaiting once we have no parents ++ } ++ } ++ ++ // called only when a node is returned after being executed ++ private List returnNode(final DependencyNode node) { ++ final Task task = node.task; ++ ++ // now that the task is completed, we can push its children to the awaiting queue ++ this.pushChildren(node); ++ ++ if (task.isFiniteRadius()) { ++ // remove from dependency map ++ this.removeNodeFromMap(node); ++ } else { ++ // mark as no longer executing infinite radius ++ if (!this.isInfiniteRadiusScheduled) { ++ throw new IllegalStateException(); ++ } ++ this.isInfiniteRadiusScheduled = false; ++ } ++ ++ // decrement executing count, we are done executing this task ++ --this.currentlyExecuting; ++ ++ if (this.currentlyExecuting == 0) { ++ return this.scheduler.treeFinished(); ++ } ++ ++ return this.scheduler.canQueueTasks() ? this.tryPushTasks() : null; ++ } ++ ++ private List tryPushTasks() { ++ // tasks are not queued, but only created here - we do hold the lock for the map ++ List ret = null; ++ PrioritisedExecutor.PrioritisedTask pushedTask; ++ while ((pushedTask = this.tryPushTask()) != null) { ++ if (ret == null) { ++ ret = new ArrayList<>(); ++ } ++ ret.add(pushedTask); ++ } ++ ++ return ret; ++ } ++ ++ private void removeNodeFromMap(final DependencyNode node) { ++ final Task task = node.task; ++ ++ final int centerX = task.chunkX; ++ final int centerZ = task.chunkZ; ++ final int radius = task.radius; ++ ++ final int minX = centerX - radius; ++ final int maxX = centerX + radius; ++ ++ final int minZ = centerZ - radius; ++ final int maxZ = centerZ + radius; ++ ++ for (int currZ = minZ; currZ <= maxZ; ++currZ) { ++ for (int currX = minX; currX <= maxX; ++currX) { ++ this.nodeByPosition.remove(CoordinateUtils.getChunkKey(currX, currZ), node); ++ } ++ } ++ } ++ ++ private void pushChildren(final DependencyNode node) { ++ // add all the children that we can into awaiting ++ final List children = node.children; ++ if (children != null) { ++ for (int i = 0, len = children.size(); i < len; ++i) { ++ final DependencyNode child = children.get(i); ++ int newParents = --child.parents; ++ if (newParents == 0) { ++ // no more dependents, we can push to awaiting ++ // even if the child is purged, we need to push it so that its children will be pushed ++ this.awaiting.add(child); ++ } else if (newParents < 0) { ++ throw new IllegalStateException(); ++ } ++ } ++ } ++ } ++ ++ private DependencyNode pollAwaiting() { ++ final DependencyNode ret = this.awaiting.poll(); ++ if (ret == null) { ++ return ret; ++ } ++ ++ if (ret.parents != 0) { ++ throw new IllegalStateException(); ++ } ++ ++ if (ret.purged) { ++ // need to manually remove from state here ++ this.pushChildren(ret); ++ this.removeNodeFromMap(ret); ++ } // else: delay children push until the task has finished ++ ++ return ret; ++ } ++ ++ private DependencyNode pollInfinite() { ++ return this.infiniteRadius.poll(); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask tryPushTask() { ++ if (this.currentlyExecuting >= this.maxToSchedule || this.isInfiniteRadiusScheduled) { ++ return null; ++ } ++ ++ DependencyNode firstInfinite; ++ while ((firstInfinite = this.infiniteRadius.peek()) != null && firstInfinite.purged) { ++ this.pollInfinite(); ++ } ++ ++ DependencyNode firstAwaiting; ++ while ((firstAwaiting = this.awaiting.peek()) != null && firstAwaiting.purged) { ++ this.pollAwaiting(); ++ } ++ ++ if (firstInfinite == null && firstAwaiting == null) { ++ return null; ++ } ++ ++ // firstAwaiting compared to firstInfinite ++ final int compare; ++ ++ if (firstAwaiting == null) { ++ // we choose first infinite, or infinite < awaiting ++ compare = 1; ++ } else if (firstInfinite == null) { ++ // we choose first awaiting, or awaiting < infinite ++ compare = -1; ++ } else { ++ compare = DEPENDENCY_NODE_COMPARATOR.compare(firstAwaiting, firstInfinite); ++ } ++ ++ if (compare >= 0) { ++ if (this.currentlyExecuting != 0) { ++ // don't queue infinite task while other tasks are executing in parallel ++ return null; ++ } ++ ++this.currentlyExecuting; ++ this.pollInfinite(); ++ this.isInfiniteRadiusScheduled = true; ++ return firstInfinite.task.pushTask(this.executor); ++ } else { ++ ++this.currentlyExecuting; ++ this.pollAwaiting(); ++ return firstAwaiting.task.pushTask(this.executor); ++ } ++ } ++ } ++ ++ private static final class DependencyNode { ++ ++ private final Task task; ++ private final DependencyTree tree; ++ ++ // dependency tree fields ++ // (must hold lock on the scheduler to use) ++ // null is the same as empty, we just use it so that we don't allocate the set unless we need to ++ private List children; ++ // 0 indicates that this task is considered "awaiting" ++ private int parents; ++ // false -> scheduled and not cancelled ++ // true -> scheduled but cancelled ++ private boolean purged; ++ private final long id; ++ ++ public DependencyNode(final Task task, final DependencyTree tree) { ++ this.task = task; ++ this.id = tree.nextId(); ++ this.tree = tree; ++ } ++ } ++ ++ private static final class Task implements PrioritisedExecutor.PrioritisedTask, Runnable { ++ ++ // task specific fields ++ private final RadiusAwarePrioritisedExecutor scheduler; ++ private final int chunkX; ++ private final int chunkZ; ++ private final int radius; ++ private Runnable run; ++ private PrioritisedExecutor.Priority priority; ++ ++ private DependencyNode dependencyNode; ++ private PrioritisedExecutor.PrioritisedTask queuedTask; ++ ++ private Task(final RadiusAwarePrioritisedExecutor scheduler, final int chunkX, final int chunkZ, final int radius, ++ final Runnable run, final PrioritisedExecutor.Priority priority) { ++ this.scheduler = scheduler; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.radius = radius; ++ this.run = run; ++ this.priority = priority; ++ } ++ ++ private boolean isFiniteRadius() { ++ return this.radius >= 0; ++ } ++ ++ private PrioritisedExecutor.PrioritisedTask pushTask(final PrioritisedExecutor executor) { ++ return this.queuedTask = executor.createTask(this, this.priority); ++ } ++ ++ private void executeTask() { ++ final Runnable run = this.run; ++ this.run = null; ++ run.run(); ++ } ++ ++ private static void scheduleTasks(final List toSchedule) { ++ if (toSchedule != null) { ++ for (int i = 0, len = toSchedule.size(); i < len; ++i) { ++ toSchedule.get(i).queue(); ++ } ++ } ++ } ++ ++ private void returnNode() { ++ final List toSchedule; ++ synchronized (this.scheduler) { ++ final DependencyNode node = this.dependencyNode; ++ this.dependencyNode = null; ++ toSchedule = node.tree.returnNode(node); ++ } ++ ++ scheduleTasks(toSchedule); ++ } ++ ++ @Override ++ public void run() { ++ final Runnable run = this.run; ++ this.run = null; ++ try { ++ run.run(); ++ } finally { ++ this.returnNode(); ++ } ++ } ++ ++ @Override ++ public boolean queue() { ++ final List toSchedule; ++ synchronized (this.scheduler) { ++ if (this.queuedTask != null || this.dependencyNode != null || this.priority == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ toSchedule = this.scheduler.queue(this, this.priority); ++ } ++ ++ scheduleTasks(toSchedule); ++ return true; ++ } ++ ++ @Override ++ public boolean cancel() { ++ final PrioritisedExecutor.PrioritisedTask task; ++ synchronized (this.scheduler) { ++ if ((task = this.queuedTask) == null) { ++ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ this.priority = PrioritisedExecutor.Priority.COMPLETING; ++ if (this.dependencyNode != null) { ++ this.dependencyNode.purged = true; ++ this.dependencyNode = null; ++ } ++ ++ return true; ++ } ++ } ++ ++ if (task.cancel()) { ++ // must manually return the node ++ this.run = null; ++ this.returnNode(); ++ return true; ++ } ++ return false; ++ } ++ ++ @Override ++ public boolean execute() { ++ final PrioritisedExecutor.PrioritisedTask task; ++ synchronized (this.scheduler) { ++ if ((task = this.queuedTask) == null) { ++ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ this.priority = PrioritisedExecutor.Priority.COMPLETING; ++ if (this.dependencyNode != null) { ++ this.dependencyNode.purged = true; ++ this.dependencyNode = null; ++ } ++ // fall through to execution logic ++ } ++ } ++ ++ if (task != null) { ++ // will run the return node logic automatically ++ return task.execute(); ++ } else { ++ // don't run node removal/insertion logic, we aren't actually removed from the dependency tree ++ this.executeTask(); ++ return true; ++ } ++ } ++ ++ @Override ++ public PrioritisedExecutor.Priority getPriority() { ++ final PrioritisedExecutor.PrioritisedTask task; ++ synchronized (this.scheduler) { ++ if ((task = this.queuedTask) == null) { ++ return this.priority; ++ } ++ } ++ ++ return task.getPriority(); ++ } ++ ++ @Override ++ public boolean setPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ final PrioritisedExecutor.PrioritisedTask task; ++ List toSchedule = null; ++ synchronized (this.scheduler) { ++ if ((task = this.queuedTask) == null) { ++ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ if (this.priority == priority) { ++ return true; ++ } ++ ++ this.priority = priority; ++ if (this.dependencyNode != null) { ++ // need to re-insert node ++ this.dependencyNode.purged = true; ++ this.dependencyNode = null; ++ toSchedule = this.scheduler.queue(this, priority); ++ } ++ } ++ } ++ ++ if (task != null) { ++ return task.setPriority(priority); ++ } ++ ++ scheduleTasks(toSchedule); ++ ++ return true; ++ } ++ ++ @Override ++ public boolean raisePriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ final PrioritisedExecutor.PrioritisedTask task; ++ List toSchedule = null; ++ synchronized (this.scheduler) { ++ if ((task = this.queuedTask) == null) { ++ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ if (this.priority.isHigherOrEqualPriority(priority)) { ++ return true; ++ } ++ ++ this.priority = priority; ++ if (this.dependencyNode != null) { ++ // need to re-insert node ++ this.dependencyNode.purged = true; ++ this.dependencyNode = null; ++ toSchedule = this.scheduler.queue(this, priority); ++ } ++ } ++ } ++ ++ if (task != null) { ++ return task.raisePriority(priority); ++ } ++ ++ scheduleTasks(toSchedule); ++ ++ return true; ++ } ++ ++ @Override ++ public boolean lowerPriority(final PrioritisedExecutor.Priority priority) { ++ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { ++ throw new IllegalArgumentException("Invalid priority " + priority); ++ } ++ ++ final PrioritisedExecutor.PrioritisedTask task; ++ List toSchedule = null; ++ synchronized (this.scheduler) { ++ if ((task = this.queuedTask) == null) { ++ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { ++ return false; ++ } ++ ++ if (this.priority.isLowerOrEqualPriority(priority)) { ++ return true; ++ } ++ ++ this.priority = priority; ++ if (this.dependencyNode != null) { ++ // need to re-insert node ++ this.dependencyNode.purged = true; ++ this.dependencyNode = null; ++ toSchedule = this.scheduler.queue(this, priority); ++ } ++ } ++ } ++ ++ if (task != null) { ++ return task.lowerPriority(priority); ++ } ++ ++ scheduleTasks(toSchedule); ++ ++ return true; ++ } ++ } ++} +diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java +index e47fb2aa5e885162cae5cbfc9f33ff7864bf538e..b68b37274f22c2a89d723aec4d1c6be813eef73c 100644 +--- a/src/main/java/io/papermc/paper/command/PaperCommand.java ++++ b/src/main/java/io/papermc/paper/command/PaperCommand.java +@@ -43,6 +43,7 @@ public final class PaperCommand extends Command { + commands.put(Set.of("mobcaps", "playermobcaps"), new MobcapsCommand()); + commands.put(Set.of("dumplisteners"), new DumpListenersCommand()); + commands.put(Set.of("fixlight"), new FixLightCommand()); ++ commands.put(Set.of("debug", "chunkinfo", "holderinfo"), new ChunkDebugCommand()); + + return commands.entrySet().stream() + .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue()))) +diff --git a/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java b/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java +new file mode 100644 +index 0000000000000000000000000000000000000000..962d3cae6340fc11607b59355e291629618f289c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java +@@ -0,0 +1,265 @@ ++package io.papermc.paper.command.subcommands; ++ ++import io.papermc.paper.command.CommandUtil; ++import io.papermc.paper.command.PaperSubcommand; ++import java.io.File; ++import java.time.LocalDateTime; ++import java.time.format.DateTimeFormatter; ++import java.util.ArrayList; ++import java.util.Collections; ++import java.util.List; ++import java.util.Locale; ++import io.papermc.paper.util.MCUtil; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.FullChunkStatus; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.ImposterProtoChunk; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.ProtoChunk; ++import org.bukkit.Bukkit; ++import org.bukkit.command.CommandSender; ++import org.bukkit.craftbukkit.CraftWorld; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.checker.nullness.qual.Nullable; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++import static net.kyori.adventure.text.Component.text; ++import static net.kyori.adventure.text.format.NamedTextColor.BLUE; ++import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA; ++import static net.kyori.adventure.text.format.NamedTextColor.GREEN; ++import static net.kyori.adventure.text.format.NamedTextColor.RED; ++ ++@DefaultQualifier(NonNull.class) ++public final class ChunkDebugCommand implements PaperSubcommand { ++ @Override ++ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { ++ switch (subCommand) { ++ case "debug" -> this.doDebug(sender, args); ++ case "chunkinfo" -> this.doChunkInfo(sender, args); ++ case "holderinfo" -> this.doHolderInfo(sender, args); ++ } ++ return true; ++ } ++ ++ @Override ++ public List tabComplete(final CommandSender sender, final String subCommand, final String[] args) { ++ switch (subCommand) { ++ case "debug" -> { ++ if (args.length == 1) { ++ return CommandUtil.getListMatchingLast(sender, args, "help", "chunks"); ++ } ++ } ++ case "holderinfo" -> { ++ List worldNames = new ArrayList<>(); ++ worldNames.add("*"); ++ for (org.bukkit.World world : Bukkit.getWorlds()) { ++ worldNames.add(world.getName()); ++ } ++ if (args.length == 1) { ++ return CommandUtil.getListMatchingLast(sender, args, worldNames); ++ } ++ } ++ case "chunkinfo" -> { ++ List worldNames = new ArrayList<>(); ++ worldNames.add("*"); ++ for (org.bukkit.World world : Bukkit.getWorlds()) { ++ worldNames.add(world.getName()); ++ } ++ if (args.length == 1) { ++ return CommandUtil.getListMatchingLast(sender, args, worldNames); ++ } ++ } ++ } ++ return Collections.emptyList(); ++ } ++ ++ private void doChunkInfo(final CommandSender sender, final String[] args) { ++ List worlds; ++ if (args.length < 1 || args[0].equals("*")) { ++ worlds = Bukkit.getWorlds(); ++ } else { ++ worlds = new ArrayList<>(args.length); ++ for (final String arg : args) { ++ org.bukkit.@Nullable World world = Bukkit.getWorld(arg); ++ if (world == null) { ++ sender.sendMessage(text("World '" + arg + "' is invalid", RED)); ++ return; ++ } ++ worlds.add(world); ++ } ++ } ++ ++ int accumulatedTotal = 0; ++ int accumulatedInactive = 0; ++ int accumulatedBorder = 0; ++ int accumulatedTicking = 0; ++ int accumulatedEntityTicking = 0; ++ ++ for (final org.bukkit.World bukkitWorld : worlds) { ++ final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle(); ++ ++ int total = 0; ++ int inactive = 0; ++ int full = 0; ++ int blockTicking = 0; ++ int entityTicking = 0; ++ ++ for (final ChunkHolder chunk : io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(world)) { ++ if (chunk.getFullChunkNowUnchecked() == null) { ++ continue; ++ } ++ ++ ++total; ++ ++ FullChunkStatus state = chunk.getFullStatus(); ++ ++ switch (state) { ++ case INACCESSIBLE -> ++inactive; ++ case FULL -> ++full; ++ case BLOCK_TICKING -> ++blockTicking; ++ case ENTITY_TICKING -> ++entityTicking; ++ } ++ } ++ ++ accumulatedTotal += total; ++ accumulatedInactive += inactive; ++ accumulatedBorder += full; ++ accumulatedTicking += blockTicking; ++ accumulatedEntityTicking += entityTicking; ++ ++ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":"))); ++ sender.sendMessage(text().color(DARK_AQUA).append( ++ text("Total: ", BLUE), text(total), ++ text(" Inactive: ", BLUE), text(inactive), ++ text(" Full: ", BLUE), text(full), ++ text(" Block Ticking: ", BLUE), text(blockTicking), ++ text(" Entity Ticking: ", BLUE), text(entityTicking) ++ )); ++ } ++ if (worlds.size() > 1) { ++ sender.sendMessage(text().append(text("Chunks in ", BLUE), text("all listed worlds", GREEN), text(":", DARK_AQUA))); ++ sender.sendMessage(text().color(DARK_AQUA).append( ++ text("Total: ", BLUE), text(accumulatedTotal), ++ text(" Inactive: ", BLUE), text(accumulatedInactive), ++ text(" Full: ", BLUE), text(accumulatedBorder), ++ text(" Block Ticking: ", BLUE), text(accumulatedTicking), ++ text(" Entity Ticking: ", BLUE), text(accumulatedEntityTicking) ++ )); ++ } ++ } ++ ++ private void doHolderInfo(final CommandSender sender, final String[] args) { ++ List worlds; ++ if (args.length < 1 || args[0].equals("*")) { ++ worlds = Bukkit.getWorlds(); ++ } else { ++ worlds = new ArrayList<>(args.length); ++ for (final String arg : args) { ++ org.bukkit.@Nullable World world = Bukkit.getWorld(arg); ++ if (world == null) { ++ sender.sendMessage(text("World '" + arg + "' is invalid", RED)); ++ return; ++ } ++ worlds.add(world); ++ } ++ } ++ ++ int accumulatedTotal = 0; ++ int accumulatedCanUnload = 0; ++ int accumulatedNull = 0; ++ int accumulatedReadOnly = 0; ++ int accumulatedProtoChunk = 0; ++ int accumulatedFullChunk = 0; ++ ++ for (final org.bukkit.World bukkitWorld : worlds) { ++ final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle(); ++ ++ int total = 0; ++ int canUnload = 0; ++ int nullChunks = 0; ++ int readOnly = 0; ++ int protoChunk = 0; ++ int fullChunk = 0; ++ ++ for (final ChunkHolder chunk : world.chunkTaskScheduler.chunkHolderManager.getOldChunkHolders()) { // Paper - change updating chunks map ++ final ChunkAccess lastChunk = chunk.getAvailableChunkNow(); ++ ++ ++total; ++ ++ if (lastChunk == null) { ++ ++nullChunks; ++ } else if (lastChunk instanceof ImposterProtoChunk) { ++ ++readOnly; ++ } else if (lastChunk instanceof ProtoChunk) { ++ ++protoChunk; ++ } else if (lastChunk instanceof LevelChunk) { ++ ++fullChunk; ++ } ++ ++ if (chunk.newChunkHolder.isSafeToUnload() == null) { ++ ++canUnload; ++ } ++ } ++ ++ accumulatedTotal += total; ++ accumulatedCanUnload += canUnload; ++ accumulatedNull += nullChunks; ++ accumulatedReadOnly += readOnly; ++ accumulatedProtoChunk += protoChunk; ++ accumulatedFullChunk += fullChunk; ++ ++ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":"))); ++ sender.sendMessage(text().color(DARK_AQUA).append( ++ text("Total: ", BLUE), text(total), ++ text(" Unloadable: ", BLUE), text(canUnload), ++ text(" Null: ", BLUE), text(nullChunks), ++ text(" ReadOnly: ", BLUE), text(readOnly), ++ text(" Proto: ", BLUE), text(protoChunk), ++ text(" Full: ", BLUE), text(fullChunk) ++ )); ++ } ++ if (worlds.size() > 1) { ++ sender.sendMessage(text().append(text("Chunks in ", BLUE), text("all listed worlds", GREEN), text(":", DARK_AQUA))); ++ sender.sendMessage(text().color(DARK_AQUA).append( ++ text("Total: ", BLUE), text(accumulatedTotal), ++ text(" Unloadable: ", BLUE), text(accumulatedCanUnload), ++ text(" Null: ", BLUE), text(accumulatedNull), ++ text(" ReadOnly: ", BLUE), text(accumulatedReadOnly), ++ text(" Proto: ", BLUE), text(accumulatedProtoChunk), ++ text(" Full: ", BLUE), text(accumulatedFullChunk) ++ )); ++ } ++ } ++ ++ private void doDebug(final CommandSender sender, final String[] args) { ++ if (args.length < 1) { ++ sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED)); ++ return; ++ } ++ ++ final String debugType = args[0].toLowerCase(Locale.ENGLISH); ++ switch (debugType) { ++ case "chunks" -> { ++ if (args.length >= 2 && args[1].toLowerCase(Locale.ENGLISH).equals("help")) { ++ sender.sendMessage(text("Use /paper debug chunks [world] to dump loaded chunk information to a file", RED)); ++ break; ++ } ++ File file = new File(new File(new File("."), "debug"), ++ "chunks-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + ".txt"); ++ sender.sendMessage(text("Writing chunk information dump to " + file, GREEN)); ++ try { ++ MCUtil.dumpChunks(file, false); ++ sender.sendMessage(text("Successfully written chunk information!", GREEN)); ++ } catch (Throwable thr) { ++ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr); ++ sender.sendMessage(text("Failed to dump chunk information, see console", RED)); ++ } ++ } ++ // "help" & default ++ default -> sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED)); ++ } ++ } ++ ++} +diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +index 481cfe3a291196c0391c2ec8560e566e0e1e2fb8..30fe1c0645a07d663b08c0f988a1ab3a750bf7c4 100644 +--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java ++++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +@@ -29,6 +29,45 @@ public class GlobalConfiguration extends ConfigurationPart { + public static GlobalConfiguration get() { + return instance; + } ++ ++ public ChunkLoadingBasic chunkLoadingBasic; ++ ++ public class ChunkLoadingBasic extends ConfigurationPart { ++ @Comment("The maximum rate in chunks per second that the server will send to any individual player. Set to -1 to disable this limit.") ++ public double playerMaxChunkSendRate = 75.0; ++ ++ @Comment( ++ "The maximum rate at which chunks will load for any individual player. " + ++ "Note that this setting also affects chunk generations, since a chunk load is always first issued to test if a" + ++ "chunk is already generated. Set to -1 to disable this limit." ++ ) ++ public double playerMaxChunkLoadRate = 100.0; ++ ++ @Comment("The maximum rate at which chunks will generate for any individual player. Set to -1 to disable this limit.") ++ public double playerMaxChunkGenerateRate = -1.0; ++ } ++ ++ public ChunkLoadingAdvanced chunkLoadingAdvanced; ++ ++ public class ChunkLoadingAdvanced extends ConfigurationPart { ++ @Comment( ++ "Set to true if the server will match the chunk send radius that clients have configured" + ++ "in their view distance settings if the client is less-than the server's send distance." ++ ) ++ public boolean autoConfigSendDistance = true; ++ ++ @Comment( ++ "Specifies the maximum amount of concurrent chunk loads that an individual player can have." + ++ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit." ++ ) ++ public int playerMaxConcurrentChunkLoads = 0; ++ ++ @Comment( ++ "Specifies the maximum amount of concurrent chunk generations that an individual player can have." + ++ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit." ++ ) ++ public int playerMaxConcurrentChunkGenerates = 0; ++ } + static void set(GlobalConfiguration instance) { + GlobalConfiguration.instance = instance; + } +@@ -130,21 +169,6 @@ public class GlobalConfiguration extends ConfigurationPart { + public int incomingPacketThreshold = 300; + } + +- public ChunkLoading chunkLoading; +- +- public class ChunkLoading extends ConfigurationPart { +- public int minLoadRadius = 2; +- public int maxConcurrentSends = 2; +- public boolean autoconfigSendDistance = true; +- public double targetPlayerChunkSendRate = 100.0; +- public double globalMaxChunkSendRate = -1.0; +- public boolean enableFrustumPriority = false; +- public double globalMaxChunkLoadRate = -1.0; +- public double playerMaxConcurrentLoads = 20.0; +- public double globalMaxConcurrentLoads = 500.0; +- public double playerMaxChunkLoadRate = -1.0; +- } +- + public UnsupportedSettings unsupportedSettings; + + public class UnsupportedSettings extends ConfigurationPart { +@@ -203,7 +227,7 @@ public class GlobalConfiguration extends ConfigurationPart { + + @PostProcess + private void postProcess() { +- //io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.init(this); ++ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.init(this); + } + } + +diff --git a/src/main/java/io/papermc/paper/threadedregions/TickRegions.java b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d5d39e9c1f326e91010237b0db80d527ac52f4d6 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java +@@ -0,0 +1,9 @@ ++package io.papermc.paper.threadedregions; ++ ++// placeholder class for Folia ++public class TickRegions { ++ ++ public static int getRegionChunkShift() { ++ return 4; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/IntervalledCounter.java b/src/main/java/io/papermc/paper/util/IntervalledCounter.java +index cea9c098ade00ee87b8efc8164ab72f5279758f0..197224e31175252d8438a8df585bbb65f2288d7f 100644 +--- a/src/main/java/io/papermc/paper/util/IntervalledCounter.java ++++ b/src/main/java/io/papermc/paper/util/IntervalledCounter.java +@@ -2,6 +2,8 @@ package io.papermc.paper.util; + + public final class IntervalledCounter { + ++ private static final int INITIAL_SIZE = 8; ++ + protected long[] times; + protected long[] counts; + protected final long interval; +@@ -11,8 +13,8 @@ public final class IntervalledCounter { + protected int tail; // exclusive + + public IntervalledCounter(final long interval) { +- this.times = new long[8]; +- this.counts = new long[8]; ++ this.times = new long[INITIAL_SIZE]; ++ this.counts = new long[INITIAL_SIZE]; + this.interval = interval; + } + +@@ -67,13 +69,13 @@ public final class IntervalledCounter { + this.tail = nextTail; + } + +- public void updateAndAdd(final int count) { ++ public void updateAndAdd(final long count) { + final long currTime = System.nanoTime(); + this.updateCurrentTime(currTime); + this.addTime(currTime, count); + } + +- public void updateAndAdd(final int count, final long currTime) { ++ public void updateAndAdd(final long count, final long currTime) { + this.updateCurrentTime(currTime); + this.addTime(currTime, count); + } +@@ -93,9 +95,13 @@ public final class IntervalledCounter { + this.tail = size; + + if (tail >= head) { ++ // sequentially ordered from [head, tail) + System.arraycopy(oldElements, head, newElements, 0, size); + System.arraycopy(oldCounts, head, newCounts, 0, size); + } else { ++ // ordered from [head, length) ++ // then followed by [0, tail) ++ + System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head); + System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail); + +@@ -106,10 +112,18 @@ public final class IntervalledCounter { + + // returns in units per second + public double getRate() { +- return this.size() / (this.interval * 1.0e-9); ++ return (double)this.sum / ((double)this.interval * 1.0E-9); ++ } ++ ++ public long getInterval() { ++ return this.interval; + } + +- public long size() { ++ public long getSum() { + return this.sum; + } ++ ++ public int totalDataPoints() { ++ return this.tail >= this.head ? (this.tail - this.head) : (this.tail + (this.counts.length - this.head)); ++ } + } +diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java +index 5c401c7e30ed5f6f32b1ad6523537aa46173cff5..e5c2e53388ff61e4aeae22a74b525d9a26ea0200 100644 +--- a/src/main/java/io/papermc/paper/util/MCUtil.java ++++ b/src/main/java/io/papermc/paper/util/MCUtil.java +@@ -7,17 +7,30 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; + import io.papermc.paper.math.BlockPosition; + import io.papermc.paper.math.FinePosition; + import io.papermc.paper.math.Position; ++import com.google.gson.JsonArray; ++import com.google.gson.JsonObject; ++import com.google.gson.internal.Streams; ++import com.google.gson.stream.JsonWriter; ++import com.mojang.datafixers.util.Either; + import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet; + import java.lang.ref.Cleaner; ++import it.unimi.dsi.fastutil.objects.ReferenceArrayList; + import net.minecraft.core.BlockPos; + import net.minecraft.core.Direction; + import net.minecraft.core.Vec3i; + import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.ChunkMap; ++import net.minecraft.server.level.DistanceManager; + import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.server.level.Ticket; + import net.minecraft.world.entity.Entity; + import net.minecraft.world.level.ChunkPos; + import net.minecraft.world.level.ClipContext; + import net.minecraft.world.level.Level; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; + import net.minecraft.world.phys.Vec3; + import org.apache.commons.lang.exception.ExceptionUtils; + import com.mojang.authlib.GameProfile; +@@ -30,8 +43,11 @@ import org.spigotmc.AsyncCatcher; + + import javax.annotation.Nonnull; + import javax.annotation.Nullable; ++import java.io.*; ++import java.nio.charset.StandardCharsets; + import java.util.List; + import java.util.Queue; ++import java.util.Set; + import java.util.concurrent.CompletableFuture; + import java.util.concurrent.ExecutionException; + import java.util.concurrent.LinkedBlockingQueue; +@@ -532,6 +548,98 @@ public final class MCUtil { + } + } + ++ public static ChunkStatus getChunkStatus(ChunkHolder chunk) { ++ return chunk.getChunkHolderStatus(); ++ } ++ ++ public static void dumpChunks(File file, boolean watchdog) throws IOException { ++ file.getParentFile().mkdirs(); ++ file.createNewFile(); ++ ReferenceArrayList worlds = new ReferenceArrayList<>(org.bukkit.Bukkit.getWorlds()); ++ ReferenceArrayList loadedWorlds = new ReferenceArrayList<>(worlds); ++ JsonObject data = new JsonObject(); ++ ++ data.addProperty("server-version", org.bukkit.Bukkit.getVersion()); ++ data.addProperty("data-version", 1); ++ ++ { ++ JsonArray players = new JsonArray(); ++ data.add("all-players", players); ++ List playerList = MinecraftServer.getServer().getPlayerList().players; ++ for (ServerPlayer player : playerList) { ++ JsonObject playerData = new JsonObject(); ++ players.add(playerData); ++ ++ Level playerWorld = player.level(); ++ org.bukkit.World craftWorld = playerWorld.getWorld(); ++ Entity.RemovalReason removalReason = player.getRemovalReason(); ++ ++ playerData.addProperty("name", player.getScoreboardName()); ++ playerData.addProperty("x", player.getX()); ++ playerData.addProperty("y", player.getY()); ++ playerData.addProperty("z", player.getZ()); ++ playerData.addProperty("world", playerWorld == null ? "null world" : craftWorld.getName()); ++ playerData.addProperty("removalReason", removalReason == null ? "null" : removalReason.name()); ++ ++ if (!worlds.contains(craftWorld)) { ++ worlds.add(craftWorld); ++ } ++ } ++ } ++ ++ JsonArray chunkWaitInformation = new JsonArray(); ++ data.add("chunk-wait-infos", chunkWaitInformation); ++ ++ for (io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.ChunkInfo chunkInfo : io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.getChunkInfos()) { ++ chunkWaitInformation.add(chunkInfo.toString()); ++ } ++ ++ JsonArray worldsData = new JsonArray(); ++ ++ for (org.bukkit.World bukkitWorld : worlds) { ++ JsonObject worldData = new JsonObject(); ++ ++ ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)bukkitWorld).getHandle(); ++ List players = world.players(); ++ ++ worldData.addProperty("is-loaded", loadedWorlds.contains(bukkitWorld)); ++ worldData.addProperty("name", world.getWorld().getName()); ++ worldData.addProperty("view-distance", world.getWorld().getViewDistance()); // Paper - replace chunk loader system ++ worldData.addProperty("tick-view-distance", world.getWorld().getSimulationDistance()); // Paper - replace chunk loader system ++ ++ JsonArray playersData = new JsonArray(); ++ ++ for (ServerPlayer player : players) { ++ JsonObject playerData = new JsonObject(); ++ ++ playerData.addProperty("name", player.getScoreboardName()); ++ playerData.addProperty("x", player.getX()); ++ playerData.addProperty("y", player.getY()); ++ playerData.addProperty("z", player.getZ()); ++ ++ playersData.add(playerData); ++ } ++ ++ worldData.add("players", playersData); ++ worldData.add("chunk-data", watchdog ? world.chunkTaskScheduler.chunkHolderManager.getDebugJsonForWatchdog() : world.chunkTaskScheduler.chunkHolderManager.getDebugJson()); ++ worldsData.add(worldData); ++ } ++ ++ data.add("worlds", worldsData); ++ ++ StringWriter stringWriter = new StringWriter(); ++ JsonWriter jsonWriter = new JsonWriter(stringWriter); ++ jsonWriter.setIndent(" "); ++ jsonWriter.setLenient(false); ++ Streams.write(data, jsonWriter); ++ ++ String fileData = stringWriter.toString(); ++ ++ try (PrintStream out = new PrintStream(new FileOutputStream(file), false, StandardCharsets.UTF_8)) { ++ out.print(fileData); ++ } ++ } ++ + public static int getTicketLevelFor(net.minecraft.world.level.chunk.status.ChunkStatus status) { + return net.minecraft.server.level.ChunkMap.MAX_VIEW_DISTANCE + net.minecraft.world.level.chunk.status.ChunkStatus.getDistance(status); + } +diff --git a/src/main/java/io/papermc/paper/util/TickThread.java b/src/main/java/io/papermc/paper/util/TickThread.java +index 73e83d56a340f0c7dcb8ff737d621003e72c6de4..bdaf062f9b66ceab303a0807eca301342886a8ea 100644 +--- a/src/main/java/io/papermc/paper/util/TickThread.java ++++ b/src/main/java/io/papermc/paper/util/TickThread.java +@@ -1,12 +1,20 @@ + package io.papermc.paper.util; + ++import net.minecraft.core.BlockPos; + import net.minecraft.server.MinecraftServer; + import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.server.network.ServerGamePacketListenerImpl; ++import net.minecraft.util.Mth; + import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.phys.AABB; ++import net.minecraft.world.phys.Vec3; + import org.bukkit.Bukkit; + import java.util.concurrent.atomic.AtomicInteger; + +-public final class TickThread extends Thread { ++public class TickThread extends Thread { + + public static final boolean STRICT_THREAD_CHECKS = Boolean.getBoolean("paper.strict-thread-checks"); + +@@ -16,6 +24,10 @@ public final class TickThread extends Thread { + } + } + ++ /** ++ * @deprecated ++ */ ++ @Deprecated + public static void softEnsureTickThread(final String reason) { + if (!STRICT_THREAD_CHECKS) { + return; +@@ -23,6 +35,10 @@ public final class TickThread extends Thread { + ensureTickThread(reason); + } + ++ /** ++ * @deprecated ++ */ ++ @Deprecated + public static void ensureTickThread(final String reason) { + if (!isTickThread()) { + MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); +@@ -30,6 +46,20 @@ public final class TickThread extends Thread { + } + } + ++ public static void ensureTickThread(final ServerLevel world, final BlockPos pos, final String reason) { ++ if (!isTickThreadFor(world, pos)) { ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); ++ throw new IllegalStateException(reason); ++ } ++ } ++ ++ public static void ensureTickThread(final ServerLevel world, final ChunkPos pos, final String reason) { ++ if (!isTickThreadFor(world, pos)) { ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); ++ throw new IllegalStateException(reason); ++ } ++ } ++ + public static void ensureTickThread(final ServerLevel world, final int chunkX, final int chunkZ, final String reason) { + if (!isTickThreadFor(world, chunkX, chunkZ)) { + MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); +@@ -44,6 +74,20 @@ public final class TickThread extends Thread { + } + } + ++ public static void ensureTickThread(final ServerLevel world, final AABB aabb, final String reason) { ++ if (!isTickThreadFor(world, aabb)) { ++ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); ++ throw new IllegalStateException(reason); ++ } ++ } ++ ++ public static void ensureTickThread(final ServerLevel world, final double blockX, final double blockZ, final String reason) { ++ if (!isTickThreadFor(world, blockX, blockZ)) { ++ MinecraftServer.LOGGER.error("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) */ + + private static final AtomicInteger ID_GENERATOR = new AtomicInteger(); +@@ -66,13 +110,45 @@ public final class TickThread extends Thread { + } + + public static boolean isTickThread() { +- return Bukkit.isPrimaryThread(); ++ return Thread.currentThread() instanceof TickThread; ++ } ++ ++ public static boolean isShutdownThread() { ++ return false; ++ } ++ ++ public static boolean isTickThreadFor(final ServerLevel world, final BlockPos pos) { ++ return isTickThread(); ++ } ++ ++ public static boolean isTickThreadFor(final ServerLevel world, final ChunkPos pos) { ++ return isTickThread(); ++ } ++ ++ public static boolean isTickThreadFor(final ServerLevel world, final Vec3 pos) { ++ return isTickThread(); + } + + public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ) { + return isTickThread(); + } + ++ public static boolean isTickThreadFor(final ServerLevel world, final AABB aabb) { ++ return isTickThread(); ++ } ++ ++ public static boolean isTickThreadFor(final ServerLevel world, final double blockX, final double blockZ) { ++ return isTickThread(); ++ } ++ ++ public static boolean isTickThreadFor(final ServerLevel world, final Vec3 position, final Vec3 deltaMovement, final int buffer) { ++ return isTickThread(); ++ } ++ ++ public static boolean isTickThreadFor(final ServerLevel world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) { ++ return isTickThread(); ++ } ++ + public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ, final int radius) { + return isTickThread(); + } +diff --git a/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java b/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java +new file mode 100644 +index 0000000000000000000000000000000000000000..c78cbec447032de9fe69748591bef6be300160ed +--- /dev/null ++++ b/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java +@@ -0,0 +1,607 @@ ++package io.papermc.paper.world; ++ ++import com.destroystokyo.paper.util.maplist.EntityList; ++import io.papermc.paper.chunk.system.entity.EntityLookup; ++import io.papermc.paper.util.TickThread; ++import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; ++import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.level.ChunkHolder; ++import net.minecraft.server.level.FullChunkStatus; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.EntityType; ++import net.minecraft.world.entity.boss.EnderDragonPart; ++import net.minecraft.world.entity.boss.enderdragon.EnderDragon; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.chunk.storage.EntityStorage; ++import net.minecraft.world.level.entity.Visibility; ++import net.minecraft.world.phys.AABB; ++import org.bukkit.craftbukkit.event.CraftEventFactory; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Iterator; ++import java.util.List; ++import java.util.function.Predicate; ++import org.bukkit.event.entity.EntityRemoveEvent; ++ ++public final class ChunkEntitySlices { ++ ++ protected final int minSection; ++ protected final int maxSection; ++ public final int chunkX; ++ public final int chunkZ; ++ protected final ServerLevel world; ++ ++ protected final EntityCollectionBySection allEntities; ++ protected final EntityCollectionBySection hardCollidingEntities; ++ protected final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByClass; ++ protected final EntityList entities = new EntityList(); ++ ++ public FullChunkStatus status; ++ ++ protected boolean isTransient; ++ ++ public boolean isTransient() { ++ return this.isTransient; ++ } ++ ++ public void setTransient(final boolean value) { ++ this.isTransient = value; ++ } ++ ++ // TODO implement container search optimisations ++ ++ public ChunkEntitySlices(final ServerLevel world, final int chunkX, final int chunkZ, final FullChunkStatus status, ++ final int minSection, final int maxSection) { // inclusive, inclusive ++ this.minSection = minSection; ++ this.maxSection = maxSection; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.world = world; ++ ++ this.allEntities = new EntityCollectionBySection(this); ++ this.hardCollidingEntities = new EntityCollectionBySection(this); ++ this.entitiesByClass = new Reference2ObjectOpenHashMap<>(); ++ ++ this.status = status; ++ } ++ ++ // Paper start - optimise CraftChunk#getEntities ++ public org.bukkit.entity.Entity[] getChunkEntities() { ++ List ret = new java.util.ArrayList<>(); ++ final Entity[] entities = this.entities.getRawData(); ++ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) { ++ final Entity entity = entities[i]; ++ if (entity == null) { ++ continue; ++ } ++ final org.bukkit.entity.Entity bukkit = entity.getBukkitEntity(); ++ if (bukkit != null && bukkit.isValid()) { ++ ret.add(bukkit); ++ } ++ } ++ ++ return ret.toArray(new org.bukkit.entity.Entity[0]); ++ } ++ ++ public CompoundTag save() { ++ final int len = this.entities.size(); ++ if (len == 0) { ++ return null; ++ } ++ ++ final Entity[] rawData = this.entities.getRawData(); ++ final List collectedEntities = new ArrayList<>(len); ++ for (int i = 0; i < len; ++i) { ++ final Entity entity = rawData[i]; ++ if (entity.shouldBeSaved()) { ++ collectedEntities.add(entity); ++ } ++ } ++ ++ if (collectedEntities.isEmpty()) { ++ return null; ++ } ++ ++ return EntityStorage.saveEntityChunk(collectedEntities, new ChunkPos(this.chunkX, this.chunkZ), this.world); ++ } ++ ++ // returns true if this chunk has transient entities remaining ++ public boolean unload() { ++ final int len = this.entities.size(); ++ final Entity[] collectedEntities = Arrays.copyOf(this.entities.getRawData(), len); ++ ++ for (int i = 0; i < len; ++i) { ++ final Entity entity = collectedEntities[i]; ++ if (entity.isRemoved()) { ++ // removed by us below ++ continue; ++ } ++ if (entity.shouldBeSaved()) { ++ entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, EntityRemoveEvent.Cause.UNLOAD); ++ if (entity.isVehicle()) { ++ // we cannot assume that these entities are contained within this chunk, because entities can ++ // desync - so we need to remove them all ++ for (final Entity passenger : entity.getIndirectPassengers()) { ++ passenger.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, EntityRemoveEvent.Cause.UNLOAD); ++ } ++ } ++ } ++ } ++ ++ return this.entities.size() != 0; ++ } ++ ++ private List getAllEntities() { ++ final int len = this.entities.size(); ++ if (len == 0) { ++ return new ArrayList<>(); ++ } ++ ++ final Entity[] rawData = this.entities.getRawData(); ++ final List collectedEntities = new ArrayList<>(len); ++ for (int i = 0; i < len; ++i) { ++ collectedEntities.add(rawData[i]); ++ } ++ ++ return collectedEntities; ++ } ++ ++ public void callEntitiesLoadEvent() { ++ CraftEventFactory.callEntitiesLoadEvent(this.world, new ChunkPos(this.chunkX, this.chunkZ), this.getAllEntities()); ++ } ++ ++ public void callEntitiesUnloadEvent() { ++ CraftEventFactory.callEntitiesUnloadEvent(this.world, new ChunkPos(this.chunkX, this.chunkZ), this.getAllEntities()); ++ } ++ // Paper end - optimise CraftChunk#getEntities ++ ++ public boolean isEmpty() { ++ return this.entities.size() == 0; ++ } ++ ++ public void mergeInto(final ChunkEntitySlices slices) { ++ final Entity[] entities = this.entities.getRawData(); ++ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) { ++ final Entity entity = entities[i]; ++ slices.addEntity(entity, entity.sectionY); ++ } ++ } ++ ++ private boolean preventStatusUpdates; ++ public boolean startPreventingStatusUpdates() { ++ final boolean ret = this.preventStatusUpdates; ++ this.preventStatusUpdates = true; ++ return ret; ++ } ++ ++ public boolean isPreventingStatusUpdates() { ++ return this.preventStatusUpdates; ++ } ++ ++ public void stopPreventingStatusUpdates(final boolean prev) { ++ this.preventStatusUpdates = prev; ++ } ++ ++ public void updateStatus(final FullChunkStatus status, final EntityLookup lookup) { ++ this.status = status; ++ ++ final Entity[] entities = this.entities.getRawData(); ++ ++ for (int i = 0, size = this.entities.size(); i < size; ++i) { ++ final Entity entity = entities[i]; ++ ++ final Visibility oldVisibility = EntityLookup.getEntityStatus(entity); ++ entity.chunkStatus = status; ++ final Visibility newVisibility = EntityLookup.getEntityStatus(entity); ++ ++ lookup.entityStatusChange(entity, this, oldVisibility, newVisibility, false, false, false); ++ } ++ } ++ ++ public boolean addEntity(final Entity entity, final int chunkSection) { ++ if (!this.entities.add(entity)) { ++ return false; ++ } ++ entity.chunkStatus = this.status; ++ final int sectionIndex = chunkSection - this.minSection; ++ ++ this.allEntities.addEntity(entity, sectionIndex); ++ ++ if (entity.hardCollides()) { ++ this.hardCollidingEntities.addEntity(entity, sectionIndex); ++ } ++ ++ for (final Iterator, EntityCollectionBySection>> iterator = ++ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final Reference2ObjectMap.Entry, EntityCollectionBySection> entry = iterator.next(); ++ ++ if (entry.getKey().isInstance(entity)) { ++ entry.getValue().addEntity(entity, sectionIndex); ++ } ++ } ++ ++ return true; ++ } ++ ++ public boolean removeEntity(final Entity entity, final int chunkSection) { ++ if (!this.entities.remove(entity)) { ++ return false; ++ } ++ entity.chunkStatus = null; ++ final int sectionIndex = chunkSection - this.minSection; ++ ++ this.allEntities.removeEntity(entity, sectionIndex); ++ ++ if (entity.hardCollides()) { ++ this.hardCollidingEntities.removeEntity(entity, sectionIndex); ++ } ++ ++ for (final Iterator, EntityCollectionBySection>> iterator = ++ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { ++ final Reference2ObjectMap.Entry, EntityCollectionBySection> entry = iterator.next(); ++ ++ if (entry.getKey().isInstance(entity)) { ++ entry.getValue().removeEntity(entity, sectionIndex); ++ } ++ } ++ ++ return true; ++ } ++ ++ public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { ++ this.hardCollidingEntities.getEntities(except, box, into, predicate); ++ } ++ ++ public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { ++ this.allEntities.getEntitiesWithEnderDragonParts(except, box, into, predicate); ++ } ++ ++ public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List into, final Predicate predicate) { ++ this.allEntities.getEntities(except, box, into, predicate); ++ } ++ ++ public void getEntities(final EntityType type, final AABB box, final List into, ++ final Predicate predicate) { ++ this.allEntities.getEntities(type, box, (List)into, (Predicate)predicate); ++ } ++ ++ protected EntityCollectionBySection initClass(final Class clazz) { ++ final EntityCollectionBySection ret = new EntityCollectionBySection(this); ++ ++ for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) { ++ final BasicEntityList sectionEntities = this.allEntities.entitiesBySection[sectionIndex]; ++ if (sectionEntities == null) { ++ continue; ++ } ++ ++ final Entity[] storage = sectionEntities.storage; ++ ++ for (int i = 0, len = Math.min(storage.length, sectionEntities.size()); i < len; ++i) { ++ final Entity entity = storage[i]; ++ ++ if (clazz.isInstance(entity)) { ++ ret.addEntity(entity, sectionIndex); ++ } ++ } ++ } ++ ++ return ret; ++ } ++ ++ public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, ++ final Predicate predicate) { ++ EntityCollectionBySection collection = this.entitiesByClass.get(clazz); ++ if (collection != null) { ++ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate); ++ } else { ++ this.entitiesByClass.putIfAbsent(clazz, collection = this.initClass(clazz)); ++ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate); ++ } ++ } ++ ++ protected static final class BasicEntityList { ++ ++ protected static final Entity[] EMPTY = new Entity[0]; ++ protected static final int DEFAULT_CAPACITY = 4; ++ ++ protected E[] storage; ++ protected int size; ++ ++ public BasicEntityList() { ++ this(0); ++ } ++ ++ public BasicEntityList(final int cap) { ++ this.storage = (E[])(cap <= 0 ? EMPTY : new Entity[cap]); ++ } ++ ++ public boolean isEmpty() { ++ return this.size == 0; ++ } ++ ++ public int size() { ++ return this.size; ++ } ++ ++ private void resize() { ++ if (this.storage == EMPTY) { ++ this.storage = (E[])new Entity[DEFAULT_CAPACITY]; ++ } else { ++ this.storage = Arrays.copyOf(this.storage, this.storage.length * 2); ++ } ++ } ++ ++ public void add(final E entity) { ++ final int idx = this.size++; ++ if (idx >= this.storage.length) { ++ this.resize(); ++ this.storage[idx] = entity; ++ } else { ++ this.storage[idx] = entity; ++ } ++ } ++ ++ public int indexOf(final E entity) { ++ final E[] storage = this.storage; ++ ++ for (int i = 0, len = Math.min(this.storage.length, this.size); i < len; ++i) { ++ if (storage[i] == entity) { ++ return i; ++ } ++ } ++ ++ return -1; ++ } ++ ++ public boolean remove(final E entity) { ++ final int idx = this.indexOf(entity); ++ if (idx == -1) { ++ return false; ++ } ++ ++ final int size = --this.size; ++ final E[] storage = this.storage; ++ if (idx != size) { ++ System.arraycopy(storage, idx + 1, storage, idx, size - idx); ++ } ++ ++ storage[size] = null; ++ ++ return true; ++ } ++ ++ public boolean has(final E entity) { ++ return this.indexOf(entity) != -1; ++ } ++ } ++ ++ protected static final class EntityCollectionBySection { ++ ++ protected final ChunkEntitySlices manager; ++ protected final long[] nonEmptyBitset; ++ protected final BasicEntityList[] entitiesBySection; ++ protected int count; ++ ++ public EntityCollectionBySection(final ChunkEntitySlices manager) { ++ this.manager = manager; ++ ++ final int sectionCount = manager.maxSection - manager.minSection + 1; ++ ++ this.nonEmptyBitset = new long[(sectionCount + (Long.SIZE - 1)) >>> 6]; // (sectionCount + (Long.SIZE - 1)) / Long.SIZE ++ this.entitiesBySection = new BasicEntityList[sectionCount]; ++ } ++ ++ public void addEntity(final Entity entity, final int sectionIndex) { ++ BasicEntityList list = this.entitiesBySection[sectionIndex]; ++ ++ if (list != null && list.has(entity)) { ++ return; ++ } ++ ++ if (list == null) { ++ this.entitiesBySection[sectionIndex] = list = new BasicEntityList<>(); ++ this.nonEmptyBitset[sectionIndex >>> 6] |= (1L << (sectionIndex & (Long.SIZE - 1))); ++ } ++ ++ list.add(entity); ++ ++this.count; ++ } ++ ++ public void removeEntity(final Entity entity, final int sectionIndex) { ++ final BasicEntityList list = this.entitiesBySection[sectionIndex]; ++ ++ if (list == null || !list.remove(entity)) { ++ return; ++ } ++ ++ --this.count; ++ ++ if (list.isEmpty()) { ++ this.entitiesBySection[sectionIndex] = null; ++ this.nonEmptyBitset[sectionIndex >>> 6] ^= (1L << (sectionIndex & (Long.SIZE - 1))); ++ } ++ } ++ ++ public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { ++ if (this.count == 0) { ++ return; ++ } ++ ++ final int minSection = this.manager.minSection; ++ final int maxSection = this.manager.maxSection; ++ ++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); ++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); ++ ++ final BasicEntityList[] entitiesBySection = this.entitiesBySection; ++ ++ for (int section = min; section <= max; ++section) { ++ final BasicEntityList list = entitiesBySection[section - minSection]; ++ ++ if (list == null) { ++ continue; ++ } ++ ++ final Entity[] storage = list.storage; ++ ++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { ++ final Entity entity = storage[i]; ++ ++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { ++ continue; ++ } ++ ++ if (predicate != null && !predicate.test(entity)) { ++ continue; ++ } ++ ++ into.add(entity); ++ } ++ } ++ } ++ ++ public void getEntitiesWithEnderDragonParts(final Entity except, final AABB box, final List into, ++ final Predicate predicate) { ++ if (this.count == 0) { ++ return; ++ } ++ ++ final int minSection = this.manager.minSection; ++ final int maxSection = this.manager.maxSection; ++ ++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); ++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); ++ ++ final BasicEntityList[] entitiesBySection = this.entitiesBySection; ++ ++ for (int section = min; section <= max; ++section) { ++ final BasicEntityList list = entitiesBySection[section - minSection]; ++ ++ if (list == null) { ++ continue; ++ } ++ ++ final Entity[] storage = list.storage; ++ ++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { ++ final Entity entity = storage[i]; ++ ++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { ++ continue; ++ } ++ ++ if (predicate == null || predicate.test(entity)) { ++ into.add(entity); ++ } // else: continue to test the ender dragon parts ++ ++ if (entity instanceof EnderDragon) { ++ for (final EnderDragonPart part : ((EnderDragon)entity).subEntities) { ++ if (part == except || !part.getBoundingBox().intersects(box)) { ++ continue; ++ } ++ ++ if (predicate != null && !predicate.test(part)) { ++ continue; ++ } ++ ++ into.add(part); ++ } ++ } ++ } ++ } ++ } ++ ++ public void getEntitiesWithEnderDragonParts(final Entity except, final Class clazz, final AABB box, final List into, ++ final Predicate predicate) { ++ if (this.count == 0) { ++ return; ++ } ++ ++ final int minSection = this.manager.minSection; ++ final int maxSection = this.manager.maxSection; ++ ++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); ++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); ++ ++ final BasicEntityList[] entitiesBySection = this.entitiesBySection; ++ ++ for (int section = min; section <= max; ++section) { ++ final BasicEntityList list = entitiesBySection[section - minSection]; ++ ++ if (list == null) { ++ continue; ++ } ++ ++ final Entity[] storage = list.storage; ++ ++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { ++ final Entity entity = storage[i]; ++ ++ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { ++ continue; ++ } ++ ++ if (predicate == null || predicate.test(entity)) { ++ into.add(entity); ++ } // else: continue to test the ender dragon parts ++ ++ if (entity instanceof EnderDragon) { ++ for (final EnderDragonPart part : ((EnderDragon)entity).subEntities) { ++ if (part == except || !part.getBoundingBox().intersects(box) || !clazz.isInstance(part)) { ++ continue; ++ } ++ ++ if (predicate != null && !predicate.test(part)) { ++ continue; ++ } ++ ++ into.add(part); ++ } ++ } ++ } ++ } ++ } ++ ++ public void getEntities(final EntityType type, final AABB box, final List into, ++ final Predicate predicate) { ++ if (this.count == 0) { ++ return; ++ } ++ ++ final int minSection = this.manager.minSection; ++ final int maxSection = this.manager.maxSection; ++ ++ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); ++ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); ++ ++ final BasicEntityList[] entitiesBySection = this.entitiesBySection; ++ ++ for (int section = min; section <= max; ++section) { ++ final BasicEntityList list = entitiesBySection[section - minSection]; ++ ++ if (list == null) { ++ continue; ++ } ++ ++ final Entity[] storage = list.storage; ++ ++ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { ++ final Entity entity = storage[i]; ++ ++ if (entity == null || (type != null && entity.getType() != type) || !entity.getBoundingBox().intersects(box)) { ++ continue; ++ } ++ ++ if (predicate != null && !predicate.test((T)entity)) { ++ continue; ++ } ++ ++ into.add((T)entity); ++ } ++ } ++ } ++ } ++} +diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java +index c33f85b570f159ab465b5a10a8044a81f2797f43..244a19ecd0234fa1d7a6ecfea20751595688605d 100644 +--- a/src/main/java/net/minecraft/server/Main.java ++++ b/src/main/java/net/minecraft/server/Main.java +@@ -320,6 +320,7 @@ public class Main { + + convertable_conversionsession.saveDataTag(iregistrycustom_dimension, savedata); + */ ++ Class.forName(net.minecraft.world.entity.npc.VillagerTrades.class.getName()); // Paper - load this sync so it won't fail later async + final DedicatedServer dedicatedserver = (DedicatedServer) MinecraftServer.spin((thread) -> { + DedicatedServer dedicatedserver1 = new DedicatedServer(optionset, worldLoader.get(), thread, convertable_conversionsession, resourcepackrepository, worldstem, dedicatedserversettings, DataFixers.getDataFixer(), services, LoggerChunkProgressListener::createFromGameruleRadius); + +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 881679270a344f1a4d70c3c47871a495af202109..40708dfd84dc21bc5e047d9fb3b092cb854a8041 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -318,7 +318,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { + AtomicReference atomicreference = new AtomicReference(); +- Thread thread = new Thread(() -> { ++ Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system + ((MinecraftServer) atomicreference.get()).runServer(); + }, "Server thread"); + +@@ -654,7 +654,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { +- return worldserver1.getChunkSource().chunkMap.hasWork(); +- })) { +- this.nextTickTimeNanos = Util.getNanos() + TimeUtil.NANOSECONDS_PER_MILLISECOND; +- iterator = this.getAllLevels().iterator(); +- +- while (iterator.hasNext()) { +- worldserver = (ServerLevel) iterator.next(); +- worldserver.getChunkSource().removeTicketsOnClosing(); +- worldserver.getChunkSource().tick(() -> { +- return true; +- }, false); +- } +- +- this.waitUntilNextTick(); +- } +- +- this.saveAllChunks(false, true, false); +- iterator = this.getAllLevels().iterator(); +- +- while (iterator.hasNext()) { +- worldserver = (ServerLevel) iterator.next(); +- if (worldserver != null) { +- try { +- worldserver.close(); +- } catch (IOException ioexception) { +- MinecraftServer.LOGGER.error("Exception closing the level", ioexception); +- } +- } +- } ++ this.saveAllChunks(false, true, false, true); // Paper - rewrite chunk system - move closing into here + + this.isSaving = false; + this.resources.close(); +@@ -1023,6 +1004,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { +- for (final Entity entity : level.getEntities().getAll()) { ++ for (final Entity entity : level.getEntityLookup().getAllCopy()) { // Paper - rewrite chunk system + if (entity.isRemoved()) { + continue; + } +@@ -2625,6 +2620,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { ++ LOGGER.info("Async debug chunks executing"); ++ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(false); ++ CommandSender sender = MinecraftServer.getServer().console; ++ java.io.File file = new java.io.File(new java.io.File(new java.io.File("."), "debug"), ++ "chunks-" + java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(java.time.LocalDateTime.now()) + ".txt"); ++ sender.sendMessage(net.kyori.adventure.text.Component.text("Writing chunk information dump to " + file, net.kyori.adventure.text.format.NamedTextColor.GREEN)); ++ try { ++ io.papermc.paper.util.MCUtil.dumpChunks(file, true); ++ sender.sendMessage(net.kyori.adventure.text.Component.text("Successfully written chunk information!", net.kyori.adventure.text.format.NamedTextColor.GREEN)); ++ } catch (Throwable thr) { ++ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr); ++ sender.sendMessage(net.kyori.adventure.text.Component.text("Failed to dump chunk information, see console", net.kyori.adventure.text.format.NamedTextColor.RED)); ++ } ++ }; ++ Thread t = new Thread(run); ++ t.setName("Async debug thread #" + ASYNC_DEBUG_CHUNKS_COUNT.getAndIncrement()); ++ t.setDaemon(true); ++ t.start(); ++ return; ++ } ++ // Paper end - rewrite chunk system + this.serverCommandQueue.add(new ConsoleInput(command, commandSource)); // Paper - Perf: use proper queue + } + +diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java +index 88729d92878f98729eb5669cce5ae5b1418865a1..13d15a135dd0373bef4a5ac9ffb56dbbf53353a0 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java ++++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java +@@ -46,17 +46,12 @@ public class ChunkHolder { + public static final ChunkResult NOT_DONE_YET = ChunkResult.error("Not done yet"); + private static final CompletableFuture> UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.completedFuture(ChunkHolder.UNLOADED_LEVEL_CHUNK); + private static final List CHUNK_STATUSES = ChunkStatus.getStatusList(); +- private final AtomicReferenceArray>> futures; ++ // Paper - rewrite chunk system + private final LevelHeightAccessor levelHeightAccessor; +- private volatile CompletableFuture> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage +- private volatile CompletableFuture> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage +- private volatile CompletableFuture> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage +- public CompletableFuture chunkToSave; // Paper - public ++ // Paper - rewrite chunk system + @Nullable + private final DebugBuffer chunkToSaveHistory; +- public int oldTicketLevel; +- private int ticketLevel; +- private int queueLevel; ++ // Paper - rewrite chunk system + public final ChunkPos pos; + private boolean hasChangedSections; + private final ShortSet[] changedBlocksPerSection; +@@ -65,11 +60,20 @@ public class ChunkHolder { + private final LevelLightEngine lightEngine; + private final ChunkHolder.LevelChangeListener onLevelChange; + public final ChunkHolder.PlayerProvider playerProvider; +- private boolean wasAccessibleSinceLastSave; +- private CompletableFuture pendingFullStateConfirmation; +- private CompletableFuture sendSync; ++ // Paper - rewrite chunk system + + private final ChunkMap chunkMap; // Paper ++ // Paper start - no-tick view distance ++ public final LevelChunk getSendingChunk() { ++ // it's important that we use getChunkAtIfLoadedImmediately to mirror the chunk sending logic used ++ // in Chunk's neighbour callback ++ LevelChunk ret = this.chunkMap.level.getChunkSource().getChunkAtIfLoadedImmediately(this.pos.x, this.pos.z); ++ if (ret != null && ret.areNeighboursLoaded(1)) { ++ return ret; ++ } ++ return null; ++ } ++ // Paper end - no-tick view distance + + // Paper start + public void onChunkAdd() { +@@ -81,147 +85,131 @@ public class ChunkHolder { + } + // Paper end + +- public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) { +- this.futures = new AtomicReferenceArray(ChunkHolder.CHUNK_STATUSES.size()); +- this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; +- this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; +- this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; +- this.chunkToSave = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error ++ public final io.papermc.paper.chunk.system.scheduling.NewChunkHolder newChunkHolder; // Paper - rewrite chunk system ++ ++ // Paper start - replace player chunk loader ++ private final com.destroystokyo.paper.util.maplist.ReferenceList playersSentChunkTo = new com.destroystokyo.paper.util.maplist.ReferenceList<>(); ++ ++ public void addPlayer(ServerPlayer player) { ++ if (!this.playersSentChunkTo.add(player)) { ++ throw new IllegalStateException("Already sent chunk " + this.pos + " in world '" + this.chunkMap.level.getWorld().getName() + "' to player " + player); ++ } ++ } ++ ++ public void removePlayer(ServerPlayer player) { ++ if (!this.playersSentChunkTo.remove(player)) { ++ throw new IllegalStateException("Have not sent chunk " + this.pos + " in world '" + this.chunkMap.level.getWorld().getName() + "' to player " + player); ++ } ++ } ++ ++ public boolean hasChunkBeenSent() { ++ return this.playersSentChunkTo.size() != 0; ++ } ++ ++ public boolean hasBeenSent(ServerPlayer to) { ++ return this.playersSentChunkTo.contains(to); ++ } ++ // Paper end - replace player chunk loader ++ public ChunkHolder(ChunkPos pos, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.PlayerProvider playersWatchingChunkProvider, io.papermc.paper.chunk.system.scheduling.NewChunkHolder newChunkHolder) { // Paper - rewrite chunk system ++ this.newChunkHolder = newChunkHolder; // Paper - rewrite chunk system + this.chunkToSaveHistory = null; + this.blockChangedLightSectionFilter = new BitSet(); + this.skyChangedLightSectionFilter = new BitSet(); +- this.pendingFullStateConfirmation = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error +- this.sendSync = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error ++ // Paper - rewrite chunk system + this.pos = pos; + this.levelHeightAccessor = world; + this.lightEngine = lightingProvider; +- this.onLevelChange = levelUpdateListener; ++ this.onLevelChange = null; // Paper - rewrite chunk system + this.playerProvider = playersWatchingChunkProvider; +- this.oldTicketLevel = ChunkLevel.MAX_LEVEL + 1; +- this.ticketLevel = this.oldTicketLevel; +- this.queueLevel = this.oldTicketLevel; +- this.setTicketLevel(level); ++ // Paper - rewrite chunk system + this.changedBlocksPerSection = new ShortSet[world.getSectionsCount()]; + this.chunkMap = (ChunkMap)playersWatchingChunkProvider; // Paper + } + + // Paper start + public @Nullable ChunkAccess getAvailableChunkNow() { +- // TODO can we just getStatusFuture(EMPTY)? +- for (ChunkStatus curr = ChunkStatus.FULL, next = curr.getParent(); curr != next; curr = next, next = next.getParent()) { +- CompletableFuture> future = this.getFutureIfPresentUnchecked(curr); +- ChunkResult either = future.getNow(null); +- if (either == null || either.isSuccess()) { +- continue; +- } +- return either.orElseThrow(IllegalStateException::new); +- } +- return null; ++ return this.newChunkHolder.getCurrentChunk(); // Paper - rewrite chunk system + } + // Paper end + // CraftBukkit start + public LevelChunk getFullChunkNow() { +- // Note: We use the oldTicketLevel for isLoaded checks. +- if (!ChunkLevel.fullStatus(this.oldTicketLevel).isOrAfter(FullChunkStatus.FULL)) return null; +- return this.getFullChunkNowUnchecked(); ++ // Paper start - rewrite chunk system ++ if (!this.isFullChunkReady() || !(this.getAvailableChunkNow() instanceof LevelChunk chunk)) return null; // instanceof to avoid a race condition on off-main threads ++ return chunk; ++ // Paper end - rewrite chunk system + } + + public LevelChunk getFullChunkNowUnchecked() { +- CompletableFuture> statusFuture = this.getFutureIfPresentUnchecked(ChunkStatus.FULL); +- ChunkResult either = statusFuture.getNow(null); +- return (either == null) ? null : (LevelChunk) either.orElse(null); ++ // Paper start - rewrite chunk system ++ return this.getAvailableChunkNow() instanceof LevelChunk chunk ? chunk : null; ++ // Paper end - rewrite chunk system + } + // CraftBukkit end + + public CompletableFuture> getFutureIfPresentUnchecked(ChunkStatus leastStatus) { +- CompletableFuture> completablefuture = (CompletableFuture) this.futures.get(leastStatus.getIndex()); +- +- return completablefuture == null ? ChunkHolder.UNLOADED_CHUNK_FUTURE : completablefuture; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public CompletableFuture> getFutureIfPresent(ChunkStatus leastStatus) { +- return ChunkLevel.generationStatus(this.ticketLevel).isOrAfter(leastStatus) ? this.getFutureIfPresentUnchecked(leastStatus) : ChunkHolder.UNLOADED_CHUNK_FUTURE; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public final CompletableFuture> getTickingChunkFuture() { // Paper - final for inline +- return this.tickingChunkFuture; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public final CompletableFuture> getEntityTickingChunkFuture() { // Paper - final for inline +- return this.entityTickingChunkFuture; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk systemv + } + + public final CompletableFuture> getFullChunkFuture() { // Paper - final for inline +- return this.fullChunkFuture; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk systemv + } + + @Nullable + public final LevelChunk getTickingChunk() { // Paper - final for inline +- return (LevelChunk) ((ChunkResult) this.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).orElse(null); // CraftBukkit - decompile error ++ // Paper start - rewrite chunk system ++ if (!this.isTickingReady()) { ++ return null; ++ } ++ return (LevelChunk)this.getAvailableChunkNow(); ++ // Paper end - rewrite chunk system + } + + public CompletableFuture getChunkSendSyncFuture() { +- return this.sendSync; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Nullable + public LevelChunk getChunkToSend() { +- return !this.sendSync.isDone() ? null : this.getTickingChunk(); ++ return this.getSendingChunk(); // Paper - rewrite chunk system + } + + @Nullable + public ChunkStatus getLastAvailableStatus() { +- for (int i = ChunkHolder.CHUNK_STATUSES.size() - 1; i >= 0; --i) { +- ChunkStatus chunkstatus = (ChunkStatus) ChunkHolder.CHUNK_STATUSES.get(i); +- CompletableFuture> completablefuture = this.getFutureIfPresentUnchecked(chunkstatus); +- +- if (((ChunkResult) completablefuture.getNow(ChunkHolder.UNLOADED_CHUNK)).isSuccess()) { +- return chunkstatus; +- } +- } +- +- return null; ++ return this.newChunkHolder.getCurrentGenStatus(); // Paper - rewrite chunk system + } + + // Paper start + public @Nullable ChunkStatus getChunkHolderStatus() { +- for (ChunkStatus curr = ChunkStatus.FULL, next = curr.getParent(); curr != next; curr = next, next = next.getParent()) { +- CompletableFuture> future = this.getFutureIfPresentUnchecked(curr); +- ChunkResult either = future.getNow(null); +- if (either == null || !either.isSuccess()) { +- continue; +- } +- return curr; +- } +- +- return null; ++ return this.newChunkHolder.getCurrentGenStatus(); // Paper - rewrite chunk system + } + // Paper end + + @Nullable + public ChunkAccess getLastAvailable() { +- for (int i = ChunkHolder.CHUNK_STATUSES.size() - 1; i >= 0; --i) { +- ChunkStatus chunkstatus = (ChunkStatus) ChunkHolder.CHUNK_STATUSES.get(i); +- CompletableFuture> completablefuture = this.getFutureIfPresentUnchecked(chunkstatus); +- +- if (!completablefuture.isCompletedExceptionally()) { +- ChunkAccess ichunkaccess = (ChunkAccess) ((ChunkResult) completablefuture.getNow(ChunkHolder.UNLOADED_CHUNK)).orElse((Object) null); +- +- if (ichunkaccess != null) { +- return ichunkaccess; +- } +- } +- } +- +- return null; ++ return this.newChunkHolder.getCurrentChunk(); // Paper - rewrite chunk system + } + +- public final CompletableFuture getChunkToSave() { // Paper - final for inline +- return this.chunkToSave; +- } ++ // Paper - rewrite chunk system + + public void blockChanged(BlockPos pos) { +- LevelChunk chunk = this.getTickingChunk(); ++ // Paper start - replace player chunk loader ++ if (this.playersSentChunkTo.size() == 0) { ++ return; ++ } ++ // Paper end - replace player chunk loader ++ LevelChunk chunk = this.getSendingChunk(); // Paper - no-tick view distance + + if (chunk != null) { + int i = this.levelHeightAccessor.getSectionIndex(pos.getY()); +@@ -237,13 +225,13 @@ public class ChunkHolder { + } + + public void sectionLightChanged(LightLayer lightType, int y) { +- ChunkAccess ichunkaccess = (ChunkAccess) ((ChunkResult) this.getFutureIfPresent(ChunkStatus.INITIALIZE_LIGHT).getNow(ChunkHolder.UNLOADED_CHUNK)).orElse(null); // CraftBukkit - decompile error ++ ChunkAccess ichunkaccess = this.getAvailableChunkNow(); // Paper - rewrite chunk system + + if (ichunkaccess != null) { + ichunkaccess.setUnsaved(true); +- LevelChunk chunk = this.getTickingChunk(); ++ LevelChunk chunk = this.getSendingChunk(); // Paper - rewrite chunk system + +- if (chunk != null) { ++ if (this.playersSentChunkTo.size() != 0 && chunk != null) { // Paper - replace player chunk loader + int j = this.lightEngine.getMinLightSection(); + int k = this.lightEngine.getMaxLightSection(); + +@@ -263,7 +251,7 @@ public class ChunkHolder { + + // Paper start - starlight + public void broadcast(Packet packet, boolean onChunkViewEdge) { +- this.broadcast(this.playerProvider.getPlayers(this.pos, onChunkViewEdge), packet); ++ this.broadcast(this.getPlayers(onChunkViewEdge), packet); // Paper - rewrite chunk system + } + // Paper end - starlight + +@@ -273,7 +261,7 @@ public class ChunkHolder { + List list; + + if (!this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty()) { +- list = this.playerProvider.getPlayers(this.pos, true); ++ list = this.getPlayers(true); // Paper - rewrite chunk system + if (!list.isEmpty()) { + ClientboundLightUpdatePacket packetplayoutlightupdate = new ClientboundLightUpdatePacket(chunk.getPos(), this.lightEngine, this.skyChangedLightSectionFilter, this.blockChangedLightSectionFilter); + +@@ -285,7 +273,7 @@ public class ChunkHolder { + } + + if (this.hasChangedSections) { +- list = this.playerProvider.getPlayers(this.pos, false); ++ list = this.getPlayers(false); // Paper - rewrite chunk system + + for (int i = 0; i < this.changedBlocksPerSection.length; ++i) { + ShortSet shortset = this.changedBlocksPerSection[i]; +@@ -343,75 +331,33 @@ public class ChunkHolder { + + } + +- private void broadcast(List players, Packet packet) { +- players.forEach((entityplayer) -> { +- entityplayer.connection.send(packet); +- }); +- } +- +- public CompletableFuture> getOrScheduleFuture(ChunkStatus targetStatus, ChunkMap chunkStorage) { +- int i = targetStatus.getIndex(); +- CompletableFuture> completablefuture = (CompletableFuture) this.futures.get(i); ++ // Paper start - rewrite chunk system ++ public List getPlayers(boolean onlyOnWatchDistanceEdge) { ++ List ret = new java.util.ArrayList<>(); + +- if (completablefuture != null) { +- ChunkResult chunkresult = (ChunkResult) completablefuture.getNow(ChunkHolder.NOT_DONE_YET); +- +- if (chunkresult == null) { +- String s = String.valueOf(targetStatus); +- String s1 = "value in future for status: " + s + " was incorrectly set to null at chunk: " + String.valueOf(this.pos); +- +- throw chunkStorage.debugFuturesAndCreateReportedException(new IllegalStateException("null value previously set for chunk status"), s1); +- } +- +- if (chunkresult == ChunkHolder.NOT_DONE_YET || chunkresult.isSuccess()) { +- return completablefuture; ++ for (int i = 0, len = this.playersSentChunkTo.size(); i < len; ++i) { ++ ServerPlayer player = this.playersSentChunkTo.getUnchecked(i); ++ if (onlyOnWatchDistanceEdge && !this.chunkMap.level.playerChunkLoader.isChunkSent(player, this.pos.x, this.pos.z, onlyOnWatchDistanceEdge)) { ++ continue; + } ++ ret.add(player); + } + +- if (ChunkLevel.generationStatus(this.ticketLevel).isOrAfter(targetStatus)) { +- CompletableFuture> completablefuture1 = chunkStorage.schedule(this, targetStatus); +- +- this.updateChunkToSave(completablefuture1, "schedule " + String.valueOf(targetStatus)); +- this.futures.set(i, completablefuture1); +- return completablefuture1; +- } else { +- return completablefuture == null ? ChunkHolder.UNLOADED_CHUNK_FUTURE : completablefuture; +- } ++ return ret; + } ++ // Paper end - rewrite chunk system + +- protected void addSaveDependency(String thenDesc, CompletableFuture then) { +- if (this.chunkToSaveHistory != null) { +- this.chunkToSaveHistory.push(new ChunkHolder.ChunkSaveDebug(Thread.currentThread(), then, thenDesc)); +- } + +- this.chunkToSave = this.chunkToSave.thenCombine(then, (ichunkaccess, object) -> { +- return ichunkaccess; +- }); +- } +- +- private void updateChunkToSave(CompletableFuture> then, String thenDesc) { +- if (this.chunkToSaveHistory != null) { +- this.chunkToSaveHistory.push(new ChunkHolder.ChunkSaveDebug(Thread.currentThread(), then, thenDesc)); +- } +- +- this.chunkToSave = this.chunkToSave.thenCombine(then, (ichunkaccess, chunkresult) -> { +- return (ChunkAccess) ChunkResult.orElse(chunkresult, ichunkaccess); ++ private void broadcast(List players, Packet packet) { ++ players.forEach((entityplayer) -> { ++ entityplayer.connection.send(packet); + }); + } + +- public void addSendDependency(CompletableFuture postProcessingFuture) { +- if (this.sendSync.isDone()) { +- this.sendSync = postProcessingFuture; +- } else { +- this.sendSync = this.sendSync.thenCombine(postProcessingFuture, (object, object1) -> { +- return null; +- }); +- } +- +- } ++ // Paper - rewrite chunk system + + public FullChunkStatus getFullStatus() { +- return ChunkLevel.fullStatus(this.ticketLevel); ++ return this.newChunkHolder.getChunkStatus(); // Paper - rewrite chunk system + } + + public final ChunkPos getPos() { // Paper - final for inline +@@ -419,238 +365,17 @@ public class ChunkHolder { + } + + public final int getTicketLevel() { // Paper - final for inline +- return this.ticketLevel; +- } +- +- public int getQueueLevel() { +- return this.queueLevel; +- } +- +- private void setQueueLevel(int level) { +- this.queueLevel = level; +- } +- +- public void setTicketLevel(int level) { +- this.ticketLevel = level; +- } +- +- private void scheduleFullChunkPromotion(ChunkMap playerchunkmap, CompletableFuture> completablefuture, Executor executor, FullChunkStatus fullchunkstatus) { +- this.pendingFullStateConfirmation.cancel(false); +- CompletableFuture completablefuture1 = new CompletableFuture(); +- +- completablefuture1.thenRunAsync(() -> { +- playerchunkmap.onFullChunkStatusChange(this.pos, fullchunkstatus); +- }, executor); +- this.pendingFullStateConfirmation = completablefuture1; +- completablefuture.thenAccept((chunkresult) -> { +- chunkresult.ifSuccess((chunk) -> { +- completablefuture1.complete(null); // CraftBukkit - decompile error +- }); +- }); +- } +- +- private void demoteFullChunk(ChunkMap playerchunkmap, FullChunkStatus fullchunkstatus) { +- this.pendingFullStateConfirmation.cancel(false); +- playerchunkmap.onFullChunkStatusChange(this.pos, fullchunkstatus); +- } +- +- protected void updateFutures(ChunkMap chunkStorage, Executor executor) { +- ChunkStatus chunkstatus = ChunkLevel.generationStatus(this.oldTicketLevel); +- ChunkStatus chunkstatus1 = ChunkLevel.generationStatus(this.ticketLevel); +- boolean flag = ChunkLevel.isLoaded(this.oldTicketLevel); +- boolean flag1 = ChunkLevel.isLoaded(this.ticketLevel); +- FullChunkStatus fullchunkstatus = ChunkLevel.fullStatus(this.oldTicketLevel); +- FullChunkStatus fullchunkstatus1 = ChunkLevel.fullStatus(this.ticketLevel); +- // CraftBukkit start +- // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins. +- if (fullchunkstatus.isOrAfter(FullChunkStatus.FULL) && !fullchunkstatus1.isOrAfter(FullChunkStatus.FULL)) { +- this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> { +- LevelChunk chunk = (LevelChunk) either.orElse(null); +- if (chunk != null) { +- chunkStorage.callbackExecutor.execute(() -> { +- // Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick +- // lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag. +- // These actions may however happen deferred, so we manually set the needsSaving flag already here. +- chunk.setUnsaved(true); +- chunk.unloadCallback(); +- }); +- } +- }).exceptionally((throwable) -> { +- // ensure exceptions are printed, by default this is not the case +- MinecraftServer.LOGGER.error("Failed to schedule unload callback for chunk " + ChunkHolder.this.pos, throwable); +- return null; +- }); +- +- // Run callback right away if the future was already done +- chunkStorage.callbackExecutor.run(); +- } +- // CraftBukkit end +- +- if (flag) { +- ChunkResult chunkresult = ChunkResult.error(() -> { +- return "Unloaded ticket level " + String.valueOf(this.pos); +- }); +- +- for (int i = flag1 ? chunkstatus1.getIndex() + 1 : 0; i <= chunkstatus.getIndex(); ++i) { +- CompletableFuture> completablefuture = (CompletableFuture) this.futures.get(i); +- +- if (completablefuture == null) { +- this.futures.set(i, CompletableFuture.completedFuture(chunkresult)); +- } +- } +- } +- +- boolean flag2 = fullchunkstatus.isOrAfter(FullChunkStatus.FULL); +- boolean flag3 = fullchunkstatus1.isOrAfter(FullChunkStatus.FULL); +- +- this.wasAccessibleSinceLastSave |= flag3; +- if (!flag2 && flag3) { +- int expectCreateCount = ++this.fullChunkCreateCount; // Paper +- this.fullChunkFuture = chunkStorage.prepareAccessibleChunk(this); +- this.scheduleFullChunkPromotion(chunkStorage, this.fullChunkFuture, executor, FullChunkStatus.FULL); +- // Paper start - cache ticking ready status +- this.fullChunkFuture.thenAccept(chunkResult -> { +- chunkResult.ifSuccess(chunk -> { +- if (ChunkHolder.this.fullChunkCreateCount == expectCreateCount) { +- ChunkHolder.this.isFullChunkReady = true; +- io.papermc.paper.chunk.system.ChunkSystem.onChunkBorder(chunk, this); +- } +- }); +- }); +- this.updateChunkToSave(this.fullChunkFuture, "full"); +- } +- +- if (flag2 && !flag3) { +- // Paper start +- if (this.isFullChunkReady) { +- io.papermc.paper.chunk.system.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper +- } +- // Paper end +- this.fullChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); +- this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; +- ++this.fullChunkCreateCount; // Paper - cache ticking ready status +- this.isFullChunkReady = false; // Paper - cache ticking ready status +- } +- +- boolean flag4 = fullchunkstatus.isOrAfter(FullChunkStatus.BLOCK_TICKING); +- boolean flag5 = fullchunkstatus1.isOrAfter(FullChunkStatus.BLOCK_TICKING); +- +- if (!flag4 && flag5) { +- this.tickingChunkFuture = chunkStorage.prepareTickingChunk(this); +- this.scheduleFullChunkPromotion(chunkStorage, this.tickingChunkFuture, executor, FullChunkStatus.BLOCK_TICKING); +- // Paper start - cache ticking ready status +- this.tickingChunkFuture.thenAccept(chunkResult -> { +- chunkResult.ifSuccess(chunk -> { +- // note: Here is a very good place to add callbacks to logic waiting on this. +- ChunkHolder.this.isTickingReady = true; +- io.papermc.paper.chunk.system.ChunkSystem.onChunkTicking(chunk, this); +- }); +- }); +- // Paper end +- this.updateChunkToSave(this.tickingChunkFuture, "ticking"); +- } +- +- if (flag4 && !flag5) { +- // Paper start +- if (this.isTickingReady) { +- io.papermc.paper.chunk.system.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper +- } +- // Paper end +- this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage +- this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; +- } +- +- boolean flag6 = fullchunkstatus.isOrAfter(FullChunkStatus.ENTITY_TICKING); +- boolean flag7 = fullchunkstatus1.isOrAfter(FullChunkStatus.ENTITY_TICKING); +- +- if (!flag6 && flag7) { +- if (this.entityTickingChunkFuture != ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE) { +- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException()); +- } +- +- this.entityTickingChunkFuture = chunkStorage.prepareEntityTickingChunk(this); +- this.scheduleFullChunkPromotion(chunkStorage, this.entityTickingChunkFuture, executor, FullChunkStatus.ENTITY_TICKING); +- // Paper start - cache ticking ready status +- this.entityTickingChunkFuture.thenAccept(chunkResult -> { +- chunkResult.ifSuccess(chunk -> { +- ChunkHolder.this.isEntityTickingReady = true; +- io.papermc.paper.chunk.system.ChunkSystem.onChunkEntityTicking(chunk, this); +- }); +- }); +- // Paper end +- this.updateChunkToSave(this.entityTickingChunkFuture, "entity ticking"); +- } +- +- if (flag6 && !flag7) { +- // Paper start +- if (this.isEntityTickingReady) { +- io.papermc.paper.chunk.system.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); +- } +- // Paper end +- this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage +- this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; +- } +- +- if (!fullchunkstatus1.isOrAfter(fullchunkstatus)) { +- this.demoteFullChunk(chunkStorage, fullchunkstatus1); +- } +- +- this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel); +- this.oldTicketLevel = this.ticketLevel; +- // CraftBukkit start +- // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins. +- if (!fullchunkstatus.isOrAfter(FullChunkStatus.FULL) && fullchunkstatus1.isOrAfter(FullChunkStatus.FULL)) { +- this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> { +- LevelChunk chunk = (LevelChunk) either.orElse(null); +- if (chunk != null) { +- chunkStorage.callbackExecutor.execute(() -> { +- chunk.loadCallback(); +- }); +- } +- }).exceptionally((throwable) -> { +- // ensure exceptions are printed, by default this is not the case +- MinecraftServer.LOGGER.error("Failed to schedule load callback for chunk " + ChunkHolder.this.pos, throwable); +- return null; +- }); +- +- // Run callback right away if the future was already done +- chunkStorage.callbackExecutor.run(); +- } +- // CraftBukkit end +- } +- +- public boolean wasAccessibleSinceLastSave() { +- return this.wasAccessibleSinceLastSave; ++ return this.newChunkHolder.getTicketLevel(); // Paper - rewrite chunk system + } + +- public void refreshAccessibility() { +- this.wasAccessibleSinceLastSave = ChunkLevel.fullStatus(this.ticketLevel).isOrAfter(FullChunkStatus.FULL); +- } ++ // Paper - rewrite chunk system + + public void replaceProtoChunk(ImposterProtoChunk chunk) { +- for (int i = 0; i < this.futures.length(); ++i) { +- CompletableFuture> completablefuture = (CompletableFuture) this.futures.get(i); +- +- if (completablefuture != null) { +- ChunkAccess ichunkaccess = (ChunkAccess) ((ChunkResult) completablefuture.getNow(ChunkHolder.UNLOADED_CHUNK)).orElse((Object) null); +- +- if (ichunkaccess instanceof ProtoChunk) { +- this.futures.set(i, CompletableFuture.completedFuture(ChunkResult.of(chunk))); +- } +- } +- } +- +- this.updateChunkToSave(CompletableFuture.completedFuture(ChunkResult.of(chunk.getWrapped())), "replaceProto"); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public List>>> getAllFutures() { +- List>>> list = new ArrayList(); +- +- for (int i = 0; i < ChunkHolder.CHUNK_STATUSES.size(); ++i) { +- list.add(Pair.of((ChunkStatus) ChunkHolder.CHUNK_STATUSES.get(i), (CompletableFuture) this.futures.get(i))); +- } +- +- return list; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @FunctionalInterface +@@ -670,15 +395,15 @@ public class ChunkHolder { + + // Paper start + public final boolean isEntityTickingReady() { +- return this.isEntityTickingReady; ++ return this.newChunkHolder.isEntityTickingReady(); // Paper - rewrite chunk system + } + + public final boolean isTickingReady() { +- return this.isTickingReady; ++ return this.newChunkHolder.isTickingReady(); // Paper - rewrite chunk system + } + + public final boolean isFullChunkReady() { +- return this.isFullChunkReady; ++ return this.newChunkHolder.isFullChunkReady(); // Paper - rewrite chunk system + } + // Paper end + } +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd150238fdefe5 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -122,10 +122,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + public static final int MIN_VIEW_DISTANCE = 2; + public static final int MAX_VIEW_DISTANCE = 32; + public static final int FORCED_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING); +- public final Long2ObjectLinkedOpenHashMap updatingChunkMap = new Long2ObjectLinkedOpenHashMap(); +- public volatile Long2ObjectLinkedOpenHashMap visibleChunkMap; +- private final Long2ObjectLinkedOpenHashMap pendingUnloads; +- private final LongSet entitiesInLevel; ++ // Paper - rewrite chunk system + public final ServerLevel level; + private final ThreadedLevelLightEngine lightEngine; + public final BlockableEventLoop mainThreadExecutor; // Paper - public +@@ -134,15 +131,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + private final ChunkGeneratorStructureState chunkGeneratorState; + public final Supplier overworldDataStorage; + private final PoiManager poiManager; +- public final LongSet toDrop; ++ // Paper - rewrite chunk system + private boolean modified; +- private final ChunkTaskPriorityQueueSorter queueSorter; +- private final ProcessorHandle> worldgenMailbox; +- private final ProcessorHandle> mainThreadMailbox; ++ // Paper - rewrite chunk system + public final ChunkProgressListener progressListener; + private final ChunkStatusUpdateListener chunkStatusListener; + public final ChunkMap.ChunkDistanceManager distanceManager; +- private final AtomicInteger tickingGenerated; ++ public final AtomicInteger tickingGenerated; // Paper - public + private final String storageName; + private final PlayerMap playerMap; + public final Int2ObjectMap entityMap; +@@ -150,28 +145,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + private final Long2LongMap chunkSaveCooldowns; + private final Queue unloadQueue; + public int serverViewDistance; +- private WorldGenContext worldGenContext; +- +- // 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 { +- +- private final java.util.Queue queue = new java.util.ArrayDeque<>(); +- +- @Override +- public void execute(Runnable runnable) { +- this.queue.add(runnable); +- } +- +- @Override +- public void run() { +- Runnable task; +- while ((task = this.queue.poll()) != null) { +- task.run(); +- } +- } +- }; +- // CraftBukkit end ++ private WorldGenContext worldGenContext; public final WorldGenContext getWorldGenContext() { return this.worldGenContext; } // Paper - rewrite chunk system + + // Paper start - distance maps + private final com.destroystokyo.paper.util.misc.PooledLinkedHashSets pooledLinkedPlayerHashSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>(); +@@ -181,6 +155,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ()); + // Note: players need to be explicitly added to distance maps before they can be updated + this.nearbyPlayers.addPlayer(player); ++ this.level.playerChunkLoader.addPlayer(player); // Paper - replace chunk loader + } + + void removePlayerFromDistanceMaps(ServerPlayer player) { +@@ -188,6 +163,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ()); + // Note: players need to be explicitly added to distance maps before they can be updated + this.nearbyPlayers.removePlayer(player); ++ this.level.playerChunkLoader.removePlayer(player); // Paper - replace chunk loader + } + + void updateMaps(ServerPlayer player) { +@@ -195,6 +171,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ()); + // Note: players need to be explicitly added to distance maps before they can be updated + this.nearbyPlayers.tickPlayer(player); ++ this.level.playerChunkLoader.updatePlayer(player); // Paper - replace chunk loader + } + // Paper end + // Paper start +@@ -224,17 +201,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) { +- return this.pendingUnloads.get(io.papermc.paper.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); ++ return null; // Paper - rewrite chunk system + } + public final io.papermc.paper.util.player.NearbyPlayers nearbyPlayers; + // Paper end + + public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory, int viewDistance, boolean dsync) { + super(new RegionStorageInfo(session.getLevelId(), world.dimension(), "chunk"), session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync); +- this.visibleChunkMap = this.updatingChunkMap.clone(); +- this.pendingUnloads = new Long2ObjectLinkedOpenHashMap(); +- this.entitiesInLevel = new LongOpenHashSet(); +- this.toDrop = new LongOpenHashSet(); ++ // Paper - rewrite chunk system + this.tickingGenerated = new AtomicInteger(); + this.playerMap = new PlayerMap(); + this.entityMap = new Int2ObjectOpenHashMap(); +@@ -263,19 +237,17 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + this.chunkGeneratorState = chunkGenerator.createState(iregistrycustom.lookupOrThrow(Registries.STRUCTURE_SET), this.randomState, j, world.spigotConfig); // Spigot + this.mainThreadExecutor = mainThreadExecutor; +- ProcessorMailbox threadedmailbox = ProcessorMailbox.create(executor, "worldgen"); ++ // Paper - rewrite chunk system + + Objects.requireNonNull(mainThreadExecutor); +- ProcessorHandle mailbox = ProcessorHandle.of("main", mainThreadExecutor::tell); ++ // Paper - rewrite chunk system + + this.progressListener = worldGenerationProgressListener; + this.chunkStatusListener = chunkStatusChangeListener; +- ProcessorMailbox threadedmailbox1 = ProcessorMailbox.create(executor, "light"); ++ // Paper - rewrite chunk system + +- this.queueSorter = new ChunkTaskPriorityQueueSorter(ImmutableList.of(threadedmailbox, mailbox, threadedmailbox1), executor, Integer.MAX_VALUE); +- this.worldgenMailbox = this.queueSorter.getProcessor(threadedmailbox, false); +- this.mainThreadMailbox = this.queueSorter.getProcessor(mailbox, false); +- this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), threadedmailbox1, this.queueSorter.getProcessor(threadedmailbox1, false)); ++ // Paper - rewrite chunk system ++ this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), null, null); // Paper - rewrite chunk system + this.distanceManager = new ChunkMap.ChunkDistanceManager(executor, mainThreadExecutor); + this.overworldDataStorage = persistentStateManagerFactory; + this.poiManager = new PoiManager(new RegionStorageInfo(session.getLevelId(), world.dimension(), "poi"), path.resolve("poi"), dataFixer, dsync, iregistrycustom, world); +@@ -333,23 +305,15 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + boolean isChunkTracked(ServerPlayer player, int chunkX, int chunkZ) { +- return player.getChunkTrackingView().contains(chunkX, chunkZ) && !player.connection.chunkSender.isPending(ChunkPos.asLong(chunkX, chunkZ)); ++ // Paper start - rewrite player chunk loader ++ return this.level.playerChunkLoader.isChunkSent(player, chunkX, chunkZ); ++ // Paper end - rewrite player chunk loader + } + + private boolean isChunkOnTrackedBorder(ServerPlayer player, int chunkX, int chunkZ) { +- if (!this.isChunkTracked(player, chunkX, chunkZ)) { +- return false; +- } else { +- for (int k = -1; k <= 1; ++k) { +- for (int l = -1; l <= 1; ++l) { +- if ((k != 0 || l != 0) && !this.isChunkTracked(player, chunkX + k, chunkZ + l)) { +- return true; +- } +- } +- } +- +- return false; +- } ++ // Paper start - rewrite player chunk loader ++ return this.level.playerChunkLoader.isChunkSent(player, chunkX, chunkZ, true); ++ // Paper end - rewrite player chunk loader + } + + protected ThreadedLevelLightEngine getLightEngine() { +@@ -358,20 +322,22 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + @Nullable + protected ChunkHolder getUpdatingChunkIfPresent(long pos) { +- return (ChunkHolder) this.updatingChunkMap.get(pos); ++ // Paper start - rewrite chunk system ++ io.papermc.paper.chunk.system.scheduling.NewChunkHolder holder = this.level.chunkTaskScheduler.chunkHolderManager.getChunkHolder(pos); ++ return holder == null ? null : holder.vanillaChunkHolder; ++ // Paper end - rewrite chunk system + } + + @Nullable + public ChunkHolder getVisibleChunkIfPresent(long pos) { +- return (ChunkHolder) this.visibleChunkMap.get(pos); ++ // Paper start - rewrite chunk system ++ io.papermc.paper.chunk.system.scheduling.NewChunkHolder holder = this.level.chunkTaskScheduler.chunkHolderManager.getChunkHolder(pos); ++ return holder == null ? null : holder.vanillaChunkHolder; ++ // Paper end - rewrite chunk system + } + + protected IntSupplier getChunkQueueLevel(long pos) { +- return () -> { +- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); +- +- return playerchunk == null ? ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1 : Math.min(playerchunk.getQueueLevel(), ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1); +- }; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public String getChunkDebugData(ChunkPos chunkPos) { +@@ -400,80 +366,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + private CompletableFuture>> getChunkRangeFuture(ChunkHolder centerChunk, int margin, IntFunction distanceToStatus) { +- if (margin == 0) { +- ChunkStatus chunkstatus = (ChunkStatus) distanceToStatus.apply(0); +- +- return centerChunk.getOrScheduleFuture(chunkstatus, this).thenApply((chunkresult) -> { +- return chunkresult.map(List::of); +- }); +- } else { +- List>> list = new ArrayList(); +- List list1 = new ArrayList(); +- ChunkPos chunkcoordintpair = centerChunk.getPos(); +- int j = chunkcoordintpair.x; +- int k = chunkcoordintpair.z; +- +- for (int l = -margin; l <= margin; ++l) { +- for (int i1 = -margin; i1 <= margin; ++i1) { +- int j1 = Math.max(Math.abs(i1), Math.abs(l)); +- ChunkPos chunkcoordintpair1 = new ChunkPos(j + i1, k + l); +- long k1 = chunkcoordintpair1.toLong(); +- ChunkHolder playerchunk1 = this.getUpdatingChunkIfPresent(k1); +- +- if (playerchunk1 == null) { +- return CompletableFuture.completedFuture(ChunkResult.error(() -> { +- return "Unloaded " + String.valueOf(chunkcoordintpair1); +- })); +- } +- +- ChunkStatus chunkstatus1 = (ChunkStatus) distanceToStatus.apply(j1); +- CompletableFuture> completablefuture = playerchunk1.getOrScheduleFuture(chunkstatus1, this); +- +- list1.add(playerchunk1); +- list.add(completablefuture); +- } +- } +- +- CompletableFuture>> completablefuture1 = Util.sequence(list); +- CompletableFuture>> completablefuture2 = completablefuture1.thenApply((list2) -> { +- List list3 = Lists.newArrayList(); +- // CraftBukkit start - decompile error +- int cnt = 0; +- +- for (Iterator iterator = list2.iterator(); iterator.hasNext(); ++cnt) { +- final int l1 = cnt; +- // CraftBukkit end +- ChunkResult chunkresult = (ChunkResult) iterator.next(); +- +- if (chunkresult == null) { +- throw this.debugFuturesAndCreateReportedException(new IllegalStateException("At least one of the chunk futures were null"), "n/a"); +- } +- +- ChunkAccess ichunkaccess = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error +- +- if (ichunkaccess == null) { +- return ChunkResult.error(() -> { +- String s = String.valueOf(new ChunkPos(j + l1 % (margin * 2 + 1), k + l1 / (margin * 2 + 1))); +- +- return "Unloaded " + s + " " + chunkresult.getError(); +- }); +- } +- +- list3.add(ichunkaccess); +- } +- +- return ChunkResult.of(list3); +- }); +- Iterator iterator = list1.iterator(); +- +- while (iterator.hasNext()) { +- ChunkHolder playerchunk2 = (ChunkHolder) iterator.next(); +- +- playerchunk2.addSaveDependency("getChunkRangeFuture " + String.valueOf(chunkcoordintpair) + " " + margin, completablefuture2); +- } +- +- return completablefuture2; +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public ReportedException debugFuturesAndCreateReportedException(IllegalStateException exception, String details) { +@@ -503,263 +396,72 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + public CompletableFuture> prepareEntityTickingChunk(ChunkHolder chunk) { +- return this.getChunkRangeFuture(chunk, 2, (i) -> { +- return ChunkStatus.FULL; +- }).thenApplyAsync((chunkresult) -> { +- return chunkresult.map((list) -> { +- return (LevelChunk) list.get(list.size() / 2); +- }); +- }, this.mainThreadExecutor); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Nullable + ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k) { +- if (!ChunkLevel.isLoaded(k) && !ChunkLevel.isLoaded(level)) { +- return holder; +- } else { +- if (holder != null) { +- holder.setTicketLevel(level); +- } +- +- if (holder != null) { +- if (!ChunkLevel.isLoaded(level)) { +- this.toDrop.add(pos); +- } else { +- this.toDrop.remove(pos); +- } +- } +- +- if (ChunkLevel.isLoaded(level) && holder == null) { +- holder = (ChunkHolder) this.pendingUnloads.remove(pos); +- if (holder != null) { +- holder.setTicketLevel(level); +- } else { +- holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this.queueSorter, this); +- // Paper start +- io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderCreate(this.level, holder); +- // Paper end +- } +- +- // Paper start +- holder.onChunkAdd(); +- // Paper end +- this.updatingChunkMap.put(pos, holder); +- this.modified = true; +- } +- +- return holder; +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Override + public void close() throws IOException { +- try { +- this.queueSorter.close(); +- this.poiManager.close(); +- } finally { +- super.close(); +- } ++ throw new UnsupportedOperationException("Use ServerChunkCache#close"); // Paper - rewrite chunk system ++ } + ++ // Paper start - rewrite chunk system ++ protected void saveIncrementally() { ++ this.level.chunkTaskScheduler.chunkHolderManager.autoSave(); // Paper - rewrite chunk system + } ++ // Paper end - - rewrite chunk system + + protected void saveAllChunks(boolean flush) { +- if (flush) { +- List list = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList(); // Paper +- MutableBoolean mutableboolean = new MutableBoolean(); +- +- do { +- mutableboolean.setFalse(); +- list.stream().map((playerchunk) -> { +- CompletableFuture completablefuture; +- +- do { +- completablefuture = playerchunk.getChunkToSave(); +- BlockableEventLoop iasynctaskhandler = this.mainThreadExecutor; +- +- Objects.requireNonNull(completablefuture); +- iasynctaskhandler.managedBlock(completablefuture::isDone); +- } while (completablefuture != playerchunk.getChunkToSave()); +- +- return (ChunkAccess) completablefuture.join(); +- }).filter((ichunkaccess) -> { +- return ichunkaccess instanceof ImposterProtoChunk || ichunkaccess instanceof LevelChunk; +- }).filter(this::save).forEach((ichunkaccess) -> { +- mutableboolean.setTrue(); +- }); +- } while (mutableboolean.isTrue()); +- +- this.processUnloads(() -> { +- return true; +- }); +- this.flushWorker(); +- } else { +- io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).forEach(this::saveChunkIfNeeded); +- } +- ++ this.level.chunkTaskScheduler.chunkHolderManager.saveAllChunks(flush, false, false); // Paper - rewrite chunk system + } + + protected void tick(BooleanSupplier shouldKeepTicking) { + ProfilerFiller gameprofilerfiller = this.level.getProfiler(); + ++ try (Timing ignored = this.level.timings.poiUnload.startTiming()) { // Paper + gameprofilerfiller.push("poi"); + this.poiManager.tick(shouldKeepTicking); ++ } // Paper + gameprofilerfiller.popPush("chunk_unload"); + if (!this.level.noSave()) { ++ try (Timing ignored = this.level.timings.chunkUnload.startTiming()) { // Paper + this.processUnloads(shouldKeepTicking); ++ } // Paper + } + + gameprofilerfiller.pop(); + } + + public boolean hasWork() { +- return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || io.papermc.paper.chunk.system.ChunkSystem.hasAnyChunkHolders(this.level) || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets(); // Paper ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private void processUnloads(BooleanSupplier shouldKeepTicking) { +- LongIterator longiterator = this.toDrop.iterator(); +- +- for (int i = 0; longiterator.hasNext() && (shouldKeepTicking.getAsBoolean() || i < 200 || this.toDrop.size() > 2000); longiterator.remove()) { +- long j = longiterator.nextLong(); +- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.remove(j); +- +- if (playerchunk != null) { +- playerchunk.onChunkRemove(); // Paper +- this.pendingUnloads.put(j, playerchunk); +- this.modified = true; +- ++i; +- this.scheduleUnload(j, playerchunk); +- } +- } +- +- int k = Math.max(0, this.unloadQueue.size() - 2000); +- +- Runnable runnable; +- +- while ((shouldKeepTicking.getAsBoolean() || k > 0) && (runnable = (Runnable) this.unloadQueue.poll()) != null) { +- --k; +- runnable.run(); +- } +- +- int l = 0; +- Iterator objectiterator = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper +- +- while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) { +- if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) { +- ++l; +- } +- } ++ this.level.chunkTaskScheduler.chunkHolderManager.processUnloads(); // Paper - rewrite chunk system + + } + + private void scheduleUnload(long pos, ChunkHolder holder) { +- CompletableFuture completablefuture = holder.getChunkToSave(); +- Consumer consumer = (ichunkaccess) -> { // CraftBukkit - decompile error +- CompletableFuture completablefuture1 = holder.getChunkToSave(); +- +- if (completablefuture1 != completablefuture) { +- this.scheduleUnload(pos, holder); +- } else { +- // Paper start +- boolean removed; +- if ((removed = this.pendingUnloads.remove(pos, holder)) && ichunkaccess != null) { +- io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(this.level, holder); +- // Paper end +- if (ichunkaccess instanceof LevelChunk) { +- ((LevelChunk) ichunkaccess).setLoaded(false); +- } +- +- this.save(ichunkaccess); +- if (this.entitiesInLevel.remove(pos) && ichunkaccess instanceof LevelChunk) { +- LevelChunk chunk = (LevelChunk) ichunkaccess; +- +- this.level.unload(chunk); +- } +- +- this.lightEngine.updateChunkStatus(ichunkaccess.getPos()); +- this.lightEngine.tryScheduleUpdate(); +- this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null); +- this.chunkSaveCooldowns.remove(ichunkaccess.getPos().toLong()); +- } else if (removed) { // Paper start +- io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(this.level, holder); +- } // Paper end +- +- } +- }; +- Queue queue = this.unloadQueue; +- +- Objects.requireNonNull(this.unloadQueue); +- completablefuture.thenAcceptAsync(consumer, queue::add).whenComplete((ovoid, throwable) -> { +- if (throwable != null) { +- ChunkMap.LOGGER.error("Failed to save chunk {}", holder.getPos(), throwable); +- } +- +- }); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + protected boolean promoteChunkMap() { +- if (!this.modified) { +- return false; +- } else { +- this.visibleChunkMap = this.updatingChunkMap.clone(); +- this.modified = false; +- return true; +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public CompletableFuture> schedule(ChunkHolder holder, ChunkStatus requiredStatus) { +- ChunkPos chunkcoordintpair = holder.getPos(); +- +- if (requiredStatus == ChunkStatus.EMPTY) { +- return this.scheduleChunkLoad(chunkcoordintpair).thenApply(ChunkResult::of); +- } else { +- if (requiredStatus == ChunkStatus.LIGHT) { +- this.distanceManager.addTicket(TicketType.LIGHT, chunkcoordintpair, ChunkLevel.byStatus(ChunkStatus.LIGHT), chunkcoordintpair); +- } +- +- if (!requiredStatus.hasLoadDependencies()) { +- ChunkAccess ichunkaccess = (ChunkAccess) ((ChunkResult) holder.getOrScheduleFuture(requiredStatus.getParent(), this).getNow(ChunkHolder.UNLOADED_CHUNK)).orElse((Object) null); +- +- if (ichunkaccess != null && ichunkaccess.getStatus().isOrAfter(requiredStatus)) { +- CompletableFuture completablefuture = requiredStatus.load(this.worldGenContext, (ichunkaccess1) -> { +- return this.protoChunkToFullChunk(holder, ichunkaccess1); +- }, ichunkaccess); +- +- this.progressListener.onStatusChange(chunkcoordintpair, requiredStatus); +- return completablefuture.thenApply(ChunkResult::of); +- } +- } +- +- return this.scheduleChunkGeneration(holder, requiredStatus); +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private CompletableFuture scheduleChunkLoad(ChunkPos pos) { +- return this.readChunk(pos).thenApply((optional) -> { +- return optional.filter((nbttagcompound) -> { +- boolean flag = ChunkMap.isChunkDataValid(nbttagcompound); +- +- if (!flag) { +- ChunkMap.LOGGER.error("Chunk file at {} is missing level data, skipping", pos); +- } +- +- return flag; +- }); +- }).thenApplyAsync((optional) -> { +- this.level.getProfiler().incrementCounter("chunkLoad"); +- if (optional.isPresent()) { +- ProtoChunk protochunk = ChunkSerializer.read(this.level, this.poiManager, pos, (CompoundTag) optional.get()); +- +- this.markPosition(pos, protochunk.getStatus().getChunkType()); +- return protochunk; +- } else { +- return this.createEmptyChunk(pos); +- } +- }, this.mainThreadExecutor).exceptionallyAsync((throwable) -> { +- return this.handleChunkLoadFailure(throwable, pos); +- }, this.mainThreadExecutor); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + +- private static boolean isChunkDataValid(CompoundTag nbt) { ++ public static boolean isChunkDataValid(CompoundTag nbt) { // Paper - async chunk loading + return nbt.contains("Status", 8); + } + +@@ -816,60 +518,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + private CompletableFuture> scheduleChunkGeneration(ChunkHolder holder, ChunkStatus requiredStatus) { +- ChunkPos chunkcoordintpair = holder.getPos(); +- CompletableFuture>> completablefuture = this.getChunkRangeFuture(holder, requiredStatus.getRange(), (i) -> { +- return this.getDependencyStatus(requiredStatus, i); +- }); +- +- this.level.getProfiler().incrementCounter(() -> { +- return "chunkGenerate " + String.valueOf(requiredStatus); +- }); +- Executor executor = (runnable) -> { +- this.worldgenMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable)); +- }; +- +- return completablefuture.thenComposeAsync((chunkresult) -> { +- List list = (List) chunkresult.orElse(null); // CraftBukkit - decompile error +- +- if (list == null) { +- this.releaseLightTicket(chunkcoordintpair); +- Objects.requireNonNull(chunkresult); +- return CompletableFuture.completedFuture(ChunkResult.error(chunkresult::getError)); +- } else { +- try { +- ChunkAccess ichunkaccess = (ChunkAccess) list.get(list.size() / 2); +- CompletableFuture completablefuture1; +- +- if (ichunkaccess.getStatus().isOrAfter(requiredStatus)) { +- completablefuture1 = requiredStatus.load(this.worldGenContext, (ichunkaccess1) -> { +- return this.protoChunkToFullChunk(holder, ichunkaccess1); +- }, ichunkaccess); +- } else { +- completablefuture1 = requiredStatus.generate(this.worldGenContext, executor, (ichunkaccess1) -> { +- return this.protoChunkToFullChunk(holder, ichunkaccess1); +- }, list); +- } +- +- this.progressListener.onStatusChange(chunkcoordintpair, requiredStatus); +- return completablefuture1.thenApply(ChunkResult::of); +- } catch (Exception exception) { +- exception.getStackTrace(); +- CrashReport crashreport = CrashReport.forThrowable(exception, "Exception generating new chunk"); +- CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk to be generated"); +- +- crashreportsystemdetails.setDetail("Status being generated", () -> { +- return BuiltInRegistries.CHUNK_STATUS.getKey(requiredStatus).toString(); +- }); +- crashreportsystemdetails.setDetail("Location", (Object) String.format(Locale.ROOT, "%d,%d", chunkcoordintpair.x, chunkcoordintpair.z)); +- crashreportsystemdetails.setDetail("Position hash", (Object) ChunkPos.asLong(chunkcoordintpair.x, chunkcoordintpair.z)); +- crashreportsystemdetails.setDetail("Generator", (Object) this.generator); +- this.mainThreadExecutor.execute(() -> { +- throw new ReportedException(crashreport); +- }); +- throw new ReportedException(crashreport); +- } +- } +- }, executor); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + protected void releaseLightTicket(ChunkPos pos) { +@@ -880,7 +529,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + })); + } + +- private ChunkStatus getDependencyStatus(ChunkStatus centerChunkTargetStatus, int distance) { ++ public static ChunkStatus getDependencyStatus(ChunkStatus centerChunkTargetStatus, int distance) { // Paper -> public, static + ChunkStatus chunkstatus1; + + if (distance == 0) { +@@ -892,7 +541,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + return chunkstatus1; + } + +- private static void postLoadProtoChunk(ServerLevel world, List nbt) { ++ public static void postLoadProtoChunk(ServerLevel world, List nbt, ChunkPos position) { // Paper - public and add chunk position parameter + if (!nbt.isEmpty()) { + // CraftBukkit start - these are spawned serialized (DefinedStructure) and we don't call an add event below at the moment due to ordering complexities + world.addWorldGenChunkEntities(EntityType.loadEntitiesRecursive(nbt, world).filter((entity) -> { +@@ -908,45 +557,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + checkDupeUUID(world, entity); // Paper - duplicate uuid resolving + return !needsRemoval; +- })); ++ }), position); // Paper - rewrite chunk system + // CraftBukkit end + } + + } + + private CompletableFuture protoChunkToFullChunk(ChunkHolder playerchunk, ChunkAccess ichunkaccess) { +- return CompletableFuture.supplyAsync(() -> { +- ChunkPos chunkcoordintpair = playerchunk.getPos(); +- ProtoChunk protochunk = (ProtoChunk) ichunkaccess; +- LevelChunk chunk; +- +- if (protochunk instanceof ImposterProtoChunk) { +- chunk = ((ImposterProtoChunk) protochunk).getWrapped(); +- } else { +- chunk = new LevelChunk(this.level, protochunk, (chunk1) -> { +- ChunkMap.postLoadProtoChunk(this.level, protochunk.getEntities()); +- }); +- playerchunk.replaceProtoChunk(new ImposterProtoChunk(chunk, false)); +- } +- +- chunk.setFullStatus(() -> { +- return ChunkLevel.fullStatus(playerchunk.getTicketLevel()); +- }); +- chunk.runPostLoad(); +- if (this.entitiesInLevel.add(chunkcoordintpair.toLong())) { +- chunk.setLoaded(true); +- chunk.registerAllBlockEntitiesAfterLevelLoad(); +- chunk.registerTickContainerInLevel(this.level); +- } +- +- return chunk; +- }, (runnable) -> { +- ProcessorHandle mailbox = this.mainThreadMailbox; +- long i = playerchunk.getPos().toLong(); +- +- Objects.requireNonNull(playerchunk); +- mailbox.tell(ChunkTaskPriorityQueueSorter.message(runnable, i, playerchunk::getTicketLevel)); +- }); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + // Paper start - duplicate uuid resolving +@@ -990,61 +608,16 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // Paper end - duplicate uuid resolving + + public CompletableFuture> prepareTickingChunk(ChunkHolder holder) { +- CompletableFuture>> completablefuture = this.getChunkRangeFuture(holder, 1, (i) -> { +- return ChunkStatus.FULL; +- }); +- CompletableFuture> completablefuture1 = completablefuture.thenApplyAsync((chunkresult) -> { +- return chunkresult.map((list) -> { +- return (LevelChunk) list.get(list.size() / 2); +- }); +- }, (runnable) -> { +- this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable)); +- }).thenApplyAsync((chunkresult) -> { +- return chunkresult.ifSuccess((chunk) -> { +- chunk.postProcessGeneration(); +- this.level.startTickingChunk(chunk); +- CompletableFuture completablefuture2 = holder.getChunkSendSyncFuture(); +- +- if (completablefuture2.isDone()) { +- this.onChunkReadyToSend(chunk); +- } else { +- completablefuture2.thenAcceptAsync((object) -> { +- this.onChunkReadyToSend(chunk); +- }, this.mainThreadExecutor); +- } +- +- }); +- }, this.mainThreadExecutor); +- +- completablefuture1.handle((chunkresult, throwable) -> { +- this.tickingGenerated.getAndIncrement(); +- return null; +- }); +- return completablefuture1; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private void onChunkReadyToSend(LevelChunk chunk) { +- ChunkPos chunkcoordintpair = chunk.getPos(); +- Iterator iterator = this.playerMap.getAllPlayers().iterator(); +- +- while (iterator.hasNext()) { +- ServerPlayer entityplayer = (ServerPlayer) iterator.next(); +- +- if (entityplayer.getChunkTrackingView().contains(chunkcoordintpair)) { +- ChunkMap.markChunkPendingToSend(entityplayer, chunk); +- } +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite player chunk loader + + } + + public CompletableFuture> prepareAccessibleChunk(ChunkHolder holder) { +- return this.getChunkRangeFuture(holder, 1, ChunkStatus::getStatusAroundFullChunk).thenApplyAsync((chunkresult) -> { +- return chunkresult.map((list) -> { +- return (LevelChunk) list.get(list.size() / 2); +- }); +- }, (runnable) -> { +- this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable)); +- }); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public int getTickingGenerated() { +@@ -1052,96 +625,15 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + private boolean saveChunkIfNeeded(ChunkHolder chunkHolder) { +- if (!chunkHolder.wasAccessibleSinceLastSave()) { +- return false; +- } else { +- ChunkAccess ichunkaccess = (ChunkAccess) chunkHolder.getChunkToSave().getNow(null); // CraftBukkit - decompile error +- +- if (!(ichunkaccess instanceof ImposterProtoChunk) && !(ichunkaccess instanceof LevelChunk)) { +- return false; +- } else { +- long i = ichunkaccess.getPos().toLong(); +- long j = this.chunkSaveCooldowns.getOrDefault(i, -1L); +- long k = System.currentTimeMillis(); +- +- if (k < j) { +- return false; +- } else { +- boolean flag = this.save(ichunkaccess); +- +- chunkHolder.refreshAccessibility(); +- if (flag) { +- this.chunkSaveCooldowns.put(i, k + 10000L); +- } +- +- return flag; +- } +- } +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public boolean save(ChunkAccess chunk) { +- this.poiManager.flush(chunk.getPos()); +- if (!chunk.isUnsaved()) { +- return false; +- } else { +- chunk.setUnsaved(false); +- ChunkPos chunkcoordintpair = chunk.getPos(); +- +- try { +- ChunkStatus chunkstatus = chunk.getStatus(); +- +- if (chunkstatus.getChunkType() != ChunkType.LEVELCHUNK) { +- if (this.isExistingChunkFull(chunkcoordintpair)) { +- return false; +- } +- +- if (chunkstatus == ChunkStatus.EMPTY && chunk.getAllStarts().values().stream().noneMatch(StructureStart::isValid)) { +- return false; +- } +- } +- +- this.level.getProfiler().incrementCounter("chunkSave"); +- CompoundTag nbttagcompound = ChunkSerializer.write(this.level, chunk); +- +- this.write(chunkcoordintpair, nbttagcompound).exceptionallyAsync((throwable) -> { +- this.level.getServer().reportChunkSaveFailure(chunkcoordintpair); +- return null; +- }, this.mainThreadExecutor); +- this.markPosition(chunkcoordintpair, chunkstatus.getChunkType()); +- return true; +- } catch (Exception exception) { +- ChunkMap.LOGGER.error("Failed to save chunk {},{}", new Object[]{chunkcoordintpair.x, chunkcoordintpair.z, exception}); +- this.level.getServer().reportChunkSaveFailure(chunkcoordintpair); +- return false; +- } +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private boolean isExistingChunkFull(ChunkPos pos) { +- byte b0 = this.chunkTypeCache.get(pos.toLong()); +- +- if (b0 != 0) { +- return b0 == 1; +- } else { +- CompoundTag nbttagcompound; +- +- try { +- nbttagcompound = (CompoundTag) ((Optional) this.readChunk(pos).join()).orElse((Object) null); +- if (nbttagcompound == null) { +- this.markPositionReplaceable(pos); +- return false; +- } +- } catch (Exception exception) { +- ChunkMap.LOGGER.error("Failed to read chunk {}", pos, exception); +- this.markPositionReplaceable(pos); +- return false; +- } +- +- ChunkType chunktype = ChunkSerializer.getChunkTypeFromTag(nbttagcompound); +- +- return this.markPosition(pos, chunktype) == 1; +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public void setServerViewDistance(int watchDistance) { // Paper - public +@@ -1149,37 +641,36 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + if (j != this.serverViewDistance) { + this.serverViewDistance = j; +- this.distanceManager.updatePlayerTickets(this.serverViewDistance); +- Iterator iterator = this.playerMap.getAllPlayers().iterator(); ++ this.level.playerChunkLoader.setLoadDistance(this.serverViewDistance + 1); // Paper - replace player loader system ++ } + +- while (iterator.hasNext()) { +- ServerPlayer entityplayer = (ServerPlayer) iterator.next(); ++ } + +- this.updateChunkTracking(entityplayer); +- } +- } ++ // Paper start - replace player loader system ++ public void setTickViewDistance(int distance) { ++ this.level.playerChunkLoader.setTickDistance(distance); ++ } + ++ public void setSendViewDistance(int distance) { ++ this.level.playerChunkLoader.setSendDistance(distance); + } ++ // Paper end - replace player loader system + + public int getPlayerViewDistance(ServerPlayer player) { // Paper - public +- return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance); ++ return io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(player); // Paper - per player view distance + } + + private void markChunkPendingToSend(ServerPlayer player, ChunkPos pos) { +- LevelChunk chunk = this.getChunkToSend(pos.toLong()); +- +- if (chunk != null) { +- ChunkMap.markChunkPendingToSend(player, chunk); +- } ++ throw new UnsupportedOperationException(); // Paper - per player view distance + + } + + private static void markChunkPendingToSend(ServerPlayer player, LevelChunk chunk) { +- player.connection.chunkSender.markChunkPendingToSend(chunk); ++ throw new UnsupportedOperationException(); // Paper - rewrite player chunk loader + } + + private static void dropChunk(ServerPlayer player, ChunkPos pos) { +- player.connection.chunkSender.dropChunk(player, pos); ++ // Paper - rewrite player chunk loader + } + + @Nullable +@@ -1202,30 +693,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + void dumpChunks(Writer writer) throws IOException { +- CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("z").addColumn("level").addColumn("in_memory").addColumn("status").addColumn("full_status").addColumn("accessible_ready").addColumn("ticking_ready").addColumn("entity_ticking_ready").addColumn("ticket").addColumn("spawning").addColumn("block_entity_count").addColumn("ticking_ticket").addColumn("ticking_level").addColumn("block_ticks").addColumn("fluid_ticks").build(writer); +- TickingTracker tickingtracker = this.distanceManager.tickingTracker(); +- Iterator objectbidirectionaliterator = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper +- +- while (objectbidirectionaliterator.hasNext()) { +- ChunkHolder playerchunk = objectbidirectionaliterator.next(); // Paper +- long i = playerchunk.pos.toLong(); // Paper +- ChunkPos chunkcoordintpair = new ChunkPos(i); +- // Paper +- Optional optional = Optional.ofNullable(playerchunk.getLastAvailable()); +- Optional optional1 = optional.flatMap((ichunkaccess) -> { +- return ichunkaccess instanceof LevelChunk ? Optional.of((LevelChunk) ichunkaccess) : Optional.empty(); +- }); +- +- // CraftBukkit - decompile error +- csvwriter.writeRow(chunkcoordintpair.x, chunkcoordintpair.z, playerchunk.getTicketLevel(), optional.isPresent(), optional.map(ChunkAccess::getStatus).orElse(null), optional1.map(LevelChunk::getFullStatus).orElse(null), ChunkMap.printFuture(playerchunk.getFullChunkFuture()), ChunkMap.printFuture(playerchunk.getTickingChunkFuture()), ChunkMap.printFuture(playerchunk.getEntityTickingChunkFuture()), this.distanceManager.getTicketDebugString(i), this.anyPlayerCloseEnoughForSpawning(chunkcoordintpair), optional1.map((chunk) -> { +- return chunk.getBlockEntities().size(); +- }).orElse(0), tickingtracker.getTicketDebugString(i), tickingtracker.getLevel(i), optional1.map((chunk) -> { +- return chunk.getBlockTicks().count(); +- }).orElse(0), optional1.map((chunk) -> { +- return chunk.getFluidTicks().count(); +- }).orElse(0)); +- } +- ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private static String printFuture(CompletableFuture> future) { +@@ -1240,6 +708,32 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + } + ++ // Paper start - Asynchronous chunk io ++ @Nullable ++ @Override ++ public CompoundTag readSync(ChunkPos chunkcoordintpair) throws IOException { ++ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) { ++ return io.papermc.paper.chunk.system.io.RegionFileIOThread.loadData( ++ this.level, chunkcoordintpair.x, chunkcoordintpair.z, io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA, ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread() ++ ); ++ } ++ return super.readSync(chunkcoordintpair); ++ } ++ ++ @Override ++ public CompletableFuture write(ChunkPos chunkcoordintpair, CompoundTag nbttagcompound) throws IOException { ++ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) { ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.scheduleSave( ++ this.level, chunkcoordintpair.x, chunkcoordintpair.z, nbttagcompound, ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA); ++ return null; ++ } ++ super.write(chunkcoordintpair, nbttagcompound); ++ return null; ++ } ++ // Paper end ++ + private CompletableFuture> readChunk(ChunkPos chunkPos) { + return this.read(chunkPos).thenApplyAsync((optional) -> { + return optional.map((nbttagcompound) -> this.upgradeChunkTag(nbttagcompound, chunkPos)); // CraftBukkit +@@ -1340,8 +834,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.distanceManager.addPlayer(SectionPos.of((EntityAccess) player), player); + } + +- player.setChunkTrackingView(ChunkTrackingView.EMPTY); +- this.updateChunkTracking(player); ++ // Paper - handled by player chunk loader + this.addPlayerToDistanceMaps(player); // Paper - distance maps + } else { + SectionPos sectionposition = player.getLastSectionPos(); +@@ -1352,7 +845,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + this.removePlayerFromDistanceMaps(player); // Paper - distance maps +- this.applyChunkTrackingView(player, ChunkTrackingView.EMPTY); ++ // Paper - handled by player chunk loader + } + + } +@@ -1400,71 +893,30 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.playerMap.unIgnorePlayer(player); + } + +- this.updateChunkTracking(player); ++ // Paper - replaced by PlayerChunkLoader + } + + this.updateMaps(player); // Paper - distance maps + } + + private void updateChunkTracking(ServerPlayer player) { +- ChunkPos chunkcoordintpair = player.chunkPosition(); +- int i = this.getPlayerViewDistance(player); +- ChunkTrackingView chunktrackingview = player.getChunkTrackingView(); +- +- if (chunktrackingview instanceof ChunkTrackingView.Positioned chunktrackingview_a) { +- if (chunktrackingview_a.center().equals(chunkcoordintpair) && chunktrackingview_a.viewDistance() == i) { +- return; +- } +- } +- +- this.applyChunkTrackingView(player, ChunkTrackingView.of(chunkcoordintpair, i)); ++ throw new UnsupportedOperationException(); // Paper - replaced by PlayerChunkLoader + } + + private void applyChunkTrackingView(ServerPlayer player, ChunkTrackingView chunkFilter) { +- if (player.level() == this.level) { +- ChunkTrackingView chunktrackingview1 = player.getChunkTrackingView(); +- +- if (chunkFilter instanceof ChunkTrackingView.Positioned) { +- label15: +- { +- ChunkTrackingView.Positioned chunktrackingview_a = (ChunkTrackingView.Positioned) chunkFilter; +- +- if (chunktrackingview1 instanceof ChunkTrackingView.Positioned) { +- ChunkTrackingView.Positioned chunktrackingview_a1 = (ChunkTrackingView.Positioned) chunktrackingview1; +- +- if (chunktrackingview_a1.center().equals(chunktrackingview_a.center())) { +- break label15; +- } +- } +- +- player.connection.send(new ClientboundSetChunkCacheCenterPacket(chunktrackingview_a.center().x, chunktrackingview_a.center().z)); +- } +- } +- +- ChunkTrackingView.difference(chunktrackingview1, chunkFilter, (chunkcoordintpair) -> { +- this.markChunkPendingToSend(player, chunkcoordintpair); +- }, (chunkcoordintpair) -> { +- ChunkMap.dropChunk(player, chunkcoordintpair); +- }); +- player.setChunkTrackingView(chunkFilter); +- } ++ throw new UnsupportedOperationException(); // Paper - replaced by PlayerChunkLoader + } + + @Override + public List getPlayers(ChunkPos chunkPos, boolean onlyOnWatchDistanceEdge) { +- Set set = this.playerMap.getAllPlayers(); +- Builder builder = ImmutableList.builder(); +- Iterator iterator = set.iterator(); +- +- while (iterator.hasNext()) { +- ServerPlayer entityplayer = (ServerPlayer) iterator.next(); +- +- if (onlyOnWatchDistanceEdge && this.isChunkOnTrackedBorder(entityplayer, chunkPos.x, chunkPos.z) || !onlyOnWatchDistanceEdge && this.isChunkTracked(entityplayer, chunkPos.x, chunkPos.z)) { +- builder.add(entityplayer); +- } ++ // Paper start - per player view distance ++ ChunkHolder holder = this.getVisibleChunkIfPresent(chunkPos.toLong()); ++ if (holder == null) { ++ return new java.util.ArrayList<>(); ++ } else { ++ return holder.getPlayers(onlyOnWatchDistanceEdge); + } +- +- return builder.build(); ++ // Paper end - per player view distance + } + + public void addEntity(Entity entity) { +@@ -1535,13 +987,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + protected void tick() { +- Iterator iterator = this.playerMap.getAllPlayers().iterator(); +- +- while (iterator.hasNext()) { +- ServerPlayer entityplayer = (ServerPlayer) iterator.next(); +- +- this.updateChunkTracking(entityplayer); +- } ++ // Paper - replaced by PlayerChunkLoader + + List list = Lists.newArrayList(); + List list1 = this.level.players(); +@@ -1648,16 +1094,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + public void waitForLightBeforeSending(ChunkPos centerPos, int radius) { +- int j = radius + 1; +- +- ChunkPos.rangeClosed(centerPos, j).forEach((chunkcoordintpair1) -> { +- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(chunkcoordintpair1.toLong()); +- +- if (playerchunk != null) { +- playerchunk.addSendDependency(this.lightEngine.waitForPendingTasks(chunkcoordintpair1.x, chunkcoordintpair1.z)); +- } +- +- }); ++ // Paper - rewrite player chunk loader + } + + public class ChunkDistanceManager extends DistanceManager { // Paper - public +@@ -1668,7 +1105,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + @Override + protected boolean isChunkToRemove(long pos) { +- return ChunkMap.this.toDrop.contains(pos); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Nullable +diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java +index 7a48ae2ba962ff56d0abff581b51f28b48bd9aae..ed5154e41ca858f4d6b4d1c276c66831c038d2a6 100644 +--- a/src/main/java/net/minecraft/server/level/DistanceManager.java ++++ b/src/main/java/net/minecraft/server/level/DistanceManager.java +@@ -38,65 +38,28 @@ import org.slf4j.Logger; + + public abstract class DistanceManager { + ++ // Paper start - rewrite chunk system ++ public io.papermc.paper.chunk.system.scheduling.ChunkHolderManager getChunkHolderManager() { ++ return this.chunkMap.level.chunkTaskScheduler.chunkHolderManager; ++ } ++ // Paper end - rewrite chunk system ++ + static final Logger LOGGER = LogUtils.getLogger(); + static final int PLAYER_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING); + private static final int INITIAL_TICKET_LIST_CAPACITY = 4; + final Long2ObjectMap> playersPerChunk = new Long2ObjectOpenHashMap(); +- public final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap(); +- private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker(); ++ // Paper - rewrite chunk system + private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8); +- private final TickingTracker tickingTicketsTracker = new TickingTracker(); +- private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(32); +- final Set chunksToUpdateFutures = Sets.newHashSet(); +- final ChunkTaskPriorityQueueSorter ticketThrottler; +- final ProcessorHandle> ticketThrottlerInput; +- final ProcessorHandle ticketThrottlerReleaser; +- final LongSet ticketsToRelease = new LongOpenHashSet(); +- final Executor mainThreadExecutor; +- private long ticketTickCounter; +- public int simulationDistance = 10; ++ // Paper - rewrite chunk system + private final ChunkMap chunkMap; // Paper + + protected DistanceManager(Executor workerExecutor, Executor mainThreadExecutor, ChunkMap chunkMap) { +- Objects.requireNonNull(mainThreadExecutor); +- ProcessorHandle mailbox = ProcessorHandle.of("player ticket throttler", mainThreadExecutor::execute); +- ChunkTaskPriorityQueueSorter chunktaskqueuesorter = new ChunkTaskPriorityQueueSorter(ImmutableList.of(mailbox), workerExecutor, 4); +- +- this.ticketThrottler = chunktaskqueuesorter; +- this.ticketThrottlerInput = chunktaskqueuesorter.getProcessor(mailbox, true); +- this.ticketThrottlerReleaser = chunktaskqueuesorter.getReleaseProcessor(mailbox); +- this.mainThreadExecutor = mainThreadExecutor; ++ // Paper - rewrite chunk system + this.chunkMap = chunkMap; // Paper + } + + protected void purgeStaleTickets() { +- ++this.ticketTickCounter; +- ObjectIterator>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); +- +- while (objectiterator.hasNext()) { +- Entry>> entry = (Entry) objectiterator.next(); +- Iterator> iterator = ((SortedArraySet) entry.getValue()).iterator(); +- boolean flag = false; +- +- while (iterator.hasNext()) { +- Ticket ticket = (Ticket) iterator.next(); +- +- if (ticket.timedOut(this.ticketTickCounter)) { +- iterator.remove(); +- flag = true; +- this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket); +- } +- } +- +- if (flag) { +- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false); +- } +- +- if (((SortedArraySet) entry.getValue()).isEmpty()) { +- objectiterator.remove(); +- } +- } +- ++ this.getChunkHolderManager().tick(); // Paper - rewrite chunk system + } + + private static int getTicketLevelAt(SortedArraySet> tickets) { +@@ -112,108 +75,25 @@ public abstract class DistanceManager { + protected abstract ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k); + + public boolean runAllUpdates(ChunkMap chunkStorage) { +- this.naturalSpawnChunkCounter.runAllUpdates(); +- this.tickingTicketsTracker.runAllUpdates(); +- this.playerTicketManager.runAllUpdates(); +- int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE); +- boolean flag = i != 0; +- +- if (flag) { +- ; +- } +- +- if (!this.chunksToUpdateFutures.isEmpty()) { +- // CraftBukkit start +- // Iterate pending chunk updates with protection against concurrent modification exceptions +- java.util.Iterator iter = this.chunksToUpdateFutures.iterator(); +- int expectedSize = this.chunksToUpdateFutures.size(); +- do { +- ChunkHolder playerchunk = iter.next(); +- iter.remove(); +- expectedSize--; +- +- playerchunk.updateFutures(chunkStorage, this.mainThreadExecutor); +- +- // Reset iterator if set was modified using add() +- if (this.chunksToUpdateFutures.size() != expectedSize) { +- expectedSize = this.chunksToUpdateFutures.size(); +- iter = this.chunksToUpdateFutures.iterator(); +- } +- } while (iter.hasNext()); +- // CraftBukkit end +- +- return true; +- } else { +- if (!this.ticketsToRelease.isEmpty()) { +- LongIterator longiterator = this.ticketsToRelease.iterator(); +- +- while (longiterator.hasNext()) { +- long j = longiterator.nextLong(); +- +- if (this.getTickets(j).stream().anyMatch((ticket) -> { +- return ticket.getType() == TicketType.PLAYER; +- })) { +- ChunkHolder playerchunk = chunkStorage.getUpdatingChunkIfPresent(j); +- +- if (playerchunk == null) { +- throw new IllegalStateException(); +- } +- +- CompletableFuture> completablefuture = playerchunk.getEntityTickingChunkFuture(); +- +- completablefuture.thenAccept((chunkresult) -> { +- this.mainThreadExecutor.execute(() -> { +- this.ticketThrottlerReleaser.tell(ChunkTaskPriorityQueueSorter.release(() -> { +- }, j, false)); +- }); +- }); +- } +- } +- +- this.ticketsToRelease.clear(); +- } +- +- return flag; +- } ++ return this.getChunkHolderManager().processTicketUpdates(); // Paper - rewrite chunk system + } + + boolean addTicket(long i, Ticket ticket) { // CraftBukkit - void -> boolean +- SortedArraySet> arraysetsorted = this.getTickets(i); +- int j = DistanceManager.getTicketLevelAt(arraysetsorted); +- Ticket ticket1 = (Ticket) arraysetsorted.addOrGet(ticket); +- +- ticket1.setCreatedTick(this.ticketTickCounter); +- if (ticket.getTicketLevel() < j) { +- this.ticketTracker.update(i, ticket.getTicketLevel(), true); +- } +- +- return ticket == ticket1; // CraftBukkit ++ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::addTicket"); // Paper ++ return this.getChunkHolderManager().addTicketAtLevel((TicketType)ticket.getType(), i, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system + } + + boolean removeTicket(long i, Ticket ticket) { // CraftBukkit - void -> boolean +- SortedArraySet> arraysetsorted = this.getTickets(i); +- +- boolean removed = false; // CraftBukkit +- if (arraysetsorted.remove(ticket)) { +- removed = true; // CraftBukkit +- } +- +- if (arraysetsorted.isEmpty()) { +- this.tickets.remove(i); +- } +- +- this.ticketTracker.update(i, DistanceManager.getTicketLevelAt(arraysetsorted), false); +- return removed; // CraftBukkit ++ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::removeTicket"); // Paper ++ return this.getChunkHolderManager().removeTicketAtLevel((TicketType)ticket.getType(), i, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system + } + + public void addTicket(TicketType type, ChunkPos pos, int level, T argument) { +- this.addTicket(pos.toLong(), new Ticket<>(type, level, argument)); ++ this.getChunkHolderManager().addTicketAtLevel(type, pos, level, argument); // Paper - rewrite chunk system + } + + public void removeTicket(TicketType type, ChunkPos pos, int level, T argument) { +- Ticket ticket = new Ticket<>(type, level, argument); +- +- this.removeTicket(pos.toLong(), ticket); ++ this.getChunkHolderManager().removeTicketAtLevel(type, pos, level, argument); // Paper - rewrite chunk system + } + + public void addRegionTicket(TicketType type, ChunkPos pos, int radius, T argument) { +@@ -222,13 +102,7 @@ public abstract class DistanceManager { + } + + public boolean addRegionTicketAtDistance(TicketType tickettype, ChunkPos chunkcoordintpair, int i, T t0) { +- // CraftBukkit end +- Ticket ticket = new Ticket<>(tickettype, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); +- long j = chunkcoordintpair.toLong(); +- +- boolean added = this.addTicket(j, ticket); // CraftBukkit +- this.tickingTicketsTracker.addTicket(j, ticket); +- return added; // CraftBukkit ++ return this.getChunkHolderManager().addTicketAtLevel(tickettype, chunkcoordintpair, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); // Paper - rewrite chunk system + } + + public void removeRegionTicket(TicketType type, ChunkPos pos, int radius, T argument) { +@@ -237,31 +111,21 @@ public abstract class DistanceManager { + } + + public boolean removeRegionTicketAtDistance(TicketType tickettype, ChunkPos chunkcoordintpair, int i, T t0) { +- // CraftBukkit end +- Ticket ticket = new Ticket<>(tickettype, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); +- long j = chunkcoordintpair.toLong(); +- +- boolean removed = this.removeTicket(j, ticket); // CraftBukkit +- this.tickingTicketsTracker.removeTicket(j, ticket); +- return removed; // CraftBukkit ++ return this.getChunkHolderManager().removeTicketAtLevel(tickettype, chunkcoordintpair, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); // Paper - rewrite chunk system + } + +- private SortedArraySet> getTickets(long position) { +- return (SortedArraySet) this.tickets.computeIfAbsent(position, (j) -> { +- return SortedArraySet.create(4); +- }); +- } ++ // Paper - rewrite chunk system + + protected void updateChunkForced(ChunkPos pos, boolean forced) { +- Ticket ticket = new Ticket<>(TicketType.FORCED, ChunkMap.FORCED_TICKET_LEVEL, pos); ++ Ticket ticket = new Ticket<>(TicketType.FORCED, ChunkMap.FORCED_TICKET_LEVEL, pos, 0L); // Paper - rewrite chunk system + long i = pos.toLong(); + + if (forced) { + this.addTicket(i, ticket); +- this.tickingTicketsTracker.addTicket(i, ticket); ++ //this.tickingTicketsTracker.addTicket(i, ticket); // Paper - no longer used + } else { + this.removeTicket(i, ticket); +- this.tickingTicketsTracker.removeTicket(i, ticket); ++ //this.tickingTicketsTracker.removeTicket(i, ticket); // Paper - no longer used + } + + } +@@ -270,12 +134,10 @@ public abstract class DistanceManager { + ChunkPos chunkcoordintpair = pos.chunk(); + long i = chunkcoordintpair.toLong(); + +- ((ObjectSet) this.playersPerChunk.computeIfAbsent(i, (j) -> { +- return new ObjectOpenHashSet(); +- })).add(player); ++ // Paper - no longer used + this.naturalSpawnChunkCounter.update(i, 0, true); +- this.playerTicketManager.update(i, 0, true); +- this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); ++ //this.playerTicketManager.update(i, 0, true); // Paper - no longer used ++ //this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used + } + + public void removePlayer(SectionPos pos, ServerPlayer player) { +@@ -288,40 +150,44 @@ public abstract class DistanceManager { + if (objectset == null || objectset.isEmpty()) { // Paper + this.playersPerChunk.remove(i); + this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false); +- this.playerTicketManager.update(i, Integer.MAX_VALUE, false); +- this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); ++ //this.playerTicketManager.update(i, Integer.MAX_VALUE, false); // Paper - no longer used ++ //this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used + } + + } + +- private int getPlayerTicketLevel() { +- return Math.max(0, ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING) - this.simulationDistance); +- } ++ // Paper - rewrite chunk system + + public boolean inEntityTickingRange(long chunkPos) { +- return ChunkLevel.isEntityTicking(this.tickingTicketsTracker.getLevel(chunkPos)); ++ // Paper start - replace player chunk loader system ++ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(chunkPos); ++ return holder != null && holder.isEntityTickingReady(); ++ // Paper end - replace player chunk loader system + } + + public boolean inBlockTickingRange(long chunkPos) { +- return ChunkLevel.isBlockTicking(this.tickingTicketsTracker.getLevel(chunkPos)); ++ // Paper start - replace player chunk loader system ++ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(chunkPos); ++ return holder != null && holder.isTickingReady(); ++ // Paper end - replace player chunk loader system + } + + protected String getTicketDebugString(long pos) { +- SortedArraySet> arraysetsorted = (SortedArraySet) this.tickets.get(pos); +- +- return arraysetsorted != null && !arraysetsorted.isEmpty() ? ((Ticket) arraysetsorted.first()).toString() : "no_ticket"; ++ return this.getChunkHolderManager().getTicketDebugString(pos); // Paper - rewrite chunk system + } + + protected void updatePlayerTickets(int viewDistance) { +- this.playerTicketManager.updateViewDistance(viewDistance); ++ this.chunkMap.setServerViewDistance(viewDistance); // Paper - route to player chunk manager + } + +- public void updateSimulationDistance(int simulationDistance) { +- if (simulationDistance != this.simulationDistance) { +- this.simulationDistance = simulationDistance; +- this.tickingTicketsTracker.replacePlayerTicketsLevel(this.getPlayerTicketLevel()); +- } ++ // Paper start ++ public int getSimulationDistance() { ++ return this.chunkMap.level.playerChunkLoader.getAPITickDistance(); ++ } ++ // Paper end + ++ public void updateSimulationDistance(int simulationDistance) { ++ this.chunkMap.level.playerChunkLoader.setTickDistance(simulationDistance); // Paper - route to player chunk manager + } + + public int getNaturalSpawnChunkCount() { +@@ -335,103 +201,26 @@ public abstract class DistanceManager { + } + + public String getDebugStatus() { +- return this.ticketThrottler.getDebugStatus(); ++ return "No DistanceManager stats available"; // Paper - rewrite chunk system + } + +- private void dumpTickets(String path) { +- try { +- FileOutputStream fileoutputstream = new FileOutputStream(new File(path)); +- +- try { +- ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().iterator(); +- +- while (objectiterator.hasNext()) { +- Entry>> entry = (Entry) objectiterator.next(); +- ChunkPos chunkcoordintpair = new ChunkPos(entry.getLongKey()); +- Iterator iterator = ((SortedArraySet) entry.getValue()).iterator(); +- +- while (iterator.hasNext()) { +- Ticket ticket = (Ticket) iterator.next(); +- +- fileoutputstream.write((chunkcoordintpair.x + "\t" + chunkcoordintpair.z + "\t" + String.valueOf(ticket.getType()) + "\t" + ticket.getTicketLevel() + "\t\n").getBytes(StandardCharsets.UTF_8)); +- } +- } +- } catch (Throwable throwable) { +- try { +- fileoutputstream.close(); +- } catch (Throwable throwable1) { +- throwable.addSuppressed(throwable1); +- } +- +- throw throwable; +- } +- +- fileoutputstream.close(); +- } catch (IOException ioexception) { +- DistanceManager.LOGGER.error("Failed to dump tickets to {}", path, ioexception); +- } +- +- } +- +- @VisibleForTesting +- TickingTracker tickingTracker() { +- return this.tickingTicketsTracker; +- } ++ // Paper - rewrite chunk system + + public void removeTicketsOnClosing() { +- ImmutableSet> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.LIGHT, TicketType.FUTURE_AWAIT, TicketType.CHUNK_RELIGHT, ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET); // Paper - add additional tickets to preserve +- ObjectIterator>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); +- +- while (objectiterator.hasNext()) { +- Entry>> entry = (Entry) objectiterator.next(); +- Iterator> iterator = ((SortedArraySet) entry.getValue()).iterator(); +- boolean flag = false; +- +- while (iterator.hasNext()) { +- Ticket ticket = (Ticket) iterator.next(); +- +- if (!immutableset.contains(ticket.getType())) { +- iterator.remove(); +- flag = true; +- this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket); +- } +- } +- +- if (flag) { +- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false); +- } +- +- if (((SortedArraySet) entry.getValue()).isEmpty()) { +- objectiterator.remove(); +- } +- } +- ++ // Paper - rewrite chunk system - this stupid hack ain't needed anymore + } + + public boolean hasTickets() { +- return !this.tickets.isEmpty(); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + // CraftBukkit start + public void removeAllTicketsFor(TicketType ticketType, int ticketLevel, T ticketIdentifier) { +- Ticket target = new Ticket<>(ticketType, ticketLevel, ticketIdentifier); +- +- for (java.util.Iterator>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); iterator.hasNext();) { +- Entry>> entry = iterator.next(); +- SortedArraySet> tickets = entry.getValue(); +- if (tickets.remove(target)) { +- // copied from removeTicket +- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt(tickets), false); +- +- // can't use entry after it's removed +- if (tickets.isEmpty()) { +- iterator.remove(); +- } +- } +- } ++ this.getChunkHolderManager().removeAllTicketsFor(ticketType, ticketLevel, ticketIdentifier); // Paper - rewrite chunk system + } + // CraftBukkit end + ++ /* Paper - rewrite chunk system + private class ChunkTicketTracker extends ChunkTracker { + + private static final int MAX_LEVEL = ChunkLevel.MAX_LEVEL + 1; +@@ -478,6 +267,7 @@ public abstract class DistanceManager { + return this.runUpdates(distance); + } + } ++ */ // Paper - rewrite chunk system + + private class FixedPlayerDistanceChunkTracker extends ChunkTracker { + +@@ -557,6 +347,7 @@ public abstract class DistanceManager { + } + } + ++ /* Paper - rewrite chunk system + private class PlayerTicketTracker extends DistanceManager.FixedPlayerDistanceChunkTracker { + + private int viewDistance = 0; +@@ -652,4 +443,5 @@ public abstract class DistanceManager { + return distance <= this.viewDistance; + } + } ++ */ // Paper - rewrite chunk system + } +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index 2d9d4d06b75873f888ef4d8f5779a52706f821a8..f74efe41cd0da2f9749fc96fb9e0f7cf237ad1c6 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -71,7 +71,7 @@ public class ServerChunkCache extends ChunkSource { + public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet entityTickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true); + final com.destroystokyo.paper.util.concurrent.WeakSeqLock loadedChunkMapSeqLock = new com.destroystokyo.paper.util.concurrent.WeakSeqLock(); + final it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap loadedChunkMap = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(8192, 0.5f); +- long chunkFutureAwaitCounter; ++ final java.util.concurrent.atomic.AtomicLong chunkFutureAwaitCounter = new java.util.concurrent.atomic.AtomicLong(); // Paper - chunk system rewrite + private final LevelChunk[] lastLoadedChunks = new LevelChunk[4 * 4]; + // Paper end + +@@ -195,7 +195,7 @@ public class ServerChunkCache extends ChunkSource { + public LevelChunk getChunkAtIfLoadedImmediately(int x, int z) { + long k = ChunkPos.asLong(x, z); + +- if (Thread.currentThread() == this.mainThread) { ++ if (io.papermc.paper.util.TickThread.isTickThread()) { // Paper - rewrite chunk system + return this.getChunkAtIfLoadedMainThread(x, z); + } + +@@ -247,7 +247,8 @@ public class ServerChunkCache extends ChunkSource { + @Nullable + @Override + public ChunkAccess getChunk(int x, int z, ChunkStatus leastStatus, boolean create) { +- if (Thread.currentThread() != this.mainThread) { ++ final int x1 = x; final int z1 = z; // Paper - conflict on variable change ++ if (!io.papermc.paper.util.TickThread.isTickThread()) { // Paper - rewrite chunk system + return (ChunkAccess) CompletableFuture.supplyAsync(() -> { + return this.getChunk(x, z, leastStatus, create); + }, this.mainThreadProcessor).join(); +@@ -263,15 +264,7 @@ public class ServerChunkCache extends ChunkSource { + gameprofilerfiller.incrementCounter("getChunk"); + long k = ChunkPos.asLong(x, z); + +- for (int l = 0; l < 4; ++l) { +- if (k == this.lastChunkPos[l] && leastStatus == this.lastChunkStatus[l]) { +- ChunkAccess ichunkaccess = this.lastChunk[l]; +- +- if (ichunkaccess != null) { // CraftBukkit - the chunk can become accessible in the meantime TODO for non-null chunks it might also make sense to check that the chunk's state hasn't changed in the meantime +- return ichunkaccess; +- } +- } +- } ++ // Paper - rewrite chunk system - there are no correct callbacks to remove items from cache in the new chunk system + + gameprofilerfiller.incrementCounter("getChunkCacheMiss"); + CompletableFuture> completablefuture = this.getChunkFutureMainThread(x, z, leastStatus, create); +@@ -279,9 +272,11 @@ public class ServerChunkCache extends ChunkSource { + + Objects.requireNonNull(completablefuture); + if (!completablefuture.isDone()) { // Paper ++ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.pushChunkWait(this.level, x1, z1); // Paper - rewrite chunk system + com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.level, x, z); // Paper - Add debug for sync chunk loads + this.level.timings.syncChunkLoad.startTiming(); // Paper + chunkproviderserver_b.managedBlock(completablefuture::isDone); ++ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.popChunkWait(); // Paper - rewrite chunk system + this.level.timings.syncChunkLoad.stopTiming(); // Paper + } // Paper + ChunkResult chunkresult = (ChunkResult) completablefuture.join(); +@@ -299,7 +294,7 @@ public class ServerChunkCache extends ChunkSource { + @Nullable + @Override + public LevelChunk getChunkNow(int chunkX, int chunkZ) { +- if (Thread.currentThread() != this.mainThread) { ++ if (!io.papermc.paper.util.TickThread.isTickThread()) { // Paper - rewrite chunk system + return null; + } else { + return this.getChunkAtIfLoadedMainThread(chunkX, chunkZ); // Paper - Perf: Optimise getChunkAt calls for loaded chunks +@@ -313,7 +308,7 @@ public class ServerChunkCache extends ChunkSource { + } + + public CompletableFuture> getChunkFuture(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) { +- boolean flag1 = Thread.currentThread() == this.mainThread; ++ boolean flag1 = io.papermc.paper.util.TickThread.isTickThread(); // Paper - rewrite chunk system + CompletableFuture completablefuture; + + if (flag1) { +@@ -333,48 +328,54 @@ public class ServerChunkCache extends ChunkSource { + return completablefuture; + } + ++ + private CompletableFuture> getChunkFutureMainThread(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) { +- ChunkPos chunkcoordintpair = new ChunkPos(chunkX, chunkZ); +- long k = chunkcoordintpair.toLong(); +- int l = ChunkLevel.byStatus(leastStatus); +- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k); ++ // Paper start - add isUrgent - old sig left in place for dirty nms plugins ++ return getChunkFutureMainThread(chunkX, chunkZ, leastStatus, create, false); ++ } ++ private CompletableFuture> getChunkFutureMainThread(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create, boolean isUrgent) { ++ // Paper start - rewrite chunk system ++ io.papermc.paper.util.TickThread.ensureTickThread(this.level, chunkX, chunkZ, "Scheduling chunk load off-main"); ++ int minLevel = ChunkLevel.byStatus(leastStatus); ++ io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.level.chunkTaskScheduler.chunkHolderManager.getChunkHolder(chunkX, chunkZ); + +- // CraftBukkit start - don't add new ticket for currently unloading chunk +- boolean currentlyUnloading = false; +- if (playerchunk != null) { +- FullChunkStatus oldChunkState = ChunkLevel.fullStatus(playerchunk.oldTicketLevel); +- FullChunkStatus currentChunkState = ChunkLevel.fullStatus(playerchunk.getTicketLevel()); +- currentlyUnloading = (oldChunkState.isOrAfter(FullChunkStatus.FULL) && !currentChunkState.isOrAfter(FullChunkStatus.FULL)); ++ boolean needsFullScheduling = leastStatus == ChunkStatus.FULL && (chunkHolder == null || !chunkHolder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)); ++ ++ if ((chunkHolder == null || chunkHolder.getTicketLevel() > minLevel || needsFullScheduling) && !create) { ++ return ChunkHolder.UNLOADED_CHUNK_FUTURE; + } +- if (create && !currentlyUnloading) { +- // CraftBukkit end +- this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair); +- if (this.chunkAbsent(playerchunk, l)) { +- ProfilerFiller gameprofilerfiller = this.level.getProfiler(); +- +- gameprofilerfiller.push("chunkLoad"); +- this.runDistanceManagerUpdates(); +- playerchunk = this.getVisibleChunkIfPresent(k); +- gameprofilerfiller.pop(); +- if (this.chunkAbsent(playerchunk, l)) { +- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("No chunk holder after ticket has been added")); ++ ++ io.papermc.paper.chunk.system.scheduling.NewChunkHolder.ChunkCompletion chunkCompletion = chunkHolder == null ? null : chunkHolder.getLastChunkCompletion(); ++ if (needsFullScheduling || chunkCompletion == null || !chunkCompletion.genStatus().isOrAfter(leastStatus)) { ++ // schedule ++ CompletableFuture> ret = new CompletableFuture<>(); ++ Consumer complete = (ChunkAccess chunk) -> { ++ if (chunk == null) { ++ ret.complete(ChunkResult.error("Unexpected chunk unload")); ++ } else { ++ ret.complete(ChunkResult.of(chunk)); + } +- } +- } ++ }; + +- return this.chunkAbsent(playerchunk, l) ? ChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.getOrScheduleFuture(leastStatus, this.chunkMap); +- } ++ this.level.chunkTaskScheduler.scheduleChunkLoad( ++ chunkX, chunkZ, leastStatus, true, ++ isUrgent ? ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.BLOCKING : ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, ++ complete ++ ); + +- private boolean chunkAbsent(@Nullable ChunkHolder holder, int maxLevel) { +- return holder == null || holder.oldTicketLevel > maxLevel; // CraftBukkit using oldTicketLevel for isLoaded checks ++ return ret; ++ } else { ++ // can return now ++ return CompletableFuture.completedFuture(ChunkResult.of(chunkCompletion.chunk())); ++ } ++ // Paper end - rewrite chunk system + } + ++ // Paper - rewrite chunk system ++ + @Override + public boolean hasChunk(int x, int z) { +- ChunkHolder playerchunk = this.getVisibleChunkIfPresent((new ChunkPos(x, z)).toLong()); +- int k = ChunkLevel.byStatus(ChunkStatus.FULL); +- +- return !this.chunkAbsent(playerchunk, k); ++ return this.getChunkAtIfLoadedImmediately(x, z) != null; // Paper - rewrite chunk system + } + + @Nullable +@@ -386,22 +387,13 @@ public class ServerChunkCache extends ChunkSource { + if (playerchunk == null) { + return null; + } else { +- int l = ServerChunkCache.CHUNK_STATUSES.size() - 1; +- +- while (true) { +- ChunkStatus chunkstatus = (ChunkStatus) ServerChunkCache.CHUNK_STATUSES.get(l); +- ChunkAccess ichunkaccess = (ChunkAccess) ((ChunkResult) playerchunk.getFutureIfPresentUnchecked(chunkstatus).getNow(ChunkHolder.UNLOADED_CHUNK)).orElse((Object) null); +- +- if (ichunkaccess != null) { +- return ichunkaccess; +- } +- +- if (chunkstatus == ChunkStatus.INITIALIZE_LIGHT.getParent()) { +- return null; +- } +- +- --l; ++ // Paper start - rewrite chunk system ++ ChunkStatus status = playerchunk.getChunkHolderStatus(); ++ if (status != null && !status.isOrAfter(ChunkStatus.LIGHT.getParent())) { ++ return null; + } ++ return playerchunk.getAvailableChunkNow(); ++ // Paper end - rewrite chunk system + } + } + +@@ -415,15 +407,7 @@ public class ServerChunkCache extends ChunkSource { + } + + public boolean runDistanceManagerUpdates() { // Paper - public +- boolean flag = this.distanceManager.runAllUpdates(this.chunkMap); +- boolean flag1 = this.chunkMap.promoteChunkMap(); +- +- if (!flag && !flag1) { +- return false; +- } else { +- this.clearCache(); +- return true; +- } ++ return this.level.chunkTaskScheduler.chunkHolderManager.processTicketUpdates(); // Paper - rewrite chunk system + } + + // Paper start +@@ -433,9 +417,10 @@ public class ServerChunkCache extends ChunkSource { + // Paper end + + public boolean isPositionTicking(long pos) { +- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); +- +- return playerchunk == null ? false : (!this.level.shouldTickBlocksAt(pos) ? false : ((ChunkResult) playerchunk.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).isSuccess()); ++ // Paper start - replace player chunk loader system ++ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(pos); ++ return holder != null && holder.isTickingReady(); ++ // Paper end - replace player chunk loader system + } + + public void save(boolean flush) { +@@ -451,17 +436,13 @@ public class ServerChunkCache extends ChunkSource { + this.close(true); + } + +- public void close(boolean save) throws IOException { +- if (save) { +- this.save(true); +- } +- // CraftBukkit end +- this.lightEngine.close(); +- this.chunkMap.close(); ++ public void close(boolean save) { // Paper - rewrite chunk system ++ this.level.chunkTaskScheduler.chunkHolderManager.close(save, true); // Paper - rewrite chunk system + } + + // CraftBukkit start - modelled on below + public void purgeUnload() { ++ if (true) return; // Paper - tickets will be removed later, this behavior isn't really well accounted for by the chunk system + this.level.getProfiler().push("purge"); + this.distanceManager.purgeStaleTickets(); + this.runDistanceManagerUpdates(); +@@ -485,6 +466,7 @@ public class ServerChunkCache extends ChunkSource { + this.level.getProfiler().popPush("chunks"); + if (tickChunks) { + this.level.timings.chunks.startTiming(); // Paper - timings ++ this.chunkMap.level.playerChunkLoader.tick(); // Paper - replace player chunk loader - this is mostly required to account for view distance changes + this.tickChunks(); + this.level.timings.chunks.stopTiming(); // Paper - timings + this.chunkMap.tick(); +@@ -587,7 +569,12 @@ public class ServerChunkCache extends ChunkSource { + ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); + + if (playerchunk != null) { +- ((ChunkResult) playerchunk.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).ifSuccess(chunkConsumer); ++ // Paper start - rewrite chunk system ++ LevelChunk chunk = playerchunk.getFullChunkNow(); ++ if (chunk != null) { ++ chunkConsumer.accept(chunk); ++ } ++ // Paper end - rewrite chunk system + } + + } +@@ -753,17 +740,10 @@ public class ServerChunkCache extends ChunkSource { + @Override + // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task + public boolean pollTask() { +- try { + if (ServerChunkCache.this.runDistanceManagerUpdates()) { + return true; +- } else { +- ServerChunkCache.this.lightEngine.tryScheduleUpdate(); +- return super.pollTask(); + } +- } finally { +- ServerChunkCache.this.chunkMap.callbackExecutor.run(); +- } +- // CraftBukkit end ++ return super.pollTask() | ServerChunkCache.this.level.chunkTaskScheduler.executeMainThreadTask(); // Paper - rewrite chunk system + } + } + +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 7dfbb3dd6100180abd2ebab4d1e9e34928666f9e..7f8793fa8260cb8a1f55d0ba405e6df0ae6f7321 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -199,7 +199,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + public final PrimaryLevelData serverLevelData; // CraftBukkit - type + private int lastSpawnChunkRadius; + final EntityTickList entityTickList; +- public final PersistentEntitySectionManager entityManager; ++ //public final PersistentEntitySectionManager entityManager; // Paper - rewrite chunk system + private final GameEventDispatcher gameEventDispatcher; + public boolean noSave; + private final SleepStatus sleepStatus; +@@ -268,50 +268,65 @@ public class ServerLevel extends Level implements WorldGenLevel { + return true; + } + +- public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, +- java.util.function.Consumer> onLoad) { +- if (Thread.currentThread() != this.thread) { +- this.getChunkSource().mainThreadProcessor.execute(() -> { +- this.loadChunksForMoveAsync(axisalignedbb, priority, onLoad); +- }); +- return; +- } ++ public final void loadChunksAsync(BlockPos pos, int radiusBlocks, ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, ++ java.util.function.Consumer> onLoad) { ++ loadChunksAsync( ++ (pos.getX() - radiusBlocks) >> 4, ++ (pos.getX() + radiusBlocks) >> 4, ++ (pos.getZ() - radiusBlocks) >> 4, ++ (pos.getZ() + radiusBlocks) >> 4, ++ priority, onLoad ++ ); ++ } ++ ++ public final void loadChunksAsync(BlockPos pos, int radiusBlocks, ++ net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus, ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, ++ java.util.function.Consumer> onLoad) { ++ loadChunksAsync( ++ (pos.getX() - radiusBlocks) >> 4, ++ (pos.getX() + radiusBlocks) >> 4, ++ (pos.getZ() - radiusBlocks) >> 4, ++ (pos.getZ() + radiusBlocks) >> 4, ++ chunkStatus, priority, onLoad ++ ); ++ } ++ ++ public final void loadChunksAsync(int minChunkX, int maxChunkX, int minChunkZ, int maxChunkZ, ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, ++ java.util.function.Consumer> onLoad) { ++ this.loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, priority, onLoad); ++ } ++ ++ public final void loadChunksAsync(int minChunkX, int maxChunkX, int minChunkZ, int maxChunkZ, ++ net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus, ++ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, ++ java.util.function.Consumer> onLoad) { + List ret = new java.util.ArrayList<>(); +- it.unimi.dsi.fastutil.ints.IntArrayList ticketLevels = new it.unimi.dsi.fastutil.ints.IntArrayList(); +- +- int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3; +- int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3; +- +- int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3; +- int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3; +- +- int minChunkX = minBlockX >> 4; +- int maxChunkX = maxBlockX >> 4; +- +- int minChunkZ = minBlockZ >> 4; +- int maxChunkZ = maxBlockZ >> 4; + + ServerChunkCache chunkProvider = this.getChunkSource(); + + int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1); +- int[] loadedChunks = new int[1]; ++ java.util.concurrent.atomic.AtomicInteger loadedChunks = new java.util.concurrent.atomic.AtomicInteger(); + +- Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++); ++ Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter.getAndIncrement()); ++ ++ int ticketLevel = 33 + net.minecraft.world.level.chunk.status.ChunkStatus.getDistance(chunkStatus); + + java.util.function.Consumer consumer = (net.minecraft.world.level.chunk.ChunkAccess chunk) -> { + if (chunk != null) { +- int ticketLevel = Math.max(33, chunkProvider.chunkMap.getUpdatingChunkIfPresent(chunk.getPos().toLong()).getTicketLevel()); ++ synchronized (ret) { + ret.add(chunk); +- ticketLevels.add(ticketLevel); ++ } + chunkProvider.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunk.getPos(), ticketLevel, holderIdentifier); + } +- if (++loadedChunks[0] == requiredChunks) { ++ if (loadedChunks.incrementAndGet() == requiredChunks) { + try { + onLoad.accept(java.util.Collections.unmodifiableList(ret)); + } finally { + for (int i = 0, len = ret.size(); i < len; ++i) { + ChunkPos 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); +@@ -323,12 +338,228 @@ public class ServerLevel extends Level implements WorldGenLevel { + for (int cx = minChunkX; cx <= maxChunkX; ++cx) { + for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { + io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad( +- this, cx, cz, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true, priority, consumer ++ this, cx, cz, chunkStatus, true, priority, consumer + ); + } + } + } +- // Paper end ++ ++ public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, ++ java.util.function.Consumer> onLoad) { ++ ++ int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3; ++ int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3; ++ ++ int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3; ++ int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3; ++ ++ int minChunkX = minBlockX >> 4; ++ int maxChunkX = maxBlockX >> 4; ++ ++ int minChunkZ = minBlockZ >> 4; ++ int maxChunkZ = maxBlockZ >> 4; ++ ++ this.loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, priority, onLoad); ++ } ++ ++ // Paper start - rewrite chunk system ++ public final io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler chunkTaskScheduler; ++ public final io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController chunkDataControllerNew ++ = new io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController(io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA) { ++ ++ @Override ++ public net.minecraft.world.level.chunk.storage.RegionFileStorage getCache() { ++ return ServerLevel.this.getChunkSource().chunkMap.regionFileCache; ++ } ++ ++ @Override ++ public void writeData(int chunkX, int chunkZ, net.minecraft.nbt.CompoundTag compound) throws IOException { ++ ServerLevel.this.getChunkSource().chunkMap.write(new ChunkPos(chunkX, chunkZ), compound); ++ } ++ ++ @Override ++ public net.minecraft.nbt.CompoundTag readData(int chunkX, int chunkZ) throws IOException { ++ return ServerLevel.this.getChunkSource().chunkMap.readSync(new ChunkPos(chunkX, chunkZ)); ++ } ++ }; ++ public final io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController poiDataControllerNew ++ = new io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController(io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.POI_DATA) { ++ ++ @Override ++ public net.minecraft.world.level.chunk.storage.RegionFileStorage getCache() { ++ return ServerLevel.this.getChunkSource().chunkMap.getPoiManager(); ++ } ++ ++ @Override ++ public void writeData(int chunkX, int chunkZ, net.minecraft.nbt.CompoundTag compound) throws IOException { ++ ServerLevel.this.getChunkSource().chunkMap.getPoiManager().write(new ChunkPos(chunkX, chunkZ), compound); ++ } ++ ++ @Override ++ public net.minecraft.nbt.CompoundTag readData(int chunkX, int chunkZ) throws IOException { ++ return ServerLevel.this.getChunkSource().chunkMap.getPoiManager().read(new ChunkPos(chunkX, chunkZ)); ++ } ++ }; ++ public final io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController entityDataControllerNew ++ = new io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController(io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.ENTITY_DATA) { ++ ++ @Override ++ public net.minecraft.world.level.chunk.storage.RegionFileStorage getCache() { ++ return ServerLevel.this.entityStorage; ++ } ++ ++ @Override ++ public void writeData(int chunkX, int chunkZ, net.minecraft.nbt.CompoundTag compound) throws IOException { ++ ServerLevel.this.writeEntityChunk(chunkX, chunkZ, compound); ++ } ++ ++ @Override ++ public net.minecraft.nbt.CompoundTag readData(int chunkX, int chunkZ) throws IOException { ++ return ServerLevel.this.readEntityChunk(chunkX, chunkZ); ++ } ++ }; ++ private final EntityRegionFileStorage entityStorage; ++ ++ private static final class EntityRegionFileStorage extends net.minecraft.world.level.chunk.storage.RegionFileStorage { ++ ++ public EntityRegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) { ++ super(storageKey, directory, dsync); ++ } ++ ++ protected void write(ChunkPos pos, net.minecraft.nbt.CompoundTag nbt) throws IOException { ++ ChunkPos nbtPos = nbt == null ? null : EntityStorage.readChunkPos(nbt); ++ if (nbtPos != null && !pos.equals(nbtPos)) { ++ throw new IllegalArgumentException( ++ "Entity chunk coordinate and serialized data do not have matching coordinates, trying to serialize coordinate " + pos.toString() ++ + " but compound says coordinate is " + nbtPos + " for world: " + this ++ ); ++ } ++ super.write(pos, nbt); ++ } ++ } ++ ++ private void writeEntityChunk(int chunkX, int chunkZ, net.minecraft.nbt.CompoundTag compound) throws IOException { ++ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) { ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.scheduleSave( ++ this, chunkX, chunkZ, compound, ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.ENTITY_DATA); ++ return; ++ } ++ this.entityStorage.write(new ChunkPos(chunkX, chunkZ), compound); ++ } ++ ++ private net.minecraft.nbt.CompoundTag readEntityChunk(int chunkX, int chunkZ) throws IOException { ++ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) { ++ return io.papermc.paper.chunk.system.io.RegionFileIOThread.loadData( ++ this, chunkX, chunkZ, io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.ENTITY_DATA, ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread() ++ ); ++ } ++ return this.entityStorage.read(new ChunkPos(chunkX, chunkZ)); ++ } ++ ++ private final io.papermc.paper.chunk.system.entity.EntityLookup entityLookup; ++ public final io.papermc.paper.chunk.system.entity.EntityLookup getEntityLookup() { ++ return this.entityLookup; ++ } ++ ++ private final java.util.concurrent.atomic.AtomicLong nonFullSyncLoadIdGenerator = new java.util.concurrent.atomic.AtomicLong(); ++ ++ private ChunkAccess getIfAboveStatus(int chunkX, int chunkZ, net.minecraft.world.level.chunk.status.ChunkStatus status) { ++ io.papermc.paper.chunk.system.scheduling.NewChunkHolder loaded = ++ this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(chunkX, chunkZ); ++ io.papermc.paper.chunk.system.scheduling.NewChunkHolder.ChunkCompletion loadedCompletion; ++ if (loaded != null && (loadedCompletion = loaded.getLastChunkCompletion()) != null && loadedCompletion.genStatus().isOrAfter(status)) { ++ return loadedCompletion.chunk(); ++ } ++ ++ return null; ++ } ++ ++ @Override ++ public ChunkAccess syncLoadNonFull(int chunkX, int chunkZ, net.minecraft.world.level.chunk.status.ChunkStatus status) { ++ if (status == null || status.isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.FULL)) { ++ throw new IllegalArgumentException("Status: " + status); ++ } ++ ChunkAccess loaded = this.getIfAboveStatus(chunkX, chunkZ, status); ++ if (loaded != null) { ++ return loaded; ++ } ++ ++ Long ticketId = Long.valueOf(this.nonFullSyncLoadIdGenerator.getAndIncrement()); ++ int ticketLevel = 33 + net.minecraft.world.level.chunk.status.ChunkStatus.getDistance(status); ++ this.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel( ++ TicketType.NON_FULL_SYNC_LOAD, chunkX, chunkZ, ticketLevel, ticketId ++ ); ++ this.chunkTaskScheduler.chunkHolderManager.processTicketUpdates(); ++ ++ this.chunkTaskScheduler.beginChunkLoadForNonFullSync(chunkX, chunkZ, status, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.BLOCKING); ++ ++ // we could do a simple spinwait here, since we do not need to process tasks while performing this load ++ // but we process tasks only because it's a better use of the time spent ++ this.chunkSource.mainThreadProcessor.managedBlock(() -> { ++ return ServerLevel.this.getIfAboveStatus(chunkX, chunkZ, status) != null; ++ }); ++ ++ loaded = ServerLevel.this.getIfAboveStatus(chunkX, chunkZ, status); ++ if (loaded == null) { ++ throw new IllegalStateException("Expected chunk to be loaded for status " + status); ++ } ++ ++ this.chunkTaskScheduler.chunkHolderManager.removeTicketAtLevel( ++ TicketType.NON_FULL_SYNC_LOAD, chunkX, chunkZ, ticketLevel, ticketId ++ ); ++ ++ return loaded; ++ } ++ ++ public final int getRegionChunkShift() { ++ // placeholder for folia ++ return io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift(); ++ } ++ // Paper end - rewrite chunk system ++ ++ public final io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader playerChunkLoader = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader(this); ++ private final java.util.concurrent.atomic.AtomicReference viewDistances = new java.util.concurrent.atomic.AtomicReference<>(new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances(-1, -1, -1)); ++ ++ public io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances getViewDistances() { ++ return this.viewDistances.get(); ++ } ++ ++ private void updateViewDistance(final java.util.function.Function update) { ++ for (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances curr = this.viewDistances.get();;) { ++ if (this.viewDistances.compareAndSet(curr, update.apply(curr))) { ++ return; ++ } ++ } ++ } ++ ++ public void setTickViewDistance(final int distance) { ++ if ((distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE)) { ++ throw new IllegalArgumentException("Tick view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE) + ", got: " + distance); ++ } ++ this.updateViewDistance((input) -> { ++ return input.setTickViewDistance(distance); ++ }); ++ } ++ ++ public void setLoadViewDistance(final int distance) { ++ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) { ++ throw new IllegalArgumentException("Load view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance); ++ } ++ this.updateViewDistance((input) -> { ++ return input.setLoadViewDistance(distance); ++ }); ++ } ++ ++ public void setSendViewDistance(final int distance) { ++ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) { ++ throw new IllegalArgumentException("Send view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance); ++ } ++ this.updateViewDistance((input) -> { ++ return input.setSendViewDistance(distance); ++ }); ++ } + + // Paper start - optimise getPlayerByUUID + @Nullable +@@ -382,16 +613,16 @@ public class ServerLevel extends Level implements WorldGenLevel { + // CraftBukkit end + boolean flag2 = minecraftserver.forceSynchronousWrites(); + DataFixer datafixer = minecraftserver.getFixerUpper(); +- EntityPersistentStorage entitypersistentstorage = new EntityStorage(new SimpleRegionStorage(new RegionStorageInfo(convertable_conversionsession.getLevelId(), resourcekey, "entities"), convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), datafixer, flag2, DataFixTypes.ENTITY_CHUNK), this, minecraftserver); ++ this.entityStorage = new EntityRegionFileStorage(new RegionStorageInfo(convertable_conversionsession.getLevelId(), resourcekey, "entities"), convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), flag2); // Paper - rewrite chunk system + +- this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage); ++ // this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage, this.entitySliceManager); // Paper // Paper - rewrite chunk system + StructureTemplateManager structuretemplatemanager = minecraftserver.getStructureManager(); + int j = this.spigotConfig.viewDistance; // Spigot + int k = this.spigotConfig.simulationDistance; // Spigot +- PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; ++ //PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; // Paper - rewrite chunk system + +- Objects.requireNonNull(this.entityManager); +- this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, persistententitysectionmanager::updateChunkStatus, () -> { ++ //Objects.requireNonNull(this.entityManager); // Paper - rewrite chunk system ++ this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, null, () -> { // Paper - rewrite chunk system + return minecraftserver.overworld().getDataStorage(); + }); + this.chunkSource.getGeneratorState().ensureStructuresGenerated(); +@@ -426,6 +657,9 @@ public class ServerLevel extends Level implements WorldGenLevel { + return (RandomSequences) this.getDataStorage().computeIfAbsent(RandomSequences.factory(l), "random_sequences"); + }); + this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit ++ ++ this.chunkTaskScheduler = new io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler(this, io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.workerThreads); // Paper - rewrite chunk system ++ this.entityLookup = new io.papermc.paper.chunk.system.entity.EntityLookup(this, new EntityCallbacks()); // Paper - rewrite chunk system + } + + // Paper start +@@ -558,7 +792,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + gameprofilerfiller.push("checkDespawn"); + entity.checkDespawn(); + gameprofilerfiller.pop(); +- if (this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { ++ if (true || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { // Paper - now always true if in the ticking list + Entity entity1 = entity.getVehicle(); + + if (entity1 != null) { +@@ -583,13 +817,16 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + gameprofilerfiller.push("entityManagement"); +- this.entityManager.tick(); ++ //this.entityManager.tick(); // Paper - rewrite chunk system + gameprofilerfiller.pop(); + } + + @Override + public boolean shouldTickBlocksAt(long chunkPos) { +- return this.chunkSource.chunkMap.getDistanceManager().inBlockTickingRange(chunkPos); ++ // Paper start - replace player chunk loader system ++ ChunkHolder holder = this.chunkSource.chunkMap.getVisibleChunkIfPresent(chunkPos); ++ return holder != null && holder.isTickingReady(); ++ // Paper end - replace player chunk loader system + } + + protected void tickTime() { +@@ -1066,6 +1303,11 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled) { ++ // Paper start - rewrite chunk system - add close param ++ this.save(progressListener, flush, savingDisabled, false); ++ } ++ public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled, boolean close) { ++ // Paper end - rewrite chunk system - add close param + ServerChunkCache chunkproviderserver = this.getChunkSource(); + + if (!savingDisabled) { +@@ -1081,16 +1323,13 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + + timings.worldSaveChunks.startTiming(); // Paper +- chunkproviderserver.save(flush); ++ if (!close) chunkproviderserver.save(flush); // Paper - rewrite chunk system ++ if (close) chunkproviderserver.close(true); // Paper - rewrite chunk system + timings.worldSaveChunks.stopTiming(); // Paper + }// Paper +- if (flush) { +- this.entityManager.saveAll(); +- } else { +- this.entityManager.autoSave(); +- } ++ // Paper - rewrite chunk system - entity saving moved into ChunkHolder + +- } ++ } else if (close) { chunkproviderserver.close(false); } // Paper - rewrite chunk system + + // CraftBukkit start - moved from MinecraftServer.saveChunks + ServerLevel worldserver1 = this; +@@ -1226,7 +1465,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.removePlayerImmediately((ServerPlayer) entity, Entity.RemovalReason.DISCARDED); + } + +- this.entityManager.addNewEntity(player); ++ this.entityLookup.addNewEntity(player); // Paper - rewite chunk system + } + + // CraftBukkit start +@@ -1257,7 +1496,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + // CraftBukkit end + +- return this.entityManager.addNewEntity(entity); ++ return this.entityLookup.addNewEntity(entity); // Paper - rewrite chunk system + } + } + +@@ -1269,10 +1508,10 @@ public class ServerLevel extends Level implements WorldGenLevel { + public boolean tryAddFreshEntityWithPassengers(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) { + // CraftBukkit end + Stream stream = entity.getSelfAndPassengers().map(Entity::getUUID); // CraftBukkit - decompile error +- PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; ++ //PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; // Paper - rewrite chunk system + +- Objects.requireNonNull(this.entityManager); +- if (stream.anyMatch(persistententitysectionmanager::isLoaded)) { ++ //Objects.requireNonNull(this.entityManager); // Paper - rewrite chunk system ++ if (stream.anyMatch(this.entityLookup::hasEntity)) { // Paper - rewrite chunk system + return false; + } else { + this.addFreshEntityWithPassengers(entity, reason); // CraftBukkit +@@ -1858,7 +2097,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + } + +- bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.entityManager.gatherStats())); ++ bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.entityLookup.getDebugInfo())); // Paper - rewrite chunk system + bufferedwriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size())); + bufferedwriter.write(String.format(Locale.ROOT, "block_ticks: %d\n", this.getBlockTicks().count())); + bufferedwriter.write(String.format(Locale.ROOT, "fluid_ticks: %d\n", this.getFluidTicks().count())); +@@ -1907,7 +2146,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + BufferedWriter bufferedwriter2 = Files.newBufferedWriter(path1); + + try { +- playerchunkmap.dumpChunks(bufferedwriter2); ++ //playerchunkmap.dumpChunks(bufferedwriter2); // Paper - rewrite chunk system + } catch (Throwable throwable4) { + if (bufferedwriter2 != null) { + try { +@@ -1928,7 +2167,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + BufferedWriter bufferedwriter3 = Files.newBufferedWriter(path2); + + try { +- this.entityManager.dumpSections(bufferedwriter3); ++ //this.entityManager.dumpSections(bufferedwriter3); // Paper - rewrite chunk system + } catch (Throwable throwable6) { + if (bufferedwriter3 != null) { + try { +@@ -2070,7 +2309,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + @VisibleForTesting + public String getWatchdogStats() { +- return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.entityManager.gatherStats(), ServerLevel.getTypeCount(this.entityManager.getEntityGetter().getAll(), (entity) -> { ++ return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.entityLookup.getDebugInfo(), ServerLevel.getTypeCount(this.entityLookup.getAll(), (entity) -> { // Paper - rewrite chunk system + return BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString(); + }), this.blockEntityTickers.size(), ServerLevel.getTypeCount(this.blockEntityTickers, TickingBlockEntity::getType), this.getBlockTicks().count(), this.getFluidTicks().count(), this.gatherChunkSourceStats()); + } +@@ -2130,15 +2369,15 @@ public class ServerLevel extends Level implements WorldGenLevel { + @Override + public LevelEntityGetter getEntities() { + org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot +- return this.entityManager.getEntityGetter(); ++ return this.entityLookup; // Paper - rewrite chunk system + } + +- public void addLegacyChunkEntities(Stream entities) { +- this.entityManager.addLegacyChunkEntities(entities); ++ public void addLegacyChunkEntities(Stream entities, ChunkPos forChunk) { // Paper - rewrite chunk system ++ this.entityLookup.addLegacyChunkEntities(entities.toList(), forChunk); // Paper - rewrite chunk system + } + +- public void addWorldGenChunkEntities(Stream entities) { +- this.entityManager.addWorldGenChunkEntities(entities); ++ public void addWorldGenChunkEntities(Stream entities, ChunkPos forChunk) { // Paper - rewrite chunk system ++ this.entityLookup.addWorldGenChunkEntities(entities.toList(), forChunk); // Paper - rewrite chunk system + } + + public void startTickingChunk(LevelChunk chunk) { +@@ -2158,34 +2397,49 @@ public class ServerLevel extends Level implements WorldGenLevel { + @Override + public void close() throws IOException { + super.close(); +- this.entityManager.close(); ++ //this.entityManager.close(); // Paper - rewrite chunk system + } + + @Override + public String gatherChunkSourceStats() { + String s = this.chunkSource.gatherStats(); + +- return "Chunks[S] W: " + s + " E: " + this.entityManager.gatherStats(); ++ return "Chunks[S] W: " + s + " E: " + this.entityLookup.getDebugInfo(); // Paper - rewrite chunk system + } + + public boolean areEntitiesLoaded(long chunkPos) { +- return this.entityManager.areEntitiesLoaded(chunkPos); ++ // Paper start - rewrite chunk system ++ return this.getChunkIfLoadedImmediately(ChunkPos.getX(chunkPos), ChunkPos.getZ(chunkPos)) != null; ++ // Paper end - rewrite chunk system + } + + private boolean isPositionTickingWithEntitiesLoaded(long chunkPos) { +- return this.areEntitiesLoaded(chunkPos) && this.chunkSource.isPositionTicking(chunkPos); ++ // Paper start - optimize is ticking ready type functions ++ io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(chunkPos); ++ // isTicking implies the chunk is loaded, and the chunk is loaded now implies the entities are loaded ++ return chunkHolder != null && chunkHolder.isTickingReady(); ++ // Paper end + } + + public boolean isPositionEntityTicking(BlockPos pos) { +- return this.entityManager.canPositionTick(pos) && this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(ChunkPos.asLong(pos)); ++ // Paper start - rewrite chunk system ++ io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(io.papermc.paper.util.CoordinateUtils.getChunkKey(pos)); ++ return chunkHolder != null && chunkHolder.isEntityTickingReady(); ++ // Paper end - rewrite chunk system + } + + public boolean isNaturalSpawningAllowed(BlockPos pos) { +- return this.entityManager.canPositionTick(pos); ++ // Paper start - rewrite chunk system ++ io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(io.papermc.paper.util.CoordinateUtils.getChunkKey(pos)); ++ return chunkHolder != null && chunkHolder.isEntityTickingReady(); ++ // Paper end - rewrite chunk system + } + + public boolean isNaturalSpawningAllowed(ChunkPos pos) { +- return this.entityManager.canPositionTick(pos); ++ // Paper start - rewrite chunk system ++ io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(io.papermc.paper.util.CoordinateUtils.getChunkKey(pos)); ++ return chunkHolder != null && chunkHolder.isEntityTickingReady(); ++ // Paper end - rewrite chunk system + } + + @Override +@@ -2211,7 +2465,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + CrashReportCategory crashreportsystemdetails = super.fillReportDetails(report); + + crashreportsystemdetails.setDetail("Loaded entity count", () -> { +- return String.valueOf(this.entityManager.count()); ++ return String.valueOf(this.entityLookup.getAllCopy().length); // Paper + }); + return crashreportsystemdetails; + } +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index bee8cc19d7912d2ab1b731c47cf7e31d21d61866..65e6623c2d1a6cf48729f78948fc48e82057114c 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -292,6 +292,50 @@ public class ServerPlayer extends Player { + public @Nullable String clientBrandName = null; // Paper - Brand support + public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - Add API for quit reason; there are a lot of changes to do if we change all methods leading to the event + ++ // Paper start - replace player chunk loader ++ private final java.util.concurrent.atomic.AtomicReference viewDistances = new java.util.concurrent.atomic.AtomicReference<>(new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances(-1, -1, -1)); ++ public io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.PlayerChunkLoaderData chunkLoader; ++ ++ public io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances getViewDistances() { ++ return this.viewDistances.get(); ++ } ++ ++ private void updateViewDistance(final java.util.function.Function update) { ++ for (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances curr = this.viewDistances.get();;) { ++ if (this.viewDistances.compareAndSet(curr, update.apply(curr))) { ++ return; ++ } ++ } ++ } ++ ++ public void setTickViewDistance(final int distance) { ++ if ((distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE)) { ++ throw new IllegalArgumentException("Tick view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE) + ", got: " + distance); ++ } ++ this.updateViewDistance((input) -> { ++ return input.setTickViewDistance(distance); ++ }); ++ } ++ ++ public void setLoadViewDistance(final int distance) { ++ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) { ++ throw new IllegalArgumentException("Load view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance); ++ } ++ this.updateViewDistance((input) -> { ++ return input.setLoadViewDistance(distance); ++ }); ++ } ++ ++ public void setSendViewDistance(final int distance) { ++ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) { ++ throw new IllegalArgumentException("Send view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance); ++ } ++ this.updateViewDistance((input) -> { ++ return input.setSendViewDistance(distance); ++ }); ++ } ++ // Paper end - replace player chunk loader ++ + public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) { + super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile); + this.chatVisibility = ChatVisiblity.FULL; +diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java +index f206df06a7d8895175db31d4a840d7467ffe826f..8ef22f8f0d6da49247a90152e5cfa9ffc7f596a4 100644 +--- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java ++++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java +@@ -37,15 +37,12 @@ import net.minecraft.world.level.chunk.status.ChunkStatus; + public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable { + public static final int DEFAULT_BATCH_SIZE = 1000; + private static final Logger LOGGER = LogUtils.getLogger(); +- private final ProcessorMailbox taskMailbox; +- private final ObjectList> lightTasks = new ObjectArrayList<>(); ++ // Paper - rewrite chunk system + private final ChunkMap chunkMap; +- private final ProcessorHandle> sorterMailbox; +- private final int taskPerBatch = 1000; +- private final AtomicBoolean scheduled = new AtomicBoolean(); ++ // Paper - rewrite chunk system + + // Paper start - replace light engine impl +- protected final ca.spottedleaf.starlight.common.light.StarLightInterface theLightEngine; ++ public final ca.spottedleaf.starlight.common.light.StarLightInterface theLightEngine; + public final boolean hasBlockLight; + public final boolean hasSkyLight; + // Paper end - replace light engine impl +@@ -59,8 +56,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + ) { + super(chunkProvider, false, false); // Paper - destroy vanilla light engine state + this.chunkMap = chunkStorage; +- this.sorterMailbox = executor; +- this.taskMailbox = processor; ++ // Paper - rewrite chunk system + // Paper start - replace light engine impl + this.hasBlockLight = true; + this.hasSkyLight = hasBlockLight; // Nice variable name. +@@ -104,7 +100,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + ++totalChunks; + } + +- this.taskMailbox.tell(() -> { ++ this.chunkMap.level.chunkTaskScheduler.radiusAwareScheduler.queueInfiniteRadiusTask(() -> { // Paper - rewrite chunk system + this.theLightEngine.relightChunks(chunks, (ChunkPos chunkPos) -> { + chunkLightCallback.accept(chunkPos); + ((java.util.concurrent.Executor)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().mainThreadProcessor).execute(() -> { +@@ -121,7 +117,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap(); + + private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ, +- final Supplier runnable) { ++ final Supplier runnable) { // Paper - rewrite chunk system + final ServerLevel world = (ServerLevel)this.theLightEngine.getWorld(); + + final ChunkAccess center = this.theLightEngine.getAnyChunkNow(chunkX, chunkZ); +@@ -148,7 +144,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + + final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); + +- final ca.spottedleaf.starlight.common.light.StarLightInterface.LightQueue.ChunkTasks updateFuture = runnable.get(); ++ final io.papermc.paper.chunk.system.light.LightQueue.ChunkTasks updateFuture = runnable.get(); // Paper - rewrite chunk system + + if (updateFuture == null) { + // not scheduled +@@ -285,16 +281,11 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + } + + private void addTask(int x, int z, ThreadedLevelLightEngine.TaskType stage, Runnable task) { +- this.addTask(x, z, this.chunkMap.getChunkQueueLevel(ChunkPos.asLong(x, z)), stage, task); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + private void addTask(int x, int z, IntSupplier completedLevelSupplier, ThreadedLevelLightEngine.TaskType stage, Runnable task) { +- this.sorterMailbox.tell(ChunkTaskPriorityQueueSorter.message(() -> { +- this.lightTasks.add(Pair.of(stage, task)); +- if (this.lightTasks.size() >= 1000) { +- this.runUpdate(); +- } +- }, ChunkPos.asLong(x, z), completedLevelSupplier)); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Override +@@ -327,83 +318,15 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl + } + + public CompletableFuture lightChunk(ChunkAccess chunk, boolean excludeBlocks) { +- // Paper start - replace light engine impl +- if (true) { +- boolean lit = excludeBlocks; +- final ChunkPos chunkPos = chunk.getPos(); +- +- return CompletableFuture.supplyAsync(() -> { +- final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(chunk); +- if (!lit) { +- chunk.setLightCorrect(false); +- this.theLightEngine.lightChunk(chunk, emptySections); +- chunk.setLightCorrect(true); +- } else { +- this.theLightEngine.forceLoadInChunk(chunk, 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(chunkPos.x, chunkPos.z); +- } +- +- this.chunkMap.releaseLightTicket(chunkPos); +- return chunk; +- }, (runnable) -> { +- this.theLightEngine.scheduleChunkLight(chunkPos, runnable); +- this.tryScheduleUpdate(); +- }).whenComplete((final ChunkAccess c, final Throwable throwable) -> { +- if (throwable != null) { +- LOGGER.error("Failed to light chunk " + chunkPos, throwable); +- } +- }); +- } +- // Paper end - replace light engine impl +- ChunkPos chunkPos = chunk.getPos(); +- chunk.setLightCorrect(false); +- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { +- if (!excludeBlocks) { +- super.propagateLightSources(chunkPos); +- } +- }, () -> "lightChunk " + chunkPos + " " + excludeBlocks)); +- return CompletableFuture.supplyAsync(() -> { +- chunk.setLightCorrect(true); +- this.chunkMap.releaseLightTicket(chunkPos); +- return chunk; +- }, task -> this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task)); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public void tryScheduleUpdate() { +- if (this.hasLightWork() && this.scheduled.compareAndSet(false, true)) { // Paper // Paper - rewrite light engine +- this.taskMailbox.tell(() -> { +- this.runUpdate(); +- this.scheduled.set(false); +- }); +- } ++ // Paper - rewrite chunk system + } + + private void runUpdate() { +- int i = Math.min(this.lightTasks.size(), 1000); +- ObjectListIterator> objectListIterator = this.lightTasks.iterator(); +- +- int j; +- for (j = 0; objectListIterator.hasNext() && j < i; j++) { +- Pair pair = objectListIterator.next(); +- if (pair.getFirst() == ThreadedLevelLightEngine.TaskType.PRE_UPDATE) { +- pair.getSecond().run(); +- } +- } +- +- objectListIterator.back(j); +- this.theLightEngine.propagateChanges(); // Paper - rewrite light engine +- +- for (int var5 = 0; objectListIterator.hasNext() && var5 < i; var5++) { +- Pair pair2 = objectListIterator.next(); +- if (pair2.getFirst() == ThreadedLevelLightEngine.TaskType.POST_UPDATE) { +- pair2.getSecond().run(); +- } +- +- objectListIterator.remove(); +- } ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + public CompletableFuture waitForPendingTasks(int x, int z) { +diff --git a/src/main/java/net/minecraft/server/level/Ticket.java b/src/main/java/net/minecraft/server/level/Ticket.java +index eba83b085435150e5954fd5d41dda9ce1d0601ad..e97329f867de2acbdd666925ba5d2aafa7a90574 100644 +--- a/src/main/java/net/minecraft/server/level/Ticket.java ++++ b/src/main/java/net/minecraft/server/level/Ticket.java +@@ -6,9 +6,12 @@ public final class Ticket implements Comparable> { + private final TicketType type; + private final int ticketLevel; + public final T key; +- private long createdTick; ++ // Paper start - rewrite chunk system ++ public long removeDelay; + +- protected Ticket(TicketType type, int level, T argument) { ++ public Ticket(TicketType type, int level, T argument, long removeDelay) { ++ this.removeDelay = removeDelay; ++ // Paper end - rewrite chunk system + this.type = type; + this.ticketLevel = level; + this.key = argument; +@@ -41,7 +44,7 @@ public final class Ticket implements Comparable> { + + @Override + public String toString() { +- return "Ticket[" + this.type + " " + this.ticketLevel + " (" + this.key + ")] at " + this.createdTick; ++ return "Ticket[" + this.type + " " + this.ticketLevel + " (" + this.key + ")] to die in " + this.removeDelay; // Paper - rewrite chunk system + } + + public TicketType getType() { +@@ -53,11 +56,10 @@ public final class Ticket implements Comparable> { + } + + protected void setCreatedTick(long tickCreated) { +- this.createdTick = tickCreated; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + protected boolean timedOut(long currentTick) { +- long l = this.type.timeout(); +- return l != 0L && currentTick - this.createdTick > l; ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + } +diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java +index 6051e5f272838ef23276a90e21c2fc821ca155d1..658e63ebde81dc14c8ab5850fb246dc0aab25dea 100644 +--- a/src/main/java/net/minecraft/server/level/TicketType.java ++++ b/src/main/java/net/minecraft/server/level/TicketType.java +@@ -8,6 +8,7 @@ import net.minecraft.world.level.ChunkPos; + + public class TicketType { + public static final TicketType FUTURE_AWAIT = create("future_await", Long::compareTo); // Paper ++ public static final TicketType ASYNC_LOAD = create("async_load", Long::compareTo); // Paper + + private final String name; + private final Comparator comparator; +@@ -27,6 +28,15 @@ public class TicketType { + public static final TicketType PLUGIN = TicketType.create("plugin", (a, b) -> 0); // CraftBukkit + public static final TicketType PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit + public static final TicketType CHUNK_RELIGHT = create("light_update", Long::compareTo); // Paper - ensure chunks stay loaded for lighting ++ // Paper start - rewrite chunk system ++ public static final TicketType CHUNK_LOAD = create("chunk_load", Long::compareTo); ++ public static final TicketType STATUS_UPGRADE = create("status_upgrade", Long::compareTo); ++ public static final TicketType ENTITY_LOAD = create("entity_load", Long::compareTo); ++ public static final TicketType POI_LOAD = create("poi_load", Long::compareTo); ++ public static final TicketType UNLOAD_COOLDOWN = create("unload_cooldown", (u1, u2) -> 0, 5 * 20); ++ public static final TicketType NON_FULL_SYNC_LOAD = create("non_full_sync_load", Long::compareTo); ++ public static final TicketType DELAY_UNLOAD = create("delay_unload", Comparator.comparingLong(ChunkPos::toLong), 1); ++ // Paper end - rewrite chunk system + + public static TicketType create(String name, Comparator argumentComparator) { + return new TicketType<>(name, argumentComparator, 0L); +diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +index ca4c8e256047a4af45811c3e772b5a959e2ae941..1351423a12c19a01f602a202832372a399e6a867 100644 +--- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java ++++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java +@@ -544,4 +544,21 @@ public class WorldGenRegion implements WorldGenLevel { + public long nextSubTickCount() { + return this.subTickCount.getAndIncrement(); + } ++ ++ // Paper start ++ // No-op, this class doesn't provide entity access ++ @Override ++ public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { ++ return Collections.emptyList(); ++ } ++ ++ @Override ++ public void getEntities(Entity except, AABB box, Predicate predicate, List into) {} ++ ++ @Override ++ public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) {} ++ ++ @Override ++ public void getEntitiesByClass(Class clazz, Entity except, AABB box, List into, Predicate predicate) {} ++ // Paper end + } +diff --git a/src/main/java/net/minecraft/server/network/PlayerChunkSender.java b/src/main/java/net/minecraft/server/network/PlayerChunkSender.java +index c502d9b85eb68b277ae17dfea34e0475f0156647..27d0f1ed58948039004f8f1eba2f7f9609fdeec0 100644 +--- a/src/main/java/net/minecraft/server/network/PlayerChunkSender.java ++++ b/src/main/java/net/minecraft/server/network/PlayerChunkSender.java +@@ -43,16 +43,23 @@ public class PlayerChunkSender { + + public void dropChunk(ServerPlayer player, ChunkPos pos) { + if (!this.pendingChunks.remove(pos.toLong()) && player.isAlive()) { ++ // Paper start - rewrite player chunk loader ++ dropChunkStatic(player, pos); ++ } ++ } ++ public static void dropChunkStatic(ServerPlayer player, ChunkPos pos) { ++ player.serverLevel().chunkSource.chunkMap.getVisibleChunkIfPresent(pos.toLong()).removePlayer(player); + player.connection.send(new ClientboundForgetLevelChunkPacket(pos)); + // Paper start - PlayerChunkUnloadEvent + if (io.papermc.paper.event.packet.PlayerChunkUnloadEvent.getHandlerList().getRegisteredListeners().length > 0) { + new io.papermc.paper.event.packet.PlayerChunkUnloadEvent(player.getBukkitEntity().getWorld().getChunkAt(pos.longKey), player.getBukkitEntity()).callEvent(); + } + // Paper end - PlayerChunkUnloadEvent +- } + } ++ // Paper end - rewrite player chunk loader + + public void sendNextChunks(ServerPlayer player) { ++ if (true) return; // Paper - rewrite player chunk loader + if (this.unacknowledgedBatches < this.maxUnacknowledgedBatches) { + float f = Math.max(1.0F, this.desiredChunksPerTick); + this.batchQuota = Math.min(this.batchQuota + this.desiredChunksPerTick, f); +@@ -78,7 +85,8 @@ public class PlayerChunkSender { + } + } + +- private static void sendChunk(ServerGamePacketListenerImpl handler, ServerLevel world, LevelChunk chunk) { ++ public static void sendChunk(ServerGamePacketListenerImpl handler, ServerLevel world, LevelChunk chunk) { // Paper - rewrite chunk loader - public ++ handler.player.serverLevel().chunkSource.chunkMap.getVisibleChunkIfPresent(chunk.getPos().toLong()).addPlayer(handler.player); + handler.send(new ClientboundLevelChunkWithLightPacket(chunk, world.getLightEngine(), null, null)); + // Paper start - PlayerChunkLoadEvent + if (io.papermc.paper.event.packet.PlayerChunkLoadEvent.getHandlerList().getRegisteredListeners().length > 0) { +@@ -118,6 +126,7 @@ public class PlayerChunkSender { + } + + public void onChunkBatchReceivedByClient(float desiredBatchSize) { ++ if (true) return; // Paper - rewrite player chunk loader + this.unacknowledgedBatches--; + this.desiredChunksPerTick = Double.isNaN((double)desiredBatchSize) ? 0.01F : Mth.clamp(desiredBatchSize, 0.01F, 64.0F); + if (this.unacknowledgedBatches == 0) { +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 0aa28caa1254137c0bae8e213bd08c9a654f5335..c4b4e5f5c9366b241686e881cda34568a57b4877 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -296,7 +296,7 @@ public abstract class PlayerList { + boolean flag2 = gamerules.getBoolean(GameRules.RULE_LIMITED_CRAFTING); + + // Spigot - view distance +- playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), this.server.levelKeys(), this.getMaxPlayers(), worldserver1.spigotConfig.viewDistance, worldserver1.spigotConfig.simulationDistance, flag1, !flag, flag2, player.createCommonSpawnInfo(worldserver1), this.server.enforceSecureProfile())); ++ playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), this.server.levelKeys(), this.getMaxPlayers(), worldserver1.getWorld().getSendViewDistance(), worldserver1.getWorld().getSimulationDistance(), flag1, !flag, flag2, player.createCommonSpawnInfo(worldserver1), this.server.enforceSecureProfile())); // Paper - replace old player chunk management + player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit + playerconnection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked())); + playerconnection.send(new ClientboundPlayerAbilitiesPacket(player.getAbilities())); +@@ -943,8 +943,8 @@ public abstract class PlayerList { + LevelData worlddata = worldserver2.getLevelData(); + + entityplayer1.connection.send(new ClientboundRespawnPacket(entityplayer1.createCommonSpawnInfo(worldserver2), (byte) i)); +- entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.spigotConfig.viewDistance)); // Spigot +- entityplayer1.connection.send(new ClientboundSetSimulationDistancePacket(worldserver1.spigotConfig.simulationDistance)); // Spigot ++ entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.getWorld().getSendViewDistance())); // Spigot // Paper - replace old player chunk management ++ entityplayer1.connection.send(new ClientboundSetSimulationDistancePacket(worldserver1.getWorld().getSimulationDistance())); // Spigot // Paper - replace old player chunk management + entityplayer1.connection.teleport(CraftLocation.toBukkit(entityplayer1.position(), worldserver2.getWorld(), entityplayer1.getYRot(), entityplayer1.getXRot())); // CraftBukkit + entityplayer1.connection.send(new ClientboundSetDefaultSpawnPositionPacket(worldserver1.getSharedSpawnPos(), worldserver1.getSharedSpawnAngle())); + entityplayer1.connection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked())); +@@ -1496,7 +1496,7 @@ public abstract class PlayerList { + + public void setViewDistance(int viewDistance) { + this.viewDistance = viewDistance; +- this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); ++ //this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); // Paper - move into setViewDistance + Iterator iterator = this.server.getAllLevels().iterator(); + + while (iterator.hasNext()) { +@@ -1511,7 +1511,7 @@ public abstract class PlayerList { + + public void setSimulationDistance(int simulationDistance) { + this.simulationDistance = simulationDistance; +- this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); ++ //this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); // Paper - handled by playerchunkloader + Iterator iterator = this.server.getAllLevels().iterator(); + + while (iterator.hasNext()) { +diff --git a/src/main/java/net/minecraft/util/SortedArraySet.java b/src/main/java/net/minecraft/util/SortedArraySet.java +index ea72dcb064a35bc6245bc5c94d592efedd8faf41..0793dfe47e68a2b48b010aad5b12dcfa1701293a 100644 +--- a/src/main/java/net/minecraft/util/SortedArraySet.java ++++ b/src/main/java/net/minecraft/util/SortedArraySet.java +@@ -14,6 +14,14 @@ public class SortedArraySet extends AbstractSet { + T[] contents; + int size; + ++ // Paper start - rewrite chunk system ++ public SortedArraySet(final SortedArraySet other) { ++ this.comparator = other.comparator; ++ this.size = other.size; ++ this.contents = Arrays.copyOf(other.contents, this.size); ++ } ++ // Paper end - rewrite chunk system ++ + private SortedArraySet(int initialCapacity, Comparator comparator) { + this.comparator = comparator; + if (initialCapacity < 0) { +@@ -22,6 +30,41 @@ public class SortedArraySet extends AbstractSet { + this.contents = (T[])castRawArray(new Object[initialCapacity]); + } + } ++ // Paper start - optimise removeIf ++ @Override ++ public boolean removeIf(java.util.function.Predicate filter) { ++ // prev. impl used an iterator, which could be n^2 and creates garbage ++ int i = 0, len = this.size; ++ T[] backingArray = this.contents; ++ ++ for (;;) { ++ if (i >= len) { ++ return false; ++ } ++ if (!filter.test(backingArray[i])) { ++ ++i; ++ continue; ++ } ++ break; ++ } ++ ++ // we only want to write back to backingArray if we really need to ++ ++ int lastIndex = i; // this is where new elements are shifted to ++ ++ for (; i < len; ++i) { ++ T curr = backingArray[i]; ++ if (!filter.test(curr)) { // if test throws we're screwed ++ backingArray[lastIndex++] = curr; ++ } ++ } ++ ++ // cleanup end ++ Arrays.fill(backingArray, lastIndex, len, null); ++ this.size = lastIndex; ++ return true; ++ } ++ // Paper end - optimise removeIf + + public static > SortedArraySet create() { + return create(10); +@@ -110,6 +153,31 @@ public class SortedArraySet extends AbstractSet { + } + } + ++ // Paper start - rewrite chunk system ++ public T replace(T object) { ++ int i = this.findIndex(object); ++ if (i >= 0) { ++ T old = this.contents[i]; ++ this.contents[i] = object; ++ return old; ++ } else { ++ this.addInternal(object, getInsertionPosition(i)); ++ return object; ++ } ++ } ++ ++ public T removeAndGet(T object) { ++ int i = this.findIndex(object); ++ if (i >= 0) { ++ final T ret = this.contents[i]; ++ this.removeInternal(i); ++ return ret; ++ } else { ++ return null; ++ } ++ } ++ // Paper end - rewrite chunk system ++ + @Override + public boolean remove(Object object) { + int i = this.findIndex((T)object); +diff --git a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java +index 7984f17cd9c4cef8100909b6c33b3144c8096fcf..639f72618a7c22fa94effa9d0406b97fffc64cb5 100644 +--- a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java ++++ b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java +@@ -227,7 +227,13 @@ public class WorldUpgrader { + this.previousWriteFuture.join(); + } + ++ // Paper start - async chunk io ++ try { + this.previousWriteFuture = storage.write(chunkPos, nbttagcompound1); ++ } catch (final IOException e) { ++ com.destroystokyo.paper.util.SneakyThrow.sneaky(e); ++ } ++ // Paper end - async chunk io + return true; + } + } +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 3c1bcf8d1a07b35a8688160c9f05f792451338a3..03840f520624662d4ce3ac9f3065a01c71b5f299 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -482,6 +482,58 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + // Paper end + ++ // Paper 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<>()); ++ { ++ /* // Goodbye, broken on reobf... ++ Boolean hardCollides = cachedOverrides.get(this.getClass()); ++ if (hardCollides == null) { ++ try { ++ java.lang.reflect.Method getHardCollisionBoxEntityMethod = Entity.class.getMethod("canCollideWith", Entity.class); ++ java.lang.reflect.Method hasHardCollisionBoxMethod = Entity.class.getMethod("canBeCollidedWith"); ++ 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 = this instanceof Boat ++ || this instanceof net.minecraft.world.entity.monster.Shulker ++ || this instanceof net.minecraft.world.entity.vehicle.AbstractMinecart ++ || this.shouldHardCollide(); ++ } ++ ++ // plugins can override ++ protected boolean shouldHardCollide() { ++ return false; ++ } ++ ++ public final boolean hardCollides() { ++ return this.hardCollides; ++ } ++ ++ public net.minecraft.server.level.FullChunkStatus chunkStatus; ++ ++ public int sectionX = Integer.MIN_VALUE; ++ public int sectionY = Integer.MIN_VALUE; ++ public int sectionZ = Integer.MIN_VALUE; ++ ++ public boolean updatingSectionStatus = false; ++ // Paper end ++ + public Entity(EntityType type, Level world) { + this.id = Entity.ENTITY_COUNTER.incrementAndGet(); + this.passengers = ImmutableList.of(); +@@ -2608,11 +2660,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return InteractionResult.PASS; + } + +- public boolean canCollideWith(Entity other) { ++ public boolean canCollideWith(Entity other) { // Paper - diff on change, hard colliding entities override this - TODO CHECK ON UPDATE - AbstractMinecart/Boat override + return other.canBeCollidedWith() && !this.isPassengerOfSameVehicle(other); + } + +- public boolean canBeCollidedWith() { ++ public boolean canBeCollidedWith() { // Paper - diff on change, hard colliding entities override this TODO CHECK ON UPDATE - Boat/Shulker override + return false; + } + +@@ -4037,6 +4089,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + }).count(); + } + ++ // Paper start - rewrite chunk system ++ public boolean hasAnyPlayerPassengers() { ++ // copied from below ++ if (this.passengers.isEmpty()) { return false; } ++ return this.getIndirectPassengersStream().anyMatch((entity) -> entity instanceof Player); ++ } ++ // Paper end - rewrite chunk system + public boolean hasExactlyOnePlayerPassenger() { + if (this.passengers.isEmpty()) { return false; } // Paper - Optimize indirect passenger iteration + return this.countPlayerPassengers() == 1; +@@ -4387,6 +4446,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return; + } + // Paper end - Block invalid positions and bounding box ++ // Paper start - rewrite chunk system ++ if (this.updatingSectionStatus) { ++ LOGGER.error("Refusing to update position for entity {} to position {} since it is processing a section status update", this, new Vec3(x, y, z), new Throwable()); ++ return; ++ } ++ // Paper end - rewrite chunk system + // Paper start - Fix MC-4 + if (this instanceof ItemEntity) { + if (io.papermc.paper.configuration.GlobalConfiguration.get().misc.fixEntityPositionDesync) { +@@ -4514,6 +4579,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + + @Override + public final void setRemoved(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) { ++ // Paper start - rewrite chunk system ++ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot remove entity off-main"); ++ if (!((ServerLevel)this.level).getEntityLookup().canRemoveEntity(this)) { ++ LOGGER.warn("Entity " + this + " is currently prevented from being removed from the world since it is processing section status updates", new Throwable()); ++ return; ++ } ++ // Paper end - rewrite chunk system + CraftEventFactory.callEntityRemoveEvent(this, cause); + // CraftBukkit end + final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers +@@ -4525,7 +4597,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.stopRiding(); + } + +- this.getPassengers().forEach(Entity::stopRiding); ++ if (entity_removalreason != RemovalReason.UNLOADED_TO_CHUNK) this.getPassengers().forEach(Entity::stopRiding); // Paper - chunk system - don't adjust passenger state when unloading, it's just not safe (and messes with our logic in entity chunk unload) + this.levelCallback.onRemove(entity_removalreason); + // Paper start - Folia schedulers + if (!(this instanceof ServerPlayer) && entity_removalreason != RemovalReason.CHANGED_DIMENSION && !alreadyRemoved) { +@@ -4556,7 +4628,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + + @Override + public boolean shouldBeSaved() { +- return this.removalReason != null && !this.removalReason.shouldSave() ? false : (this.isPassenger() ? false : !this.isVehicle() || !this.hasExactlyOnePlayerPassenger()); ++ return this.removalReason != null && !this.removalReason.shouldSave() ? false : (this.isPassenger() ? false : !this.isVehicle() || !this.hasAnyPlayerPassengers()); // Paper - rewrite chunk system - it should check if the entity has ANY player passengers + } + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +index 71d8909f35a22256406a2232d21adfd7d94dc3a5..7b52b0507cbda76aee1db954641f397bef51f94d 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java ++++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +@@ -40,20 +40,40 @@ import net.minecraft.world.level.chunk.storage.SimpleRegionStorage; + public class PoiManager extends SectionStorage { + public static final int MAX_VILLAGE_DISTANCE = 6; + public static final int VILLAGE_SECTION_SIZE = 1; +- private final PoiManager.DistanceTracker distanceTracker; +- private final LongSet loadedChunks = new LongOpenHashSet(); ++ // Paper start - rewrite chunk system ++ // the vanilla tracker needs to be replaced because it does not support level removes ++ public final net.minecraft.server.level.ServerLevel world; ++ private final io.papermc.paper.util.misc.Delayed26WayDistancePropagator3D villageDistanceTracker = new io.papermc.paper.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.isVillageCenter(section)) { ++ this.villageDistanceTracker.setSource(section, POI_DATA_SOURCE); ++ } else { ++ this.villageDistanceTracker.removeSource(section); ++ } ++ } ++ // Paper end - rewrite chunk system + + public PoiManager( + RegionStorageInfo storageKey, Path directory, DataFixer dataFixer, boolean dsync, RegistryAccess registryManager, LevelHeightAccessor world + ) { + super( ++ // Paper start ++ storageKey, ++ directory, ++ dsync, ++ // Paper end + new SimpleRegionStorage(storageKey, directory, dataFixer, dsync, DataFixTypes.POI_CHUNK), + PoiSection::codec, + PoiSection::new, + registryManager, + world + ); +- this.distanceTracker = new PoiManager.DistanceTracker(); ++ this.world = (net.minecraft.server.level.ServerLevel)world; // Paper - rewrite chunk system + } + + public void add(BlockPos pos, Holder type) { +@@ -187,8 +207,8 @@ public class PoiManager extends SectionStorage { + } + + public int sectionsToVillage(SectionPos pos) { +- this.distanceTracker.runAllUpdates(); +- return this.distanceTracker.getLevel(pos.asLong()); ++ this.villageDistanceTracker.propagateUpdates(); // Paper - replace distance tracking util ++ return convertBetweenLevels(this.villageDistanceTracker.getLevel(io.papermc.paper.util.CoordinateUtils.getChunkSectionKey(pos))); // Paper - replace distance tracking util + } + + boolean isVillageCenter(long pos) { +@@ -202,20 +222,117 @@ public class PoiManager extends SectionStorage { + + @Override + public void tick(BooleanSupplier shouldKeepTicking) { +- super.tick(shouldKeepTicking); +- this.distanceTracker.runAllUpdates(); ++ this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system + } + + @Override +- protected void setDirty(long pos) { +- super.setDirty(pos); +- this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false); ++ public void setDirty(long pos) { ++ // Paper start - rewrite chunk system ++ int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkSectionX(pos); ++ int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkSectionZ(pos); ++ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager manager = this.world.chunkTaskScheduler.chunkHolderManager; ++ io.papermc.paper.chunk.system.poi.PoiChunk chunk = manager.getPoiChunkIfLoaded(chunkX, chunkZ, false); ++ if (chunk != null) { ++ chunk.setDirty(true); ++ } ++ this.updateDistanceTracking(pos); ++ // Paper end - rewrite chunk system + } + + @Override + protected void onSectionLoad(long pos) { +- this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false); ++ this.updateDistanceTracking(pos); // Paper - move to new distance tracking util ++ } ++ ++ // Paper start - rewrite chunk system ++ @Override ++ public Optional get(long pos) { ++ int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkSectionX(pos); ++ int chunkY = io.papermc.paper.util.CoordinateUtils.getChunkSectionY(pos); ++ int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkSectionZ(pos); ++ ++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main"); ++ ++ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager manager = this.world.chunkTaskScheduler.chunkHolderManager; ++ io.papermc.paper.chunk.system.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true); ++ ++ return ret == null ? Optional.empty() : ret.getSectionForVanilla(chunkY); ++ } ++ ++ @Override ++ public Optional getOrLoad(long pos) { ++ int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkSectionX(pos); ++ int chunkY = io.papermc.paper.util.CoordinateUtils.getChunkSectionY(pos); ++ int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkSectionZ(pos); ++ ++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main"); ++ ++ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager manager = this.world.chunkTaskScheduler.chunkHolderManager; ++ ++ if (chunkY >= io.papermc.paper.util.WorldUtil.getMinSection(this.world) && ++ chunkY <= io.papermc.paper.util.WorldUtil.getMaxSection(this.world)) { ++ io.papermc.paper.chunk.system.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true); ++ if (ret != null) { ++ return ret.getSectionForVanilla(chunkY); ++ } else { ++ return manager.loadPoiChunk(chunkX, chunkZ).getSectionForVanilla(chunkY); ++ } ++ } ++ // retain vanilla behavior: do not load section if out of bounds! ++ return Optional.empty(); ++ } ++ ++ @Override ++ protected PoiSection getOrCreate(long pos) { ++ int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkSectionX(pos); ++ int chunkY = io.papermc.paper.util.CoordinateUtils.getChunkSectionY(pos); ++ int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkSectionZ(pos); ++ ++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main"); ++ ++ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager manager = this.world.chunkTaskScheduler.chunkHolderManager; ++ ++ io.papermc.paper.chunk.system.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true); ++ if (ret != null) { ++ return ret.getOrCreateSection(chunkY); ++ } else { ++ return manager.loadPoiChunk(chunkX, chunkZ).getOrCreateSection(chunkY); ++ } ++ } ++ ++ public void onUnload(long coordinate) { // Paper - rewrite chunk system ++ int chunkX = io.papermc.paper.util.MCUtil.getCoordinateX(coordinate); ++ int chunkZ = io.papermc.paper.util.MCUtil.getCoordinateZ(coordinate); ++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Unloading poi chunk off-main"); ++ for (int section = this.levelHeightAccessor.getMinSection(); section < this.levelHeightAccessor.getMaxSection(); ++section) { ++ long sectionPos = SectionPos.asLong(chunkX, section, chunkZ); ++ this.updateDistanceTracking(sectionPos); ++ } ++ } ++ ++ public void loadInPoiChunk(io.papermc.paper.chunk.system.poi.PoiChunk poiChunk) { ++ int chunkX = poiChunk.chunkX; ++ int chunkZ = poiChunk.chunkZ; ++ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Loading poi chunk off-main"); ++ for (int sectionY = this.levelHeightAccessor.getMinSection(); sectionY < this.levelHeightAccessor.getMaxSection(); ++sectionY) { ++ PoiSection section = poiChunk.getSection(sectionY); ++ if (section != null && !section.isEmpty()) { ++ this.onSectionLoad(SectionPos.asLong(chunkX, sectionY, chunkZ)); ++ } ++ } ++ } ++ ++ public void checkConsistency(net.minecraft.world.level.chunk.ChunkAccess chunk) { ++ int chunkX = chunk.getPos().x; ++ int chunkZ = chunk.getPos().z; ++ int minY = io.papermc.paper.util.WorldUtil.getMinSection(chunk); ++ int maxY = io.papermc.paper.util.WorldUtil.getMaxSection(chunk); ++ LevelChunkSection[] sections = chunk.getSections(); ++ for (int section = minY; section <= maxY; ++section) { ++ this.checkConsistencyWithBlocks(SectionPos.of(chunkX, section, chunkZ), sections[section - minY]); ++ } + } ++ // Paper end - rewrite chunk system + + public void checkConsistencyWithBlocks(SectionPos sectionPos, LevelChunkSection chunkSection) { + Util.ifElse(this.getOrLoad(sectionPos.asLong()), poiSet -> poiSet.refresh(populator -> { +@@ -251,7 +368,7 @@ public class PoiManager extends SectionStorage { + .map(sectionPos -> Pair.of(sectionPos, this.getOrLoad(sectionPos.asLong()))) + .filter(pair -> !pair.getSecond().map(PoiSection::isValid).orElse(false)) + .map(pair -> pair.getFirst().chunk()) +- .filter(chunkPos -> this.loadedChunks.add(chunkPos.toLong())) ++ // Paper - rewrite chunk system + .forEach(chunkPos -> world.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.EMPTY)); + } + +@@ -265,7 +382,7 @@ public class PoiManager extends SectionStorage { + + @Override + protected int getLevelFromSource(long id) { +- return PoiManager.this.isVillageCenter(id) ? 0 : 7; ++ return PoiManager.this.isVillageCenter(id) ? 0 : 7; // Paper - rewrite chunk system - diff on change, this specifies the source level to use for distance tracking + } + + @Override +@@ -287,6 +404,35 @@ public class PoiManager extends SectionStorage { + } + } + ++ // Paper start - Asynchronous chunk io ++ @javax.annotation.Nullable ++ @Override ++ public net.minecraft.nbt.CompoundTag read(ChunkPos chunkcoordintpair) throws java.io.IOException { ++ // Paper start - rewrite chunk system ++ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) { ++ return io.papermc.paper.chunk.system.io.RegionFileIOThread.loadData( ++ this.world, chunkcoordintpair.x, chunkcoordintpair.z, io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.POI_DATA, ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread() ++ ); ++ } ++ // Paper end - rewrite chunk system ++ return super.read(chunkcoordintpair); ++ } ++ ++ @Override ++ public void write(ChunkPos chunkcoordintpair, net.minecraft.nbt.CompoundTag nbttagcompound) throws java.io.IOException { ++ // Paper start - rewrite chunk system ++ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) { ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.scheduleSave( ++ this.world, chunkcoordintpair.x, chunkcoordintpair.z, nbttagcompound, ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.POI_DATA); ++ return; ++ } ++ // Paper end - rewrite chunk system ++ super.write(chunkcoordintpair, nbttagcompound); ++ } ++ // Paper end ++ + public static enum Occupancy { + HAS_SPACE(PoiRecord::hasSpace), + IS_OCCUPIED(PoiRecord::isOccupied), +diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java +index 971fb29a2c3dc713cb8ab1d2eed054cc16f9c93c..5b7deae326228e482b218aeebd857a59b7434eaf 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java ++++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java +@@ -29,6 +29,7 @@ public class PoiSection { + private final Map, Set> byType = Maps.newHashMap(); + private final Runnable setDirty; + private boolean isValid; ++ public final Optional noAllocateOptional = Optional.of(this); // Paper - rewrite chunk system + + public static Codec codec(Runnable updateListener) { + return RecordCodecBuilder.create( +@@ -46,6 +47,12 @@ public class PoiSection { + this(updateListener, true, ImmutableList.of()); + } + ++ // Paper start - isEmpty ++ public boolean isEmpty() { ++ return this.isValid && this.records.isEmpty() && this.byType.isEmpty(); ++ } ++ // Paper end ++ + private PoiSection(Runnable updateListener, boolean valid, List pois) { + this.setDirty = updateListener; + this.isValid = valid; +diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java +index bd20bea7f76a7307f1698fb2dfef37125032d166..9a28912f52824acdc80a62243b136e6f365bf567 100644 +--- a/src/main/java/net/minecraft/world/level/EntityGetter.java ++++ b/src/main/java/net/minecraft/world/level/EntityGetter.java +@@ -19,6 +19,18 @@ import net.minecraft.world.phys.shapes.Shapes; + import net.minecraft.world.phys.shapes.VoxelShape; + + public interface EntityGetter { ++ ++ // Paper start ++ List getHardCollidingEntities(Entity except, AABB box, Predicate predicate); ++ ++ void getEntities(Entity except, AABB box, Predicate predicate, List into); ++ ++ void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into); ++ ++ void getEntitiesByClass(Class clazz, Entity except, final AABB box, List into, ++ Predicate predicate); ++ // Paper end ++ + List getEntities(@Nullable Entity except, AABB box, Predicate predicate); + + List getEntities(EntityTypeTest filter, AABB box, Predicate predicate); +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 975fcd4b8f93cb8c602ddeb165c485214eac10a4..d3137c9e5cc42ef191ea233b0d37eafeffc6f82c 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -547,6 +547,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement + this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i); ++ // Paper start - per player view distance - allow block updates for non-ticking chunks in player view distance ++ // if copied from above ++ } else if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0)) { // Paper - replace old player chunk management ++ ((ServerLevel)this).getChunkSource().blockChanged(blockposition); ++ // Paper end - per player view distance + } + + if ((i & 1) != 0) { +@@ -941,7 +946,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + // Paper end - Perf: Optimize capturedTileEntities lookup + // CraftBukkit end +- return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && Thread.currentThread() != this.thread ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE)); ++ return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && !io.papermc.paper.util.TickThread.isTickThread() ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE)); // Paper - rewrite chunk system + } + + public void setBlockEntity(BlockEntity blockEntity) { +@@ -1032,26 +1037,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public List getEntities(@Nullable Entity except, AABB box, Predicate predicate) { + this.getProfiler().incrementCounter("getEntities"); + List list = Lists.newArrayList(); +- +- this.getEntities().get(box, (entity1) -> { +- if (entity1 != except && predicate.test(entity1)) { +- list.add(entity1); +- } +- +- if (entity1 instanceof EnderDragon) { +- EnderDragonPart[] aentitycomplexpart = ((EnderDragon) entity1).getSubEntities(); +- int i = aentitycomplexpart.length; +- +- for (int j = 0; j < i; ++j) { +- EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; +- +- if (entity1 != except && predicate.test(entitycomplexpart)) { +- list.add(entitycomplexpart); +- } +- } +- } +- +- }); ++ ((ServerLevel)this).getEntityLookup().getEntities(except, box, list, predicate); // Paper - optimise this call + return list; + } + +@@ -1069,33 +1055,23 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + public void getEntities(EntityTypeTest filter, AABB box, Predicate predicate, List result, int limit) { + this.getProfiler().incrementCounter("getEntities"); +- this.getEntities().get(filter, box, (entity) -> { +- if (predicate.test(entity)) { +- result.add(entity); +- if (result.size() >= limit) { +- return AbortableIterationConsumer.Continuation.ABORT; +- } +- } +- +- if (entity instanceof EnderDragon entityenderdragon) { +- EnderDragonPart[] aentitycomplexpart = entityenderdragon.getSubEntities(); +- int j = aentitycomplexpart.length; +- +- for (int k = 0; k < j; ++k) { +- EnderDragonPart entitycomplexpart = aentitycomplexpart[k]; +- T t0 = filter.tryCast(entitycomplexpart); // CraftBukkit - decompile error +- +- if (t0 != null && predicate.test(t0)) { +- result.add(t0); +- if (result.size() >= limit) { +- return AbortableIterationConsumer.Continuation.ABORT; +- } +- } +- } ++ // Paper start - optimise this call ++ //TODO use limit ++ if (filter instanceof net.minecraft.world.entity.EntityType entityTypeTest) { ++ ((ServerLevel) this).getEntityLookup().getEntities(entityTypeTest, box, result, predicate); ++ } else { ++ Predicate test = (obj) -> { ++ return filter.tryCast(obj) != null; ++ }; ++ predicate = predicate == null ? test : test.and((Predicate) predicate); ++ Class base; ++ if (filter == null || (base = filter.getBaseClass()) == null || base == Entity.class) { ++ ((ServerLevel) this).getEntityLookup().getEntities((Entity) null, box, (List) result, (Predicate)predicate); ++ } else { ++ ((ServerLevel) this).getEntityLookup().getEntities(base, null, box, (List) result, (Predicate)predicate); // Paper - optimise this call + } +- +- return AbortableIterationConsumer.Continuation.CONTINUE; +- }); ++ } ++ // Paper end - optimise this call + } + + @Nullable +@@ -1385,4 +1361,45 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + } + // Paper end - notify observers even if grow failed ++ // Paper start ++ //protected final io.papermc.paper.world.EntitySliceManager entitySliceManager; // Paper - rewrite chunk system ++ ++ public org.bukkit.entity.Entity[] getChunkEntities(int chunkX, int chunkZ) { ++ io.papermc.paper.world.ChunkEntitySlices slices = ((ServerLevel)this).getEntityLookup().getChunk(chunkX, chunkZ); ++ if (slices == null) { ++ return new org.bukkit.entity.Entity[0]; ++ } ++ return slices.getChunkEntities(); ++ } ++ ++ @Override ++ public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { ++ List ret = new java.util.ArrayList<>(); ++ ((ServerLevel)this).getEntityLookup().getHardCollidingEntities(except, box, ret, predicate); ++ return ret; ++ } ++ ++ @Override ++ public void getEntities(Entity except, AABB box, Predicate predicate, List into) { ++ ((ServerLevel)this).getEntityLookup().getEntities(except, box, into, predicate); ++ } ++ ++ @Override ++ public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) { ++ ((ServerLevel)this).getEntityLookup().getHardCollidingEntities(except, box, into, predicate); ++ } ++ ++ @Override ++ public void getEntitiesByClass(Class clazz, Entity except, final AABB box, List into, ++ Predicate predicate) { ++ ((ServerLevel)this).getEntityLookup().getEntities((Class)clazz, except, box, (List)into, (Predicate)predicate); ++ } ++ ++ @Override ++ public List getEntitiesOfClass(Class entityClass, AABB box, Predicate predicate) { ++ List ret = new java.util.ArrayList<>(); ++ ((ServerLevel)this).getEntityLookup().getEntities(entityClass, null, box, ret, predicate); ++ return ret; ++ } ++ // Paper end + } +diff --git a/src/main/java/net/minecraft/world/level/LevelReader.java b/src/main/java/net/minecraft/world/level/LevelReader.java +index a0ae26d6197e1069ca09982b4f8b706c55ae8491..32bfeb9aa87b43a9d2ce46dcc99dbd0ff355b412 100644 +--- a/src/main/java/net/minecraft/world/level/LevelReader.java ++++ b/src/main/java/net/minecraft/world/level/LevelReader.java +@@ -26,6 +26,15 @@ public interface LevelReader extends BlockAndTintGetter, CollisionGetter, Signal + @Nullable + ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create); + ++ // Paper start - rewrite chunk system ++ default ChunkAccess syncLoadNonFull(int chunkX, int chunkZ, ChunkStatus status) { ++ if (status == null || status.isOrAfter(ChunkStatus.FULL)) { ++ throw new IllegalArgumentException("Status: " + status.toString()); ++ } ++ return this.getChunk(chunkX, chunkZ, status, true); ++ } ++ // Paper end - rewrite chunk system ++ + @Nullable ChunkAccess getChunkIfLoadedImmediately(int x, int z); // Paper - ifLoaded api (we need this since current impl blocks if the chunk is loading) + @Nullable default ChunkAccess getChunkIfLoadedImmediately(BlockPos pos) { return this.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4);} + +diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java +index c9cd18ce79a6ee7297a8fd14f4dbe712570b3ced..927bdebdb8ae01613f0cea074b3367bd7ffe9ab1 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java +@@ -120,7 +120,7 @@ public abstract class ChunkGenerator { + return CompletableFuture.supplyAsync(Util.wrapThreadWithTaskName("init_biomes", () -> { + chunk.fillBiomesFromNoise(this.biomeSource, noiseConfig.sampler()); + return chunk; +- }), Util.backgroundExecutor()); ++ }), executor); // Paper - run with supplied executor + } + + public abstract void applyCarvers(WorldGenRegion chunkRegion, long seed, RandomState noiseConfig, BiomeManager biomeAccess, StructureManager structureAccessor, ChunkAccess chunk, GenerationStep.Carving carverStep); +@@ -315,7 +315,7 @@ public abstract class ChunkGenerator { + return Pair.of(placement.getLocatePos(pos), holder); + } + +- ChunkAccess ichunkaccess = world.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS); ++ ChunkAccess ichunkaccess = world.syncLoadNonFull(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS); // Paper - rewrite chunk system + + structurestart = structureAccessor.getStartForStructure(SectionPos.bottomOf(ichunkaccess), (Structure) holder.value(), ichunkaccess); + } while (structurestart == null); +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +index bac191f92ea3735df19c68d5568c2c7962c8680f..5d94aee1303d9eca5f1fa9a2e033ad0d12909635 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -86,6 +86,7 @@ public class LevelChunk extends ChunkAccess { + private final Int2ObjectMap gameEventListenerRegistrySections; + private final LevelChunkTicks blockTicks; + private final LevelChunkTicks fluidTicks; ++ public volatile FullChunkStatus chunkStatus = FullChunkStatus.INACCESSIBLE; // Paper - rewrite chunk system + + public LevelChunk(Level world, ChunkPos pos) { + this(world, pos, UpgradeData.EMPTY, new LevelChunkTicks<>(), new LevelChunkTicks<>(), 0L, (LevelChunkSection[]) null, (LevelChunk.PostLoadProcessor) null, (BlendingData) null); +@@ -690,9 +691,26 @@ public class LevelChunk extends ChunkAccess { + + } + +- // CraftBukkit start +- public void loadCallback() { +- // Paper start - neighbour cache ++ // Paper start - new load callbacks ++ private io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder; ++ public io.papermc.paper.chunk.system.scheduling.NewChunkHolder getChunkHolder() { ++ return this.chunkHolder; ++ } ++ ++ public void setChunkHolder(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { ++ if (chunkHolder == null) { ++ throw new NullPointerException("Chunkholder cannot be null"); ++ } ++ if (this.chunkHolder != null) { ++ throw new IllegalStateException("Already have chunkholder: " + this.chunkHolder + ", cannot replace with " + chunkHolder); ++ } ++ this.chunkHolder = chunkHolder; ++ this.playerChunk = chunkHolder.vanillaChunkHolder; ++ } ++ ++ /* Note: We skip the light neighbour chunk loading done for the vanilla full chunk */ ++ /* Starlight does not need these chunks for lighting purposes because of edge checks */ ++ public void pushChunkIntoLoadedMap() { + int chunkX = this.chunkPos.x; + int chunkZ = this.chunkPos.z; + net.minecraft.server.level.ServerChunkCache chunkProvider = this.level.getChunkSource(); +@@ -707,10 +725,55 @@ public class LevelChunk extends ChunkAccess { + } + } + this.setNeighbourLoaded(0, 0, this); ++ this.level.getChunkSource().addLoadedChunk(this); ++ } ++ ++ public void onChunkLoad(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { ++ // figure out how this should interface with: ++ // the entity chunk load event // -> moved to the FULL status ++ // the chunk load event // -> stays here ++ // any entity add to world events // -> in FULL status ++ this.loadCallback(); ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkBorder(this, chunkHolder.vanillaChunkHolder); ++ } ++ ++ public void onChunkUnload(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { ++ // figure out how this should interface with: ++ // the entity chunk load event // -> moved to chunk unload to disk (not written yet) ++ // the chunk load event // -> stays here ++ // any entity add to world events // -> goes into the unload logic, it will completely explode ++ // etc later ++ this.unloadCallback(); ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotBorder(this, chunkHolder.vanillaChunkHolder); ++ } ++ ++ public void onChunkTicking(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { ++ this.postProcessGeneration(); ++ this.level.startTickingChunk(this); ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkTicking(this, chunkHolder.vanillaChunkHolder); ++ } ++ ++ public void onChunkNotTicking(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotTicking(this, chunkHolder.vanillaChunkHolder); ++ } ++ ++ public void onChunkEntityTicking(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkEntityTicking(this, chunkHolder.vanillaChunkHolder); ++ } ++ ++ public void onChunkNotEntityTicking(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { ++ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotEntityTicking(this, chunkHolder.vanillaChunkHolder); ++ } ++ // Paper end - new load callbacks ++ ++ // CraftBukkit start ++ public void loadCallback() { ++ if (this.loadedTicketLevel) { LOGGER.error("Double calling chunk load!", new Throwable()); } // Paper ++ // Paper - rewrite chunk system - move into separate callback + this.loadedTicketLevel = true; +- // Paper end - neighbour cache ++ // Paper - rewrite chunk system - move into separate callback + org.bukkit.Server server = this.level.getCraftServer(); +- this.level.getChunkSource().addLoadedChunk(this); // Paper ++ // Paper - rewrite chunk system - move into separate callback + if (server != null) { + /* + * If it's a new world, the first few chunks are generated inside +@@ -719,6 +782,7 @@ public class LevelChunk extends ChunkAccess { + */ + org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this); + server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkLoadEvent(bukkitChunk, this.needsDecoration)); ++ this.chunkHolder.getEntityChunk().callEntitiesLoadEvent(); // Paper - rewrite chunk system + + if (this.needsDecoration) { + try (co.aikar.timings.Timing ignored = this.level.timings.chunkLoadPopulate.startTiming()) { // Paper +@@ -747,9 +811,11 @@ public class LevelChunk extends ChunkAccess { + } + + public void unloadCallback() { ++ if (!this.loadedTicketLevel) { LOGGER.error("Double calling chunk unload!", new Throwable()); } // Paper + org.bukkit.Server server = this.level.getCraftServer(); ++ this.chunkHolder.getEntityChunk().callEntitiesUnloadEvent(); // Paper - rewrite chunk system + org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this); +- org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(bukkitChunk, this.isUnsaved()); ++ org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(bukkitChunk, true); // Paper - rewrite chunk system - force save to true so that mustNotSave is correctly set below + server.getPluginManager().callEvent(unloadEvent); + // note: saving can be prevented, but not forced if no saving is actually required + this.mustNotSave = !unloadEvent.isSaveChunk(); +@@ -771,9 +837,26 @@ public class LevelChunk extends ChunkAccess { + // Paper end + } + ++ // Paper start - add dirty system to tick lists ++ @Override ++ public void setUnsaved(boolean needsSaving) { ++ if (!needsSaving) { ++ this.blockTicks.clearDirty(); ++ this.fluidTicks.clearDirty(); ++ } ++ super.setUnsaved(needsSaving); ++ } ++ // Paper end - add dirty system to tick lists ++ + @Override + public boolean isUnsaved() { +- return super.isUnsaved() && !this.mustNotSave; ++ // Paper start - add dirty system to tick lists ++ long gameTime = this.level.getLevelData().getGameTime(); ++ if (this.blockTicks.isDirty(gameTime) || this.fluidTicks.isDirty(gameTime)) { ++ return true; ++ } ++ // Paper end - add dirty system to tick lists ++ return super.isUnsaved(); // Paper - rewrite chunk system - do NOT clobber the dirty flag + } + // CraftBukkit end + +@@ -842,7 +925,9 @@ public class LevelChunk extends ChunkAccess { + return this.blockEntities; + } + ++ public boolean isPostProcessingDone; // Paper - replace chunk loader system + public void postProcessGeneration() { ++ try { // Paper - replace chunk loader system + ChunkPos chunkcoordintpair = this.getPos(); + + for (int i = 0; i < this.postProcessing.length; ++i) { +@@ -863,6 +948,7 @@ public class LevelChunk extends ChunkAccess { + BlockState iblockdata1 = Block.updateFromNeighbourShapes(iblockdata, this.level, blockposition); + + this.level.setBlock(blockposition, iblockdata1, 20); ++ if (iblockdata1 != iblockdata) this.level.chunkSource.blockChanged(blockposition); // Paper - replace player chunk loader - notify since we send before processing full updates + } + } + +@@ -880,6 +966,10 @@ public class LevelChunk extends ChunkAccess { + + this.pendingBlockEntities.clear(); + this.upgradeData.upgrade(this); ++ } finally { // Paper start - replace chunk loader system ++ this.isPostProcessingDone = true; ++ } ++ // Paper end - replace chunk loader system + } + + @Nullable +@@ -929,7 +1019,7 @@ public class LevelChunk extends ChunkAccess { + } + + public FullChunkStatus getFullStatus() { +- return this.fullStatus == null ? FullChunkStatus.FULL : (FullChunkStatus) this.fullStatus.get(); ++ return this.chunkHolder == null ? FullChunkStatus.INACCESSIBLE : this.chunkHolder.getChunkStatus(); // Paper - rewrite chunk system + } + + public void setFullStatus(Supplier levelTypeProvider) { +diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java +index 95318092f8281d98132d1d3ceb4a5c36cf32eb05..b81c548c0e1ac53784e9c94b34b65db5f123309c 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java ++++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java +@@ -21,13 +21,15 @@ import net.minecraft.world.level.chunk.ProtoChunk; + import net.minecraft.world.level.levelgen.Heightmap; + + public class ChunkStatus { ++ static final ChunkStatus.LoadingTask PASSTHROUGH_LOAD_TASK = (WorldGenContext context, ChunkStatus status, ToFullChunk fullChunkConverter, ChunkAccess chunk) -> CompletableFuture.completedFuture(chunk); // Paper - rewrite chunk system ++ protected static final java.util.List statuses = new java.util.ArrayList<>(); // Paper - rewrite chunk system + public static final int MAX_STRUCTURE_DISTANCE = 8; + private static final EnumSet PRE_FEATURES = EnumSet.of(Heightmap.Types.OCEAN_FLOOR_WG, Heightmap.Types.WORLD_SURFACE_WG); + public static final EnumSet POST_FEATURES = EnumSet.of( + Heightmap.Types.OCEAN_FLOOR, Heightmap.Types.WORLD_SURFACE, Heightmap.Types.MOTION_BLOCKING, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES + ); + public static final ChunkStatus EMPTY = register( +- "empty", null, -1, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateEmpty, ChunkStatusTasks::loadPassThrough ++ "empty", null, -1, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateEmpty, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system + ); + public static final ChunkStatus STRUCTURE_STARTS = register( + "structure_starts", +@@ -47,22 +49,22 @@ public class ChunkStatus { + PRE_FEATURES, + ChunkType.PROTOCHUNK, + ChunkStatusTasks::generateStructureReferences, +- ChunkStatusTasks::loadPassThrough ++ PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system + ); + public static final ChunkStatus BIOMES = register( +- "biomes", STRUCTURE_REFERENCES, 8, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateBiomes, ChunkStatusTasks::loadPassThrough ++ "biomes", STRUCTURE_REFERENCES, 8, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateBiomes, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system + ); + public static final ChunkStatus NOISE = register( +- "noise", BIOMES, 8, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateNoise, ChunkStatusTasks::loadPassThrough ++ "noise", BIOMES, 8, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateNoise, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system + ); + public static final ChunkStatus SURFACE = register( +- "surface", NOISE, 8, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateSurface, ChunkStatusTasks::loadPassThrough ++ "surface", NOISE, 8, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateSurface, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system + ); + public static final ChunkStatus CARVERS = register( +- "carvers", SURFACE, 8, false, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateCarvers, ChunkStatusTasks::loadPassThrough ++ "carvers", SURFACE, 8, false, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateCarvers, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system + ); + public static final ChunkStatus FEATURES = register( +- "features", CARVERS, 8, false, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateFeatures, ChunkStatusTasks::loadPassThrough ++ "features", CARVERS, 8, false, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateFeatures, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system + ); + public static final ChunkStatus INITIALIZE_LIGHT = register( + "initialize_light", +@@ -78,7 +80,7 @@ public class ChunkStatus { + "light", INITIALIZE_LIGHT, 1, true, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateLight, ChunkStatusTasks::loadLight + ); + public static final ChunkStatus SPAWN = register( +- "spawn", LIGHT, 1, false, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateSpawn, ChunkStatusTasks::loadPassThrough ++ "spawn", LIGHT, 1, false, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateSpawn, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system + ); + public static final ChunkStatus FULL = register( + "full", SPAWN, 0, false, POST_FEATURES, ChunkType.LEVELCHUNK, ChunkStatusTasks::generateFull, ChunkStatusTasks::loadFull +@@ -128,6 +130,27 @@ public class ChunkStatus { + } + } + // Paper end - starlight ++ // Paper start - rewrite chunk system ++ public boolean isParallelCapable; // Paper ++ public int writeRadius = -1; ++ public int loadRange = 0; ++ ++ private ChunkStatus nextStatus; ++ ++ public final ChunkStatus getNextStatus() { ++ return this.nextStatus; ++ } ++ ++ public final boolean isEmptyLoadStatus() { ++ return this.loadingTask == PASSTHROUGH_LOAD_TASK; ++ } ++ ++ public final boolean isEmptyGenStatus() { ++ return this == ChunkStatus.EMPTY; ++ } ++ ++ public final java.util.concurrent.atomic.AtomicBoolean warnedAboutNoImmediateComplete = new java.util.concurrent.atomic.AtomicBoolean(); ++ // Paper end - rewrite chunk system + + private static ChunkStatus register( + String id, +@@ -190,6 +213,13 @@ public class ChunkStatus { + this.chunkType = chunkType; + this.heightmapsAfter = heightMapTypes; + this.index = previous == null ? 0 : previous.getIndex() + 1; ++ // Paper start ++ this.nextStatus = this; ++ if (statuses.size() > 0) { ++ statuses.get(statuses.size() - 1).nextStatus = this; ++ } ++ statuses.add(this); ++ // Paper end + } + + public int getIndex() { +diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java +index ce7f154b9dad4e78ee0189405cf57dcb3d5301b8..a5e8078b99161272b0f826b8c39e56d17588c264 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java ++++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java +@@ -26,8 +26,9 @@ public class ChunkStatusTasks { + return CompletableFuture.completedFuture(chunk); + } + +- static CompletableFuture loadPassThrough(WorldGenContext context, ChunkStatus status, ToFullChunk fullChunkConverter, ChunkAccess chunk) { +- return CompletableFuture.completedFuture(chunk); ++ @io.papermc.paper.annotation.DoNotUse @Deprecated(forRemoval = true) // Paper - rewrite chunk system - use ChunkStatus.PASSTHROUGH_LOAD_TASK instead ++ static CompletableFuture loadPassThrough(WorldGenContext context, ChunkStatus status, ToFullChunk fullChunkConverter, ChunkAccess chunk) { // Paper - rewrite chunk system - diff on change ++ return CompletableFuture.completedFuture(chunk); // Paper - rewrite chunk system - diff on change + } + + static CompletableFuture generateStructureStarts(WorldGenContext context, ChunkStatus status, Executor executor, ToFullChunk fullChunkConverter, List chunks, ChunkAccess chunk) { +@@ -125,7 +126,7 @@ public class ChunkStatusTasks { + ((ProtoChunk) chunk).setLightEngine(lightingProvider); + boolean flag = ChunkStatusTasks.isLighted(chunk); + +- return lightingProvider.initializeLight(chunk, flag); ++ return CompletableFuture.completedFuture(chunk); // Paper - rewrite chunk system + } + + static CompletableFuture generateLight(WorldGenContext context, ChunkStatus status, Executor executor, ToFullChunk fullChunkConverter, List chunks, ChunkAccess chunk) { +@@ -139,7 +140,7 @@ public class ChunkStatusTasks { + private static CompletableFuture lightChunk(ThreadedLevelLightEngine lightingProvider, ChunkAccess chunk) { + boolean flag = ChunkStatusTasks.isLighted(chunk); + +- return lightingProvider.lightChunk(chunk, flag); ++ return CompletableFuture.completedFuture(chunk); // Paper - rewrite chunk system + } + + static CompletableFuture generateSpawn(WorldGenContext context, ChunkStatus status, Executor executor, ToFullChunk fullChunkConverter, List chunks, ChunkAccess chunk) { +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +index 01d6b8683a9fa30d05b03ebfef8ee2dca4e83a56..b2e8082dda0d0ca32bfc32cbf9d8ae9822febc30 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +@@ -112,6 +112,17 @@ public class ChunkSerializer { + } + } + // Paper end - guard against serializing mismatching coordinates ++ // Paper start - rewrite chunk system ++ public static final class InProgressChunkHolder { ++ ++ public final ProtoChunk protoChunk; ++ ++ public CompoundTag poiData; ++ ++ public InProgressChunkHolder(final ProtoChunk protoChunk) { ++ this.protoChunk = protoChunk; ++ } ++ } + public static ProtoChunk read(ServerLevel world, PoiManager poiStorage, ChunkPos chunkPos, CompoundTag nbt) { + // Paper start - Do not let the server load chunks from newer versions + if (nbt.contains("DataVersion", net.minecraft.nbt.Tag.TAG_ANY_NUMERIC)) { +@@ -122,6 +133,12 @@ public class ChunkSerializer { + } + } + // Paper end - Do not let the server load chunks from newer versions ++ InProgressChunkHolder holder = readInProgressChunkHolder(world, poiStorage, chunkPos, nbt); ++ return holder.protoChunk; ++ } ++ ++ public static InProgressChunkHolder readInProgressChunkHolder(ServerLevel world, PoiManager poiStorage, ChunkPos chunkPos, CompoundTag nbt) { ++ // Paper end - rewrite chunk system + ChunkPos chunkcoordintpair1 = new ChunkPos(nbt.getInt("xPos"), nbt.getInt("zPos")); // Paper - guard against serializing mismatching coordinates; diff on change, see ChunkSerializer#getChunkCoordinate + + if (!Objects.equals(chunkPos, chunkcoordintpair1)) { +@@ -178,7 +195,7 @@ public class ChunkSerializer { + achunksection[k] = chunksection; + SectionPos sectionposition = SectionPos.of(chunkPos, b0); + +- poiStorage.checkConsistencyWithBlocks(sectionposition, chunksection); ++ // Paper - rewrite chunk system - moved to final load stage + } + + boolean flag3 = nbttagcompound1.contains("BlockLight", 7); +@@ -325,7 +342,7 @@ public class ChunkSerializer { + } + + if (chunktype == ChunkType.LEVELCHUNK) { +- return new ImposterProtoChunk((LevelChunk) object1, false); ++ return new InProgressChunkHolder(new ImposterProtoChunk((LevelChunk) object1, false)); // Paper - Async chunk loading + } else { + ProtoChunk protochunk1 = (ProtoChunk) object1; + +@@ -360,9 +377,41 @@ public class ChunkSerializer { + protochunk1.setCarvingMask(worldgenstage_features, new CarvingMask(nbttagcompound5.getLongArray(s1), ((ChunkAccess) object1).getMinBuildHeight())); + } + +- return protochunk1; ++ return new InProgressChunkHolder(protochunk1); // Paper - Async chunk loading ++ } ++ } ++ ++ // Paper start - async chunk save for unload ++ public record AsyncSaveData( ++ Tag blockTickList, // non-null if we had to go to the server's tick list ++ Tag fluidTickList, // non-null if we had to go to the server's tick list ++ ListTag blockEntities, ++ long worldTime ++ ) {} ++ ++ // must be called sync ++ public static AsyncSaveData getAsyncSaveData(ServerLevel world, ChunkAccess chunk) { ++ org.spigotmc.AsyncCatcher.catchOp("preparation of chunk data for async save"); ++ ++ final CompoundTag tickLists = new CompoundTag(); ++ ChunkSerializer.saveTicks(world, tickLists, chunk.getTicksForSerialization()); ++ ++ ListTag blockEntitiesSerialized = new ListTag(); ++ for (final BlockPos blockPos : chunk.getBlockEntitiesPos()) { ++ final CompoundTag blockEntityNbt = chunk.getBlockEntityNbtForSaving(blockPos, world.registryAccess()); ++ if (blockEntityNbt != null) { ++ blockEntitiesSerialized.add(blockEntityNbt); ++ } + } ++ ++ return new AsyncSaveData( ++ tickLists.get(BLOCK_TICKS_TAG), ++ tickLists.get(FLUID_TICKS_TAG), ++ blockEntitiesSerialized, ++ world.getGameTime() ++ ); + } ++ // Paper end + + private static void logErrors(ChunkPos chunkPos, int y, String message) { + ChunkSerializer.LOGGER.error("Recoverable errors when loading section [" + chunkPos.x + ", " + y + ", " + chunkPos.z + "]: " + message); +@@ -379,6 +428,11 @@ public class ChunkSerializer { + // CraftBukkit end + + public static CompoundTag write(ServerLevel world, ChunkAccess chunk) { ++ // Paper start ++ return saveChunk(world, chunk, null); ++ } ++ public static CompoundTag saveChunk(ServerLevel world, ChunkAccess chunk, @org.checkerframework.checker.nullness.qual.Nullable AsyncSaveData asyncsavedata) { ++ // Paper end + // Paper start - rewrite light impl + final int minSection = io.papermc.paper.util.WorldUtil.getMinLightSection(world); + final int maxSection = io.papermc.paper.util.WorldUtil.getMaxLightSection(world); +@@ -391,7 +445,7 @@ public class ChunkSerializer { + nbttagcompound.putInt("xPos", chunkcoordintpair.x); + nbttagcompound.putInt("yPos", chunk.getMinSection()); + nbttagcompound.putInt("zPos", chunkcoordintpair.z); +- nbttagcompound.putLong("LastUpdate", world.getGameTime()); ++ nbttagcompound.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : world.getGameTime()); // Paper - async chunk unloading + nbttagcompound.putLong("InhabitedTime", chunk.getInhabitedTime()); + nbttagcompound.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getStatus()).toString()); + BlendingData blendingdata = chunk.getBlendingData(); +@@ -485,8 +539,17 @@ public class ChunkSerializer { + nbttagcompound.putBoolean("isLightOn", false); // Paper - set to false but still store, this allows us to detect --eraseCache (as eraseCache _removes_) + } + +- ListTag nbttaglist1 = new ListTag(); +- Iterator iterator = chunk.getBlockEntitiesPos().iterator(); ++ // Paper start ++ ListTag nbttaglist1; ++ Iterator iterator; ++ if (asyncsavedata != null) { ++ nbttaglist1 = asyncsavedata.blockEntities; ++ iterator = java.util.Collections.emptyIterator(); ++ } else { ++ nbttaglist1 = new ListTag(); ++ iterator = chunk.getBlockEntitiesPos().iterator(); ++ } ++ // Paper end + + CompoundTag nbttagcompound2; + +@@ -522,7 +585,14 @@ public class ChunkSerializer { + nbttagcompound.put("CarvingMasks", nbttagcompound2); + } + ++ // Paper start ++ if (asyncsavedata != null) { ++ nbttagcompound.put(BLOCK_TICKS_TAG, asyncsavedata.blockTickList); ++ nbttagcompound.put(FLUID_TICKS_TAG, asyncsavedata.fluidTickList); ++ } else { + ChunkSerializer.saveTicks(world, nbttagcompound, chunk.getTicksForSerialization()); ++ } ++ // Paper end + nbttagcompound.put("PostProcessing", ChunkSerializer.packOffsets(chunk.getPostProcessing())); + CompoundTag nbttagcompound3 = new CompoundTag(); + Iterator iterator1 = chunk.getHeightmaps().iterator(); +@@ -578,7 +648,7 @@ public class ChunkSerializer { + + return nbttaglist == null && nbttaglist1 == null ? null : (chunk) -> { + if (nbttaglist != null) { +- world.addLegacyChunkEntities(EntityType.loadEntitiesRecursive(nbttaglist, world)); ++ world.addLegacyChunkEntities(EntityType.loadEntitiesRecursive(nbttaglist, world), chunk.getPos()); // Paper - rewrite chunk system + } + + if (nbttaglist1 != null) { +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java +index a62c90e10c0dfa4c6211a05c4071932756d7b218..554dede2ad0e45d3ee4ccc5510b7644f2e9e4250 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java +@@ -31,18 +31,21 @@ import net.minecraft.world.level.storage.DimensionDataStorage; + public class ChunkStorage implements AutoCloseable { + + public static final int LAST_MONOLYTH_STRUCTURE_DATA_VERSION = 1493; +- private final IOWorker worker; ++ // Paper start - rewrite chunk system; async chunk IO ++ private final Object persistentDataLock = new Object(); ++ public final RegionFileStorage regionFileCache; ++ // Paper end - rewrite chunk system + protected final DataFixer fixerUpper; + @Nullable + private volatile LegacyStructureDataHandler legacyStructureHandler; + + public ChunkStorage(RegionStorageInfo storageKey, Path directory, DataFixer dataFixer, boolean dsync) { + this.fixerUpper = dataFixer; +- this.worker = new IOWorker(storageKey, directory, dsync); ++ this.regionFileCache = new RegionFileStorage(storageKey, directory, dsync); // Paper - rewrite chunk system; async chunk IO + } + + public boolean isOldChunkAround(ChunkPos chunkPos, int checkRadius) { +- return this.worker.isOldChunkAround(chunkPos, checkRadius); ++ return true; // Paper - rewrite chunk system + } + + // CraftBukkit start +@@ -50,8 +53,9 @@ public class ChunkStorage implements AutoCloseable { + if (true) return true; // Paper - Perf: this isn't even needed anymore, light is purged updating to 1.14+, why are we holding up the conversion process reading chunk data off disk - return true, we need to set light populated to true so the converter recognizes the chunk as being "full" + ChunkPos pos = new ChunkPos(x, z); + if (cps != null) { +- com.google.common.base.Preconditions.checkState(org.bukkit.Bukkit.isPrimaryThread(), "primary thread"); +- if (cps.hasChunk(x, z)) { ++ // Paper start - rewrite chunk system; async chunk IO ++ if (cps.getChunkAtIfCachedImmediately(x, z) != null) { // isLoaded is a ticket level check, not a chunk loaded check! ++ // Paper end - rewrite chunk system + return true; + } + } +@@ -79,6 +83,7 @@ public class ChunkStorage implements AutoCloseable { + + public CompoundTag upgradeChunkTag(ResourceKey resourcekey, Supplier supplier, CompoundTag nbttagcompound, Optional>> optional, ChunkPos pos, @Nullable LevelAccessor generatoraccess) { + // CraftBukkit end ++ nbttagcompound = nbttagcompound.copy(); // Paper - defensive copy, another thread might modify this + int i = ChunkStorage.getVersion(nbttagcompound); + + try { +@@ -97,9 +102,11 @@ public class ChunkStorage implements AutoCloseable { + if (i < 1493) { + ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.CHUNK, nbttagcompound, i, 1493); // Paper - replace chunk converter + if (nbttagcompound.getCompound("Level").getBoolean("hasLegacyStructureData")) { ++ synchronized (this.persistentDataLock) { // Paper - Async chunk loading + LegacyStructureDataHandler persistentstructurelegacy = this.getLegacyStructureHandler(resourcekey, supplier); + + nbttagcompound = persistentstructurelegacy.updateFromLegacy(nbttagcompound); ++ } // Paper - Async chunk loading + } + } + +@@ -139,7 +146,7 @@ public class ChunkStorage implements AutoCloseable { + LegacyStructureDataHandler persistentstructurelegacy = this.legacyStructureHandler; + + if (persistentstructurelegacy == null) { +- synchronized (this) { ++ synchronized (this.persistentDataLock) { // Paper - async chunk loading + persistentstructurelegacy = this.legacyStructureHandler; + if (persistentstructurelegacy == null) { + this.legacyStructureHandler = persistentstructurelegacy = LegacyStructureDataHandler.getLegacyStructureHandler(worldKey, (DimensionDataStorage) stateManagerGetter.get()); +@@ -165,10 +172,20 @@ public class ChunkStorage implements AutoCloseable { + } + + public CompletableFuture> read(ChunkPos chunkPos) { +- return this.worker.loadAsync(chunkPos); ++ // Paper start - async chunk io ++ try { ++ return CompletableFuture.completedFuture(Optional.ofNullable(this.readSync(chunkPos))); ++ } catch (Throwable thr) { ++ return CompletableFuture.failedFuture(thr); ++ } ++ } ++ @Nullable ++ public CompoundTag readSync(ChunkPos chunkPos) throws IOException { ++ return this.regionFileCache.read(chunkPos); + } ++ // Paper end - async chunk io + +- public CompletableFuture write(ChunkPos chunkPos, CompoundTag nbt) { ++ public CompletableFuture write(ChunkPos chunkPos, CompoundTag nbt) throws IOException { // Paper - rewrite chunk system; async chunk io + // Paper start - guard against serializing mismatching coordinates + if (nbt != null && !chunkPos.equals(ChunkSerializer.getChunkCoordinate(nbt))) { + final String world = (this instanceof net.minecraft.server.level.ChunkMap) ? ((net.minecraft.server.level.ChunkMap) this).level.getWorld().getName() : null; +@@ -176,26 +193,39 @@ public class ChunkStorage implements AutoCloseable { + + " but compound says coordinate is " + ChunkSerializer.getChunkCoordinate(nbt) + (world == null ? " for an unknown world" : (" for world: " + world))); + } + // Paper end - guard against serializing mismatching coordinates ++ this.regionFileCache.write(chunkPos, nbt); // Paper - rewrite chunk system; async chunk io, move above legacy structure index + this.handleLegacyStructureIndex(chunkPos); +- return this.worker.store(chunkPos, nbt); ++ // Paper - rewrite chunk system; async chunk io, move above legacy structure index ++ return null; + } + + protected void handleLegacyStructureIndex(ChunkPos chunkPos) { + if (this.legacyStructureHandler != null) { ++ synchronized (this.persistentDataLock) { // Paper - rewrite chunk system; async chunk io + this.legacyStructureHandler.removeIndex(chunkPos.toLong()); ++ } // Paper - rewrite chunk system; async chunk io + } + + } + + public void flushWorker() { +- this.worker.synchronize(true).join(); ++ io.papermc.paper.chunk.system.io.RegionFileIOThread.flush(); // Paper - rewrite chunk system + } + + public void close() throws IOException { +- this.worker.close(); ++ this.regionFileCache.close(); // Paper - nuke IO worker + } + + public ChunkScanAccess chunkScanner() { +- return this.worker; ++ // Paper start - nuke IO worker ++ return ((chunkPos, streamTagVisitor) -> { ++ try { ++ this.regionFileCache.scanChunk(chunkPos, streamTagVisitor); ++ return java.util.concurrent.CompletableFuture.completedFuture(null); ++ } catch (IOException e) { ++ throw new RuntimeException(e); ++ } ++ }); ++ // Paper end + } + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java +index 49d8a62d2b6ca6da4e02b3cec7e42c38b7781b57..9fdf8f857a5f9b231c6d0633eaba498244214f74 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java +@@ -27,43 +27,30 @@ public class EntityStorage implements EntityPersistentStorage { + private static final String ENTITIES_TAG = "Entities"; + private static final String POSITION_TAG = "Position"; + public final ServerLevel level; +- private final SimpleRegionStorage simpleRegionStorage; ++ // Paper - rewrite chunk system + private final LongSet emptyChunks = new LongOpenHashSet(); +- public final ProcessorMailbox entityDeserializerQueue; ++ // Paper - rewrite chunk system + + public EntityStorage(SimpleRegionStorage storage, ServerLevel world, Executor executor) { +- this.simpleRegionStorage = storage; ++ // Paper - rewrite chunk system + this.level = world; +- this.entityDeserializerQueue = ProcessorMailbox.create(executor, "entity-deserializer"); ++ // Paper - rewrite chunk system + } + + @Override + public CompletableFuture> loadEntities(ChunkPos pos) { +- return this.emptyChunks.contains(pos.toLong()) +- ? CompletableFuture.completedFuture(emptyChunk(pos)) +- : this.simpleRegionStorage.read(pos).thenApplyAsync(nbt -> { +- if (nbt.isEmpty()) { +- this.emptyChunks.add(pos.toLong()); +- return emptyChunk(pos); +- } else { +- try { +- ChunkPos chunkPos2 = readChunkPos(nbt.get()); +- if (!Objects.equals(pos, chunkPos2)) { +- LOGGER.error("Chunk file at {} is in the wrong location. (Expected {}, got {})", pos, pos, chunkPos2); +- } +- } catch (Exception var6) { +- LOGGER.warn("Failed to parse chunk {} position info", pos, var6); +- } +- +- CompoundTag compoundTag = this.simpleRegionStorage.upgradeChunkTag(nbt.get(), -1); +- ListTag listTag = compoundTag.getList("Entities", 10); +- List list = EntityType.loadEntitiesRecursive(listTag, this.level).collect(ImmutableList.toImmutableList()); +- return new ChunkEntities<>(pos, list); +- } +- }, this.entityDeserializerQueue::tell); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - copy out read logic into readEntities + } + +- private static ChunkPos readChunkPos(CompoundTag chunkNbt) { ++ // Paper start - rewrite chunk system ++ public static List readEntities(ServerLevel level, CompoundTag compoundTag) { ++ ListTag listTag = compoundTag.getList("Entities", 10); ++ List list = EntityType.loadEntitiesRecursive(listTag, level).collect(ImmutableList.toImmutableList()); ++ return list; ++ } ++ // Paper end - rewrite chunk system ++ ++ public static ChunkPos readChunkPos(CompoundTag chunkNbt) { // Paper - public + int[] is = chunkNbt.getIntArray("Position"); + return new ChunkPos(is[0], is[1]); + } +@@ -78,38 +65,74 @@ public class EntityStorage implements EntityPersistentStorage { + + @Override + public void storeEntities(ChunkEntities dataList) { ++ // Paper start - rewrite chunk system ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ // Paper end - rewrite chunk system + ChunkPos chunkPos = dataList.getPos(); + if (dataList.isEmpty()) { + if (this.emptyChunks.add(chunkPos.toLong())) { +- this.simpleRegionStorage.write(chunkPos, null); ++ // Paper - rewrite chunk system - fix compile for unused field in dead code + } + } else { +- ListTag listTag = new ListTag(); +- dataList.getEntities().forEach(entity -> { +- CompoundTag compoundTagx = new CompoundTag(); +- if (entity.save(compoundTagx)) { +- listTag.add(compoundTagx); +- } +- }); +- CompoundTag compoundTag = NbtUtils.addCurrentDataVersion(new CompoundTag()); +- compoundTag.put("Entities", listTag); +- writeChunkPos(compoundTag, chunkPos); +- this.simpleRegionStorage.write(chunkPos, compoundTag).exceptionally(ex -> { +- LOGGER.error("Failed to store chunk {}", chunkPos, ex); +- return null; +- }); ++ // Paper - move into saveEntityChunk0 + this.emptyChunks.remove(chunkPos.toLong()); + } + } + ++ // Paper start - rewrite chunk system ++ public static void copyEntities(final CompoundTag from, final CompoundTag into) { ++ if (from == null) { ++ return; ++ } ++ final ListTag entitiesFrom = from.getList("Entities", net.minecraft.nbt.Tag.TAG_COMPOUND); ++ if (entitiesFrom == null || entitiesFrom.isEmpty()) { ++ return; ++ } ++ ++ final ListTag entitiesInto = into.getList("Entities", net.minecraft.nbt.Tag.TAG_COMPOUND); ++ into.put("Entities", entitiesInto); // this is in case into doesn't have any entities ++ entitiesInto.addAll(0, entitiesFrom.copy()); // need to copy, this is coming from the save thread ++ } ++ ++ public static CompoundTag saveEntityChunk(List entities, ChunkPos chunkPos, ServerLevel level) { ++ return saveEntityChunk0(entities, chunkPos, level, false); ++ } ++ private static CompoundTag saveEntityChunk0(List entities, ChunkPos chunkPos, ServerLevel level, boolean force) { ++ if (!force && entities.isEmpty()) { ++ return null; ++ } ++ ++ ListTag listTag = new ListTag(); ++ entities.forEach((entity) -> { // diff here: use entities parameter ++ CompoundTag compoundTag = new CompoundTag(); ++ if (entity.save(compoundTag)) { ++ listTag.add(compoundTag); ++ } ++ ++ }); ++ CompoundTag compoundTag = NbtUtils.addCurrentDataVersion(new CompoundTag()); ++ compoundTag.put("Entities", listTag); ++ writeChunkPos(compoundTag, chunkPos); ++ // Paper - remove worker usage ++ ++ return !force && listTag.isEmpty() ? null : compoundTag; ++ } ++ ++ public static CompoundTag upgradeChunkTag(CompoundTag chunkNbt) { ++ int i = NbtUtils.getDataVersion(chunkNbt, -1); ++ return ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.ENTITY_CHUNK, chunkNbt, i, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()); ++ } ++ // Paper end - rewrite chunk system ++ + @Override + public void flush(boolean sync) { +- this.simpleRegionStorage.synchronize(sync).join(); +- this.entityDeserializerQueue.runAll(); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + + @Override + public void close() throws IOException { +- this.simpleRegionStorage.close(); ++ throw new UnsupportedOperationException(); // Paper - rewrite chunk system + } + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java +index e858436bcf1b234d4bc6e6a117f5224d5c2d9f90..307196b2a58d4f8db3e6e3c3517a8004d4908b13 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java +@@ -48,6 +48,7 @@ public class RegionFile implements AutoCloseable { + private final IntBuffer timestamps; + @VisibleForTesting + protected final RegionBitmap usedSectors; ++ public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(); // Paper + + public RegionFile(RegionStorageInfo storageKey, Path directory, Path path, boolean dsync) throws IOException { + this(storageKey, directory, path, RegionFileVersion.getCompressionFormat(), dsync); // Paper - Configurable region compression format +@@ -250,7 +251,7 @@ public class RegionFile implements AutoCloseable { + return (byteCount + 4096 - 1) / 4096; + } + +- public boolean doesChunkExist(ChunkPos pos) { ++ public synchronized boolean doesChunkExist(ChunkPos pos) { // Paper - synchronized + int i = this.getOffset(pos); + + if (i == 0) { +@@ -417,6 +418,11 @@ public class RegionFile implements AutoCloseable { + } + + public void close() throws IOException { ++ // Paper start - Prevent regionfiles from being closed during use ++ this.fileLock.lock(); ++ synchronized (this) { ++ try { ++ // Paper end + try { + this.padToFullSector(); + } finally { +@@ -426,6 +432,10 @@ public class RegionFile implements AutoCloseable { + this.file.close(); + } + } ++ } finally { // Paper start - Prevent regionfiles from being closed during use ++ this.fileLock.unlock(); ++ } ++ } // Paper end + + } + +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +index c4eef3aade889c69cefd873bec2d031cc54103ea..3f6955be976064eb542b5c50a9d6d74457c1833c 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +@@ -26,31 +26,99 @@ public class RegionFileStorage implements AutoCloseable { + private final Path folder; + private final boolean sync; + +- RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) { ++ // Paper start - cache regionfile does not exist state ++ static final int MAX_NON_EXISTING_CACHE = 1024 * 64; ++ private final it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet nonExistingRegionFiles = new it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet(); ++ private synchronized boolean doesRegionFilePossiblyExist(long position) { ++ if (this.nonExistingRegionFiles.contains(position)) { ++ this.nonExistingRegionFiles.addAndMoveToFirst(position); ++ return false; ++ } ++ return true; ++ } ++ ++ private synchronized void createRegionFile(long position) { ++ this.nonExistingRegionFiles.remove(position); ++ } ++ ++ private synchronized void markNonExisting(long position) { ++ if (this.nonExistingRegionFiles.addAndMoveToFirst(position)) { ++ while (this.nonExistingRegionFiles.size() >= MAX_NON_EXISTING_CACHE) { ++ this.nonExistingRegionFiles.removeLastLong(); ++ } ++ } ++ } ++ ++ public synchronized boolean doesRegionFileNotExistNoIO(ChunkPos pos) { ++ long key = ChunkPos.asLong(pos.getRegionX(), pos.getRegionZ()); ++ return !this.doesRegionFilePossiblyExist(key); ++ } ++ // Paper end - cache regionfile does not exist state ++ ++ protected RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) { // Paper - protected constructor + this.folder = directory; + this.sync = dsync; + this.info = storageKey; + } + +- private RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit +- long i = ChunkPos.asLong(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ()); ++ // Paper start ++ public synchronized RegionFile getRegionFileIfLoaded(ChunkPos chunkcoordintpair) { ++ return this.regionCache.getAndMoveToFirst(ChunkPos.asLong(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ())); ++ } ++ ++ public synchronized boolean chunkExists(ChunkPos pos) throws IOException { ++ RegionFile regionfile = getRegionFile(pos, true); ++ ++ return regionfile != null ? regionfile.hasChunk(pos) : false; ++ } ++ ++ public synchronized RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit ++ return this.getRegionFile(chunkcoordintpair, existingOnly, false); ++ } ++ public synchronized RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly, boolean lock) throws IOException { ++ // Paper end ++ long i = ChunkPos.asLong(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ()); final long regionPos = i; // Paper - OBFHELPER + RegionFile regionfile = (RegionFile) this.regionCache.getAndMoveToFirst(i); + + if (regionfile != null) { ++ // Paper start ++ if (lock) { ++ // must be in this synchronized block ++ regionfile.fileLock.lock(); ++ } ++ // Paper end + return regionfile; + } else { ++ // Paper start - cache regionfile does not exist state ++ if (existingOnly && !this.doesRegionFilePossiblyExist(regionPos)) { ++ return null; ++ } ++ // Paper end - cache regionfile does not exist state + if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper - Sanitise RegionFileCache and make configurable + ((RegionFile) this.regionCache.removeLast()).close(); + } + +- FileUtil.createDirectoriesSafe(this.folder); ++ // Paper - only create directory if not existing only - moved down + Path path = this.folder; + int j = chunkcoordintpair.getRegionX(); + Path path1 = path.resolve("r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca"); +- if (existingOnly && !java.nio.file.Files.exists(path1)) return null; // CraftBukkit ++ if (existingOnly && !java.nio.file.Files.exists(path1)) { // Paper start - cache regionfile does not exist state ++ this.markNonExisting(regionPos); ++ return null; // CraftBukkit ++ } else { ++ this.createRegionFile(regionPos); ++ } ++ // Paper end - cache regionfile does not exist state ++ FileUtil.createDirectoriesSafe(this.folder); // Paper - only create directory if not existing only - moved from above + RegionFile regionfile1 = new RegionFile(this.info, path1, this.folder, this.sync); + + this.regionCache.putAndMoveToFirst(i, regionfile1); ++ // Paper start ++ if (lock) { ++ // must be in this synchronized block ++ regionfile1.fileLock.lock(); ++ } ++ // Paper end + return regionfile1; + } + } +@@ -58,11 +126,12 @@ public class RegionFileStorage implements AutoCloseable { + @Nullable + public CompoundTag read(ChunkPos pos) throws IOException { + // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing +- RegionFile regionfile = this.getRegionFile(pos, true); ++ RegionFile regionfile = this.getRegionFile(pos, true, true); // Paper + if (regionfile == null) { + return null; + } + // CraftBukkit end ++ try { // Paper + DataInputStream datainputstream = regionfile.getChunkDataInputStream(pos); + + CompoundTag nbttagcompound; +@@ -99,6 +168,9 @@ public class RegionFileStorage implements AutoCloseable { + } + + return nbttagcompound; ++ } finally { // Paper start ++ regionfile.fileLock.unlock(); ++ } // Paper end + } + + public void scanChunk(ChunkPos chunkPos, StreamTagVisitor scanner) throws IOException { +@@ -133,7 +205,13 @@ public class RegionFileStorage implements AutoCloseable { + } + + protected void write(ChunkPos pos, @Nullable CompoundTag nbt) throws IOException { +- RegionFile regionfile = this.getRegionFile(pos, false); // CraftBukkit ++ // Paper start - rewrite chunk system ++ RegionFile regionfile = this.getRegionFile(pos, nbt == null, true); // CraftBukkit ++ if (nbt == null && regionfile == null) { ++ return; ++ } ++ try { // Try finally to unlock the region file ++ // Paper end - rewrite chunk system + // Paper start - Chunk save reattempt + int attempts = 0; + Exception lastException = null; +@@ -179,9 +257,14 @@ public class RegionFileStorage implements AutoCloseable { + net.minecraft.server.MinecraftServer.LOGGER.error("Failed to save chunk {}", pos, lastException); + } + // Paper end - Chunk save reattempt ++ // Paper start - rewrite chunk system ++ } finally { ++ regionfile.fileLock.unlock(); ++ } ++ // Paper end - rewrite chunk system + } + +- public void close() throws IOException { ++ public synchronized void close() throws IOException { // Paper -> synchronized + ExceptionCollector exceptionsuppressor = new ExceptionCollector<>(); + ObjectIterator objectiterator = this.regionCache.values().iterator(); + +@@ -198,7 +281,7 @@ public class RegionFileStorage implements AutoCloseable { + exceptionsuppressor.throwIfPresent(); + } + +- public void flush() throws IOException { ++ public synchronized void flush() throws IOException { // Paper - synchronize + ObjectIterator objectiterator = this.regionCache.values().iterator(); + + while (objectiterator.hasNext()) { +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java +index 151fcbca34e02783e19fbb7b54ec4fbec2eed190..883fbe5c81e3be27007a1a0489f80ba1863e5a04 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java +@@ -12,6 +12,7 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectMap; + import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; + import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; + import java.io.IOException; ++import java.nio.file.Path; + import java.util.Map; + import java.util.Optional; + import java.util.concurrent.CompletableFuture; +@@ -31,25 +32,30 @@ import net.minecraft.world.level.ChunkPos; + import net.minecraft.world.level.LevelHeightAccessor; + import org.slf4j.Logger; + +-public class SectionStorage implements AutoCloseable { ++public class SectionStorage extends RegionFileStorage implements AutoCloseable { // Paper - nuke IOWorker + private static final Logger LOGGER = LogUtils.getLogger(); + private static final String SECTIONS_TAG = "Sections"; +- private final SimpleRegionStorage simpleRegionStorage; ++ // Paper - remove mojang I/O thread + private final Long2ObjectMap> storage = new Long2ObjectOpenHashMap<>(); + private final LongLinkedOpenHashSet dirty = new LongLinkedOpenHashSet(); + private final Function> codec; + private final Function factory; +- private final RegistryAccess registryAccess; ++ public final RegistryAccess registryAccess; // Paper - rewrite chunk system - public + protected final LevelHeightAccessor levelHeightAccessor; + + public SectionStorage( ++ // Paper start ++ RegionStorageInfo regionStorageInfo, ++ Path path, ++ boolean dsync, ++ // Paper end + SimpleRegionStorage storageAccess, + Function> codecFactory, + Function factory, + RegistryAccess registryManager, + LevelHeightAccessor world + ) { +- this.simpleRegionStorage = storageAccess; ++ super(regionStorageInfo, path, dsync); // Paper - remove mojang I/O thread + this.codec = codecFactory; + this.factory = factory; + this.registryAccess = registryManager; +@@ -112,23 +118,21 @@ public class SectionStorage implements AutoCloseable { + } + + private void readColumn(ChunkPos pos) { +- Optional optional = this.tryRead(pos).join(); +- RegistryOps registryOps = this.registryAccess.createSerializationContext(NbtOps.INSTANCE); +- this.readColumn(pos, registryOps, optional.orElse(null)); ++ throw new IllegalStateException("Only chunk system can load in state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system + } + + private CompletableFuture> tryRead(ChunkPos pos) { +- return this.simpleRegionStorage.read(pos).exceptionally(throwable -> { +- if (throwable instanceof IOException iOException) { +- LOGGER.error("Error reading chunk {} data from disk", pos, iOException); +- return Optional.empty(); +- } else { +- throw new CompletionException(throwable); +- } +- }); ++ // Paper start - rewrite chunk system ++ try { ++ return CompletableFuture.completedFuture(Optional.ofNullable(this.read(pos))); ++ } catch (Throwable thr) { ++ return CompletableFuture.failedFuture(thr); ++ } ++ // Paper end - rewrite chunk system + } + + private void readColumn(ChunkPos pos, RegistryOps ops, @Nullable CompoundTag nbt) { ++ if (true) throw new IllegalStateException("Only chunk system can load in state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system + if (nbt == null) { + for (int i = this.levelHeightAccessor.getMinSection(); i < this.levelHeightAccessor.getMaxSection(); i++) { + this.storage.put(getKey(pos, i), Optional.empty()); +@@ -138,7 +142,7 @@ public class SectionStorage implements AutoCloseable { + int j = getVersion(dynamic); + int k = SharedConstants.getCurrentVersion().getDataVersion().getVersion(); + boolean bl = j != k; +- Dynamic dynamic2 = this.simpleRegionStorage.upgradeChunkTag(dynamic, j); ++ Dynamic dynamic2 = null; // Paper - rewrite chunk system + OptionalDynamic optionalDynamic = dynamic2.get("Sections"); + + for (int l = this.levelHeightAccessor.getMinSection(); l < this.levelHeightAccessor.getMaxSection(); l++) { +@@ -162,7 +166,7 @@ public class SectionStorage implements AutoCloseable { + Dynamic dynamic = this.writeColumn(pos, registryOps); + Tag tag = dynamic.getValue(); + if (tag instanceof CompoundTag) { +- this.simpleRegionStorage.write(pos, (CompoundTag)tag); ++ try { this.write(pos, (CompoundTag)tag); } catch (IOException ex) { SectionStorage.LOGGER.error("Error writing poi chunk data to disk for chunk " + pos, ex); } // Paper - nuke IOWorker + } else { + LOGGER.error("Expected compound tag, got {}", tag); + } +@@ -212,7 +216,7 @@ public class SectionStorage implements AutoCloseable { + } + + private static int getVersion(Dynamic dynamic) { +- return dynamic.get("DataVersion").asInt(1945); ++ return dynamic.get("DataVersion").asInt(1945); // Paper - diff on change, constant used in ChunkLoadTask + } + + public void flush(ChunkPos pos) { +@@ -229,6 +233,6 @@ public class SectionStorage implements AutoCloseable { + + @Override + public void close() throws IOException { +- this.simpleRegionStorage.close(); ++ super.close(); // Paper - nuke I/O worker - don't call the worker + } + } +diff --git a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java +index 74a285b8b018a9c94ccea519f1ce8b9e2ef3cb64..83a39f900551e39d5af6f17a339a386ddee4feef 100644 +--- a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java ++++ b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java +@@ -9,52 +9,41 @@ import javax.annotation.Nullable; + import net.minecraft.world.entity.Entity; + + public class EntityTickList { +- private Int2ObjectMap active = new Int2ObjectLinkedOpenHashMap<>(); +- private Int2ObjectMap passive = new Int2ObjectLinkedOpenHashMap<>(); +- @Nullable +- private Int2ObjectMap iterated; ++ private final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet entities = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(true); // Paper - rewrite this, always keep this updated - why would we EVER tick an entity that's not ticking? + + private void ensureActiveIsNotIterated() { +- if (this.iterated == this.active) { +- this.passive.clear(); +- +- for (Entry entry : Int2ObjectMaps.fastIterable(this.active)) { +- this.passive.put(entry.getIntKey(), entry.getValue()); +- } +- +- Int2ObjectMap int2ObjectMap = this.active; +- this.active = this.passive; +- this.passive = int2ObjectMap; +- } ++ // Paper - replace with better logic, do not delay removals + } + + public void add(Entity entity) { ++ io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist addition"); // Paper + this.ensureActiveIsNotIterated(); +- this.active.put(entity.getId(), entity); ++ this.entities.add(entity); // Paper - replace with better logic, do not delay removals/additions + } + + public void remove(Entity entity) { ++ io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist removal"); // Paper + this.ensureActiveIsNotIterated(); +- this.active.remove(entity.getId()); ++ this.entities.remove(entity); // Paper - replace with better logic, do not delay removals/additions + } + + public boolean contains(Entity entity) { +- return this.active.containsKey(entity.getId()); ++ return this.entities.contains(entity); // Paper - replace with better logic, do not delay removals/additions + } + + public void forEach(Consumer action) { +- if (this.iterated != null) { +- throw new UnsupportedOperationException("Only one concurrent iteration supported"); +- } else { +- this.iterated = this.active; +- +- try { +- for (Entity entity : this.active.values()) { +- action.accept(entity); +- } +- } finally { +- this.iterated = null; ++ io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist iteration"); // Paper ++ // Paper start - replace with better logic, do not delay removals/additions ++ // To ensure nothing weird happens with dimension travelling, do not iterate over new entries... ++ // (by dfl iterator() is configured to not iterate over new entries) ++ io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.Iterator iterator = this.entities.iterator(); ++ try { ++ while (iterator.hasNext()) { ++ action.accept(iterator.next()); + } ++ } finally { ++ iterator.finishedIterating(); + } ++ // Paper end - replace with better logic, do not delay removals/additions + } + } +diff --git a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java +index 5d15c228c044a36c67014793decb314240cf6be1..dc765b92cc90f5f370254e68bbbdfa5add7935ce 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java +@@ -87,7 +87,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator { + return CompletableFuture.supplyAsync(Util.wrapThreadWithTaskName("init_biomes", () -> { + this.doCreateBiomes(blender, noiseConfig, structureAccessor, chunk); + return chunk; +- }), Util.backgroundExecutor()); ++ }), executor); // Paper - run with supplied executor + } + + private void doCreateBiomes(Blender blender, RandomState noiseConfig, StructureManager structureAccessor, ChunkAccess chunk) { +@@ -286,7 +286,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator { + + return CompletableFuture.supplyAsync(Util.wrapThreadWithTaskName("wgen_fill_noise", () -> { + return this.doFill(blender, structureAccessor, noiseConfig, chunk, j, k); +- }), Util.backgroundExecutor()).whenCompleteAsync((ichunkaccess1, throwable) -> { ++ }), executor).whenCompleteAsync((ichunkaccess1, throwable) -> { // Paper - run with supplied executor + Iterator iterator = set.iterator(); + + while (iterator.hasNext()) { +diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java +index 609100ed7aa0b23aa5a9c6fbf6878ea320bd3a93..7068657b28a9bc175ee23f5a18defb41168f1d76 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java +@@ -47,8 +47,101 @@ public class StructureCheck { + private final BiomeSource biomeSource; + private final long seed; + private final DataFixer fixerUpper; +- private final Long2ObjectMap> loadedChunks = new Long2ObjectOpenHashMap<>(); +- private final Map featureChecks = new HashMap<>(); ++ // Paper start - rewrite chunk system - synchronise this class ++ // additionally, make sure to purge entries from the maps so it does not leak memory ++ private static final int CHUNK_TOTAL_LIMIT = 50 * (2 * 100 + 1) * (2 * 100 + 1); // cache 50 structure lookups ++ private static final int PER_FEATURE_CHECK_LIMIT = 50 * (2 * 100 + 1) * (2 * 100 + 1); // cache 50 structure lookups ++ ++ private final SynchronisedLong2ObjectMap> loadedChunksSafe = new SynchronisedLong2ObjectMap<>(CHUNK_TOTAL_LIMIT); ++ private final java.util.concurrent.ConcurrentHashMap featureChecksSafe = new java.util.concurrent.ConcurrentHashMap<>(); ++ ++ private static final class SynchronisedLong2ObjectMap { ++ private final it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap map = new it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap<>(); ++ private final int limit; ++ ++ public SynchronisedLong2ObjectMap(final int limit) { ++ this.limit = limit; ++ } ++ ++ // must hold lock on map ++ private void purgeEntries() { ++ while (this.map.size() > this.limit) { ++ this.map.removeLast(); ++ } ++ } ++ ++ public V get(final long key) { ++ synchronized (this.map) { ++ return this.map.getAndMoveToFirst(key); ++ } ++ } ++ ++ public V put(final long key, final V value) { ++ synchronized (this.map) { ++ final V ret = this.map.putAndMoveToFirst(key, value); ++ this.purgeEntries(); ++ return ret; ++ } ++ } ++ ++ public V compute(final long key, final java.util.function.BiFunction remappingFunction) { ++ synchronized (this.map) { ++ // first, compute the value - if one is added, it will be at the last entry ++ this.map.compute(key, remappingFunction); ++ // move the entry to first, just in case it was added at last ++ final V ret = this.map.getAndMoveToFirst(key); ++ // now purge the last entries ++ this.purgeEntries(); ++ ++ return ret; ++ } ++ } ++ } ++ ++ private static final class SynchronisedLong2BooleanMap { ++ private final it.unimi.dsi.fastutil.longs.Long2BooleanLinkedOpenHashMap map = new it.unimi.dsi.fastutil.longs.Long2BooleanLinkedOpenHashMap(); ++ private final int limit; ++ ++ public SynchronisedLong2BooleanMap(final int limit) { ++ this.limit = limit; ++ } ++ ++ // must hold lock on map ++ private void purgeEntries() { ++ while (this.map.size() > this.limit) { ++ this.map.removeLastBoolean(); ++ } ++ } ++ ++ public boolean remove(final long key) { ++ synchronized (this.map) { ++ return this.map.remove(key); ++ } ++ } ++ ++ // note: ++ public boolean getOrCompute(final long key, final it.unimi.dsi.fastutil.longs.Long2BooleanFunction ifAbsent) { ++ synchronized (this.map) { ++ if (this.map.containsKey(key)) { ++ return this.map.getAndMoveToFirst(key); ++ } ++ } ++ ++ final boolean put = ifAbsent.get(key); ++ ++ synchronized (this.map) { ++ if (this.map.containsKey(key)) { ++ return this.map.getAndMoveToFirst(key); ++ } ++ this.map.putAndMoveToFirst(key, put); ++ ++ this.purgeEntries(); ++ ++ return put; ++ } ++ } ++ } ++ // Paper end - rewrite chunk system - synchronise this class + + public StructureCheck( + ChunkScanAccess chunkIoWorker, +@@ -90,7 +183,7 @@ public class StructureCheck { + + public StructureCheckResult checkStart(ChunkPos pos, Structure type, StructurePlacement placement, boolean skipReferencedStructures) { + long l = pos.toLong(); +- Object2IntMap object2IntMap = this.loadedChunks.get(l); ++ Object2IntMap object2IntMap = this.loadedChunksSafe.get(l); // Paper - rewrite chunk system - synchronise this class + if (object2IntMap != null) { + return this.checkStructureInfo(object2IntMap, type, skipReferencedStructures); + } else { +@@ -100,9 +193,9 @@ public class StructureCheck { + } else if (!placement.applyAdditionalChunkRestrictions(pos.x, pos.z, this.seed, this.getSaltOverride(type))) { // Paper - add missing structure seed configs + return StructureCheckResult.START_NOT_PRESENT; + } else { +- boolean bl = this.featureChecks +- .computeIfAbsent(type, structure2 -> new Long2BooleanOpenHashMap()) +- .computeIfAbsent(l, chunkPos -> this.canCreateStructure(pos, type)); ++ boolean bl = this.featureChecksSafe // Paper - rewrite chunk system - synchronise this class ++ .computeIfAbsent(type, structure2 -> new SynchronisedLong2BooleanMap(PER_FEATURE_CHECK_LIMIT)) // Paper - rewrite chunk system - synchronise this class ++ .getOrCompute(l, chunkPos -> this.canCreateStructure(pos, type)); // Paper - rewrite chunk system - synchronise this class + return !bl ? StructureCheckResult.START_NOT_PRESENT : StructureCheckResult.CHUNK_LOAD_NEEDED; + } + } +@@ -228,15 +321,26 @@ public class StructureCheck { + } + + private void storeFullResults(long pos, Object2IntMap referencesByStructure) { +- this.loadedChunks.put(pos, deduplicateEmptyMap(referencesByStructure)); +- this.featureChecks.values().forEach(generationPossibilityByChunkPos -> generationPossibilityByChunkPos.remove(pos)); ++ // Paper start - rewrite chunk system - synchronise this class ++ this.loadedChunksSafe.put(pos, deduplicateEmptyMap(referencesByStructure)); ++ // once we insert into loadedChunks, we don't really need to be very careful about removing everything ++ // from this map, as everything that checks this map uses loadedChunks first ++ // so, one way or another it's a race condition that doesn't matter ++ for (SynchronisedLong2BooleanMap value : this.featureChecksSafe.values()) { ++ value.remove(pos); ++ } ++ // Paper end - rewrite chunk system - synchronise this class + } + + public void incrementReference(ChunkPos pos, Structure structure) { +- this.loadedChunks.compute(pos.toLong(), (posx, referencesByStructure) -> { +- if (referencesByStructure == null || referencesByStructure.isEmpty()) { ++ this.loadedChunksSafe.compute(pos.toLong(), (posx, referencesByStructure) -> { // Paper start - rewrite chunk system - synchronise this class ++ // make this COW so that we do not mutate state that may be currently in use ++ if (referencesByStructure == null) { + referencesByStructure = new Object2IntOpenHashMap<>(); ++ } else { ++ referencesByStructure = referencesByStructure instanceof Object2IntOpenHashMap fastClone ? fastClone.clone() : new Object2IntOpenHashMap<>(referencesByStructure); + } ++ // Paper end - rewrite chunk system - synchronise this class + + referencesByStructure.computeInt(structure, (feature, references) -> references == null ? 1 : references + 1); + return referencesByStructure; +diff --git a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java +index 47c2b2da9799690291396effb9e1b06d71efc6fd..2cdd18f724296f10cd4a522d1e8196723d39cf45 100644 +--- a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java ++++ b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java +@@ -26,6 +26,19 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon + @Nullable + private BiConsumer, ScheduledTick> onTickAdded; + ++ // Paper start - add dirty flag ++ private boolean dirty; ++ private long lastSaved = Long.MIN_VALUE; ++ ++ public boolean isDirty(final long tick) { ++ return this.dirty || (!this.tickQueue.isEmpty() && tick != this.lastSaved); ++ } ++ ++ public void clearDirty() { ++ this.dirty = false; ++ } ++ // Paper end - add dirty flag ++ + public LevelChunkTicks() { + } + +@@ -50,6 +63,7 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon + public ScheduledTick poll() { + ScheduledTick scheduledTick = this.tickQueue.poll(); + if (scheduledTick != null) { ++ this.dirty = true; // Paper - add dirty flag + this.ticksPerPosition.remove(scheduledTick); + } + +@@ -59,6 +73,7 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon + @Override + public void schedule(ScheduledTick orderedTick) { + if (this.ticksPerPosition.add(orderedTick)) { ++ this.dirty = true; // Paper - add dirty flag + this.scheduleUnchecked(orderedTick); + } + } +@@ -81,7 +96,7 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon + while (iterator.hasNext()) { + ScheduledTick scheduledTick = iterator.next(); + if (predicate.test(scheduledTick)) { +- iterator.remove(); ++ iterator.remove(); this.dirty = true; // Paper - add dirty flag + this.ticksPerPosition.remove(scheduledTick); + } + } +@@ -98,6 +113,7 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon + + @Override + public ListTag save(long l, Function function) { ++ this.lastSaved = l; // Paper - add dirty system to level ticks + ListTag listTag = new ListTag(); + if (this.pendingTicks != null) { + for (SavedTick savedTick : this.pendingTicks) { +@@ -114,6 +130,11 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon + + public void unpack(long time) { + if (this.pendingTicks != null) { ++ // Paper start - add dirty system to level chunk ticks ++ if (this.tickQueue.isEmpty()) { ++ this.lastSaved = time; ++ } ++ // Paper end - add dirty system to level chunk ticks + int i = -this.pendingTicks.size(); + + for (SavedTick savedTick : this.pendingTicks) { +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +index 7dae8d91b74cc7df0745f0c121e3bea09b8d0b6d..1e2530c9e5212b6d2bdbc94817beddb4247dac73 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +@@ -115,7 +115,7 @@ public class CraftChunk implements Chunk { + + @Override + public boolean isEntitiesLoaded() { +- return this.getCraftWorld().getHandle().entityManager.areEntitiesLoaded(ChunkPos.asLong(this.x, this.z)); ++ return this.getCraftWorld().getHandle().areEntitiesLoaded(io.papermc.paper.util.CoordinateUtils.getChunkKey(this.x, this.z)); // Paper - rewrite chunk system + } + + @Override +@@ -124,51 +124,7 @@ public class CraftChunk implements Chunk { + this.getWorld().getChunkAt(this.x, this.z); // Transient load for this tick + } + +- PersistentEntitySectionManager entityManager = this.getCraftWorld().getHandle().entityManager; +- long pair = ChunkPos.asLong(this.x, this.z); +- +- if (entityManager.areEntitiesLoaded(pair)) { +- return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream() +- .map(net.minecraft.world.entity.Entity::getBukkitEntity) +- .filter(Objects::nonNull).toArray(Entity[]::new); +- } +- +- entityManager.ensureChunkQueuedForLoad(pair); // Start entity loading +- +- // SPIGOT-6772: Use entity mailbox and re-schedule entities if they get unloaded +- ProcessorMailbox mailbox = ((EntityStorage) entityManager.permanentStorage).entityDeserializerQueue; +- BooleanSupplier supplier = () -> { +- // only execute inbox if our entities are not present +- if (entityManager.areEntitiesLoaded(pair)) { +- return true; +- } +- +- if (!entityManager.isPending(pair)) { +- // Our entities got unloaded, this should normally not happen. +- entityManager.ensureChunkQueuedForLoad(pair); // Re-start entity loading +- } +- +- // tick loading inbox, which loads the created entities to the world +- // (if present) +- entityManager.tick(); +- // check if our entities are loaded +- return entityManager.areEntitiesLoaded(pair); +- }; +- +- // now we wait until the entities are loaded, +- // the converting from NBT to entity object is done on the main Thread which is why we wait +- while (!supplier.getAsBoolean()) { +- if (mailbox.size() != 0) { +- mailbox.run(); +- } else { +- Thread.yield(); +- LockSupport.parkNanos("waiting for entity loading", 100000L); +- } +- } +- +- return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream() +- .map(net.minecraft.world.entity.Entity::getBukkitEntity) +- .filter(Objects::nonNull).toArray(Entity[]::new); ++ return this.getCraftWorld().getHandle().getChunkEntities(this.x, this.z); // Paper - rewrite chunk system + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 5467a5985eaf159e2a4f19dd5723d05d8f2d598e..66fa93a44a348f9cd300ec0c4a2d09a7a2bf47e1 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -1407,7 +1407,6 @@ public final class CraftServer implements Server { + // Paper - Put world into worldlist before initing the world; move up + + this.getServer().prepareLevels(internal.getChunkSource().chunkMap.progressListener, internal); +- internal.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API + + this.pluginManager.callEvent(new WorldLoadEvent(internal.getWorld())); + return internal.getWorld(); +@@ -1452,7 +1451,7 @@ public final class CraftServer implements Server { + } + + handle.getChunkSource().close(save); +- handle.entityManager.close(save); // SPIGOT-6722: close entityManager ++ // handle.entityManager.close(save); // SPIGOT-6722: close entityManager // Paper - rewrite chunk system + handle.convertable.close(); + } catch (Exception ex) { + this.getLogger().log(Level.SEVERE, null, ex); +@@ -2488,7 +2487,7 @@ public final class CraftServer implements Server { + + @Override + public boolean isPrimaryThread() { +- return Thread.currentThread().equals(this.console.serverThread) || this.console.hasStopped() || !org.spigotmc.AsyncCatcher.enabled; // All bets are off if we have shut down (e.g. due to watchdog) ++ return io.papermc.paper.util.TickThread.isTickThread(); // Paper - rewrite chunk system + } + + // Paper start - Adventure +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 4b6a04e47f5d4c071607516519098fab317dcf12..01fc74e6cc8ea8808b821583afb26309587dc003 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -518,10 +518,14 @@ public class CraftWorld extends CraftRegionAccessor implements World { + ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z)); + if (playerChunk == null) return false; + +- playerChunk.getTickingChunkFuture().thenAccept(either -> { +- either.ifSuccess(chunk -> { ++ // Paper start - rewrite player chunk loader ++ net.minecraft.world.level.chunk.LevelChunk chunk = playerChunk.getSendingChunk(); ++ if (chunk == null) { ++ return false; ++ } ++ // Paper end - rewrite player chunk loader + List playersInRange = playerChunk.playerProvider.getPlayers(playerChunk.getPos(), false); +- if (playersInRange.isEmpty()) return; ++ if (playersInRange.isEmpty()) return true; // Paper - rewrite player chunk loader + + ClientboundLevelChunkWithLightPacket refreshPacket = new ClientboundLevelChunkWithLightPacket(chunk, this.world.getLightEngine(), null, null); + for (ServerPlayer player : playersInRange) { +@@ -529,8 +533,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + player.connection.send(refreshPacket); + } +- }); +- }); ++ // Paper - rewrite player chunk loader + + return true; + } +@@ -609,20 +612,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + @Override + public Collection getPluginChunkTickets(int x, int z) { + DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager; +- SortedArraySet> tickets = chunkDistanceManager.tickets.get(ChunkPos.asLong(x, z)); +- +- if (tickets == null) { +- return Collections.emptyList(); +- } +- +- ImmutableList.Builder ret = ImmutableList.builder(); +- for (Ticket ticket : tickets) { +- if (ticket.getType() == TicketType.PLUGIN_TICKET) { +- ret.add((Plugin) ticket.key); +- } +- } +- +- return ret.build(); ++ return chunkDistanceManager.getChunkHolderManager().getPluginChunkTickets(x, z); // Paper - rewrite chunk system + } + + @Override +@@ -630,7 +620,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + Map> ret = new HashMap<>(); + DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager; + +- for (Long2ObjectMap.Entry>> chunkTickets : chunkDistanceManager.tickets.long2ObjectEntrySet()) { ++ for (Long2ObjectMap.Entry>> chunkTickets : chunkDistanceManager.getChunkHolderManager().getTicketsCopy().long2ObjectEntrySet()) { // Paper - rewrite chunk system + long chunkKey = chunkTickets.getLongKey(); + SortedArraySet> tickets = chunkTickets.getValue(); + +@@ -1327,12 +1317,12 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public int getViewDistance() { +- return this.world.getChunkSource().chunkMap.serverViewDistance; ++ return this.getHandle().playerChunkLoader.getAPIViewDistance(); // Paper - replace player chunk loader + } + + @Override + public int getSimulationDistance() { +- return this.world.getChunkSource().chunkMap.getDistanceManager().simulationDistance; ++ return this.getHandle().playerChunkLoader.getAPITickDistance(); // Paper - replace player chunk loader + } + + public BlockMetadataStore getBlockMetadata() { +@@ -2495,17 +2485,20 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public void setSimulationDistance(final int simulationDistance) { +- throw new UnsupportedOperationException("Not implemented yet"); ++ if (simulationDistance < 2 || simulationDistance > 32) { ++ throw new IllegalArgumentException("Simulation distance " + simulationDistance + " is out of range of [2, 32]"); ++ } ++ this.getHandle().chunkSource.chunkMap.setTickViewDistance(simulationDistance); + } + + @Override + public int getSendViewDistance() { +- return this.getViewDistance(); ++ return this.getHandle().playerChunkLoader.getAPISendViewDistance(); // Paper - replace player chunk loader + } + + @Override + public void setSendViewDistance(final int viewDistance) { +- throw new UnsupportedOperationException("Not implemented yet"); ++ this.getHandle().chunkSource.chunkMap.setSendViewDistance(viewDistance); // Paper - replace player chunk loader + } + + // Paper start - implement pointers +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index e1af41a8d938420694a5342f08b811615dc46003..33c553d1558b3cdfac22948f3878e1a31d70f02b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -3457,31 +3457,31 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + + @Override + public int getViewDistance() { +- return io.papermc.paper.chunk.system.ChunkSystem.getLoadViewDistance(this.getHandle()); ++ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPIViewDistance(this); + } + + @Override + public void setViewDistance(final int viewDistance) { +- throw new UnsupportedOperationException("Not implemented yet"); ++ this.getHandle().setLoadViewDistance(viewDistance < 0 ? viewDistance : viewDistance + 1); + } + + @Override + public int getSimulationDistance() { +- return io.papermc.paper.chunk.system.ChunkSystem.getTickViewDistance(this.getHandle()); ++ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPITickViewDistance(this); + } + + @Override + public void setSimulationDistance(final int simulationDistance) { +- throw new UnsupportedOperationException("Not implemented yet"); ++ this.getHandle().setTickViewDistance(simulationDistance); + } + + @Override + public int getSendViewDistance() { +- return io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(this.getHandle()); ++ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPISendViewDistance(this); + } + + @Override + public void setSendViewDistance(final int viewDistance) { +- throw new UnsupportedOperationException("Not implemented yet"); ++ this.getHandle().setSendViewDistance(viewDistance); + } + } +diff --git a/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java b/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java +index b65710b648e31ab74204b5abd9397d9e6e26dac4..c77f722131e0e40e9de29bf8d42f9bc5d8fa2f7d 100644 +--- a/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java ++++ b/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java +@@ -264,7 +264,7 @@ public class CustomChunkGenerator extends InternalChunkGenerator { + return ichunkaccess1; + }; + +- return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), net.minecraft.Util.backgroundExecutor()) : future.thenApply(function); ++ return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), executor) : future.thenApply(function); // Paper - run with supplied executor + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java +index cd7f1309cf01a5f01a28aded03a36fe15adb1756..41a291d42667c38d3e5bbe47236772761e85929b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java +@@ -815,19 +815,39 @@ public abstract class DelegatedGeneratorAccess implements WorldGenLevel { + @Nullable + @Override + public BlockState getBlockStateIfLoaded(final BlockPos blockposition) { +- return null; ++ return this.handle.getBlockStateIfLoaded(blockposition); + } + + @Nullable + @Override + public FluidState getFluidIfLoaded(final BlockPos blockposition) { +- return null; ++ return this.handle.getFluidIfLoaded(blockposition); + } + + @Nullable + @Override + public ChunkAccess getChunkIfLoadedImmediately(final int x, final int z) { +- return null; ++ return this.handle.getChunkIfLoadedImmediately(x, z); ++ } ++ ++ @Override ++ public void getHardCollidingEntities(final Entity except, final AABB box, final Predicate predicate, final List into) { ++ this.handle.getHardCollidingEntities(except, box, predicate, into); ++ } ++ ++ @Override ++ public List getHardCollidingEntities(final Entity except, final AABB box, final Predicate predicate) { ++ return this.handle.getHardCollidingEntities(except, box, predicate); ++ } ++ ++ @Override ++ public void getEntities(final Entity except, final AABB box, final Predicate predicate, final List into) { ++ this.handle.getEntities(except, box, predicate, into); ++ } ++ ++ @Override ++ public void getEntitiesByClass(final Class clazz, final Entity except, final AABB box, final List into, final Predicate predicate) { ++ this.handle.getEntitiesByClass(clazz, except, box, into, predicate); + } + // Paper end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java +index e8a73d34dbb372581b03018aade170a31c266099..210f454a840aa5564f7cbf33b83d31aa74814c84 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java +@@ -268,4 +268,19 @@ public class DummyGeneratorAccess implements WorldGenLevel { + @Override + public void scheduleTick(BlockPos pos, Fluid fluid, int delay, net.minecraft.world.ticks.TickPriority priority) {} + // Paper end - add more methods ++ // Paper start ++ @Override ++ public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { ++ return java.util.Collections.emptyList(); ++ } ++ ++ @Override ++ public void getEntities(Entity except, AABB box, Predicate predicate, List into) {} ++ ++ @Override ++ public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) {} ++ ++ @Override ++ public void getEntitiesByClass(Class clazz, Entity except, AABB box, List into, Predicate predicate) {} ++ // Paper end + } +diff --git a/src/main/java/org/spigotmc/AsyncCatcher.java b/src/main/java/org/spigotmc/AsyncCatcher.java +index e8e3cc48cf1c58bd8151d1f28df28781859cd0e3..2e074c16dab1ead47914070329da0398c3274048 100644 +--- a/src/main/java/org/spigotmc/AsyncCatcher.java ++++ b/src/main/java/org/spigotmc/AsyncCatcher.java +@@ -9,7 +9,7 @@ public class AsyncCatcher + + public static void catchOp(String reason) + { +- if ( (AsyncCatcher.enabled || io.papermc.paper.util.TickThread.STRICT_THREAD_CHECKS) && Thread.currentThread() != MinecraftServer.getServer().serverThread ) // Paper ++ if (!(io.papermc.paper.util.TickThread.isTickThread())) // Paper + { + MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); // Paper + throw new IllegalStateException( "Asynchronous " + reason + "!" ); +diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java +index ad282d34919716b75acd10426cd071da9d064a51..9e5d08f57aa448552d100ca892c211d44441ef68 100644 +--- a/src/main/java/org/spigotmc/WatchdogThread.java ++++ b/src/main/java/org/spigotmc/WatchdogThread.java +@@ -8,7 +8,7 @@ import java.util.logging.Logger; + import net.minecraft.server.MinecraftServer; + import org.bukkit.Bukkit; + +-public class WatchdogThread extends Thread ++public final class WatchdogThread extends io.papermc.paper.util.TickThread // Paper - rewrite chunk system + { + + private static WatchdogThread instance; +@@ -115,6 +115,7 @@ public class WatchdogThread extends Thread + // Paper end - Different message for short timeout + log.log( Level.SEVERE, "------------------------------" ); + log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper ++ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(isLongTimeout); // Paper - rewrite chunk system + WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log ); + log.log( Level.SEVERE, "------------------------------" ); + // diff --git a/patches/server/0985-Starlight.patch b/patches/server/0985-Starlight.patch deleted file mode 100644 index 72b56b3d53..0000000000 --- a/patches/server/0985-Starlight.patch +++ /dev/null @@ -1,5429 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Wed, 28 Oct 2020 16:51:55 -0700 -Subject: [PATCH] Starlight - -See https://github.com/PaperMC/Starlight - -== AT == -public net.minecraft.server.level.ChunkHolder broadcast(Lnet/minecraft/network/protocol/Packet;Z)V - -diff --git a/src/main/java/ca/spottedleaf/starlight/common/light/BlockStarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/common/light/BlockStarLightEngine.java -new file mode 100644 -index 0000000000000000000000000000000000000000..3732a940d9603cf502983afbc4663113d1400be8 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/starlight/common/light/BlockStarLightEngine.java -@@ -0,0 +1,275 @@ -+package ca.spottedleaf.starlight.common.light; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.level.chunk.ChunkAccess; -+import net.minecraft.world.level.chunk.status.ChunkStatus; -+import net.minecraft.world.level.chunk.LevelChunkSection; -+import net.minecraft.world.level.chunk.LightChunkGetter; -+import net.minecraft.world.level.chunk.PalettedContainer; -+import net.minecraft.world.phys.shapes.Shapes; -+import net.minecraft.world.phys.shapes.VoxelShape; -+import java.util.ArrayList; -+import java.util.List; -+import java.util.Set; -+ -+public final class BlockStarLightEngine extends StarLightEngine { -+ -+ public BlockStarLightEngine(final Level world) { -+ super(false, world); -+ } -+ -+ @Override -+ protected boolean[] getEmptinessMap(final ChunkAccess chunk) { -+ return chunk.getBlockEmptinessMap(); -+ } -+ -+ @Override -+ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) { -+ chunk.setBlockEmptinessMap(to); -+ } -+ -+ @Override -+ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) { -+ return chunk.getBlockNibbles(); -+ } -+ -+ @Override -+ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) { -+ chunk.setBlockNibbles(to); -+ } -+ -+ @Override -+ protected boolean canUseChunk(final ChunkAccess chunk) { -+ return chunk.getStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect()); -+ } -+ -+ @Override -+ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) { -+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); -+ if (nibble != null) { -+ // de-initialisation is not as straightforward as with sky data, since deinit of block light is typically -+ // because a block was removed - which can decrease light. with sky data, block breaking can only result -+ // in increases, and thus the existing sky block check will actually correctly propagate light through -+ // a null section. so in order to propagate decreases correctly, we can do a couple of things: not remove -+ // the data section, or do edge checks on ALL axis (x, y, z). however I do not want edge checks running -+ // for clients at all, as they are expensive. so we don't remove the section, but to maintain the appearence -+ // of vanilla data management we "hide" them. -+ nibble.setHidden(); -+ } -+ } -+ -+ @Override -+ protected void initNibble(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; -+ } -+ -+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); -+ if (nibble == null) { -+ if (!initRemovedNibbles) { -+ throw new IllegalStateException(); -+ } else { -+ this.setNibbleInCache(chunkX, chunkY, chunkZ, new SWMRNibbleArray()); -+ } -+ } else { -+ nibble.setNonNull(); -+ } -+ } -+ -+ @Override -+ protected final void checkBlock(final LightChunkGetter 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 int currentLevel = this.getLightLevel(worldX, worldY, worldZ); -+ final BlockState blockState = this.getBlockState(worldX, worldY, worldZ); -+ final int emittedLevel = blockState.getLightEmission() & 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 BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos(); -+ protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos(); -+ -+ @Override -+ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, -+ final int expect) { -+ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ); -+ int level = centerState.getLightEmission() & 0xF; -+ -+ if (level >= (15 - 1) || level > expect) { -+ return level; -+ } -+ -+ final int sectionOffset = this.chunkSectionIndexOffset; -+ final BlockState conditionallyOpaqueState; -+ int opacity = centerState.getOpacityIfCached(); -+ -+ if (opacity == -1) { -+ this.recalcCenterPos.set(worldX, worldY, worldZ); -+ opacity = centerState.getLightBlock(lightAccess.getLevel(), 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 BlockState 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.set(offX, offY, offZ); -+ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms); -+ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms); -+ if (Shapes.faceShapeOccludes(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 LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions) { -+ for (final BlockPos pos : positions) { -+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); -+ } -+ -+ this.performLightDecrease(lightAccess); -+ } -+ -+ protected List getSources(final LightChunkGetter lightAccess, final ChunkAccess chunk) { -+ final List sources = new ArrayList<>(); -+ -+ final int offX = chunk.getPos().x << 4; -+ final int offZ = chunk.getPos().z << 4; -+ -+ final LevelChunkSection[] sections = chunk.getSections(); -+ for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) { -+ final LevelChunkSection section = sections[sectionY - this.minSection]; -+ if (section == null || section.hasOnlyAir()) { -+ // no sources in empty sections -+ continue; -+ } -+ if (!section.maybeHas((final BlockState state) -> { -+ return state.getLightEmission() > 0; -+ })) { -+ // no light sources in palette -+ continue; -+ } -+ final PalettedContainer states = section.states; -+ final int offY = sectionY << 4; -+ -+ for (int index = 0; index < (16 * 16 * 16); ++index) { -+ final BlockState state = states.get(index); -+ if (state.getLightEmission() <= 0) { -+ continue; -+ } -+ -+ // index = x | (z << 4) | (y << 8) -+ sources.add(new BlockPos(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15))); -+ } -+ } -+ -+ return sources; -+ } -+ -+ @Override -+ public void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) { -+ // setup sources -+ final int emittedMask = this.emittedLightMask; -+ final List positions = this.getSources(lightAccess, chunk); -+ for (int i = 0, len = positions.size(); i < len; ++i) { -+ final BlockPos pos = positions.get(i); -+ final BlockState blockState = this.getBlockState(pos.getX(), pos.getY(), pos.getZ()); -+ final int emittedLight = blockState.getLightEmission() & 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/ca/spottedleaf/starlight/common/light/SWMRNibbleArray.java b/src/main/java/ca/spottedleaf/starlight/common/light/SWMRNibbleArray.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4ffb4ffe01c4628d52742c5c0bbd35220eea6294 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/starlight/common/light/SWMRNibbleArray.java -@@ -0,0 +1,440 @@ -+package ca.spottedleaf.starlight.common.light; -+ -+import net.minecraft.world.level.chunk.DataLayer; -+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 -+ protected static final int INIT_STATE_HIDDEN = 3; // initialised, but conversion to Vanilla data should be treated as if NULL -+ -+ public static final int ARRAY_SIZE = 16 * 16 * 16 / (8/4); // blocks / bytes per block -+ // 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); -+ } -+ -+ public static SWMRNibbleArray fromVanilla(final DataLayer nibble) { -+ if (nibble == null) { -+ return new SWMRNibbleArray(null, true); -+ } else if (nibble.isEmpty()) { -+ return new SWMRNibbleArray(); -+ } else { -+ return new SWMRNibbleArray(nibble.getData().clone()); // make sure we don't write to the parameter later -+ } -+ } -+ -+ protected int stateUpdating; -+ protected volatile int stateVisible; -+ -+ protected byte[] storageUpdating; -+ protected boolean updatingDirty; // only returns whether storageUpdating is dirty -+ protected volatile 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("Data of wrong length: " + bytes.length); -+ } -+ this.stateVisible = this.stateUpdating = bytes == null ? (isNullNibble ? INIT_STATE_NULL : INIT_STATE_UNINIT) : INIT_STATE_INIT; -+ this.storageUpdating = this.storageVisible = bytes; -+ } -+ -+ public SWMRNibbleArray(final byte[] bytes, final int state) { -+ if (bytes != null && bytes.length != ARRAY_SIZE) { -+ throw new IllegalArgumentException("Data of wrong length: " + bytes.length); -+ } -+ if (bytes == null && (state == INIT_STATE_INIT || state == INIT_STATE_HIDDEN)) { -+ throw new IllegalArgumentException("Data cannot be null and have state be initialised"); -+ } -+ this.stateUpdating = this.stateVisible = state; -+ this.storageUpdating = this.storageVisible = bytes; -+ } -+ -+ @Override -+ public String toString() { -+ StringBuilder stringBuilder = new StringBuilder(); -+ stringBuilder.append("State: "); -+ switch (this.stateVisible) { -+ case INIT_STATE_NULL: -+ stringBuilder.append("null"); -+ break; -+ case INIT_STATE_UNINIT: -+ stringBuilder.append("uninitialised"); -+ break; -+ case INIT_STATE_INIT: -+ stringBuilder.append("initialised"); -+ break; -+ case INIT_STATE_HIDDEN: -+ stringBuilder.append("hidden"); -+ break; -+ default: -+ stringBuilder.append("unknown"); -+ break; -+ } -+ stringBuilder.append("\nData:\n"); -+ -+ final byte[] data = this.storageVisible; -+ if (data != null) { -+ for (int i = 0; i < 4096; ++i) { -+ // Copied from NibbleArray#toString -+ final int level = ((data[i >>> 1] >>> ((i & 1) << 2)) & 0xF); -+ -+ stringBuilder.append(Integer.toHexString(level)); -+ if ((i & 15) == 15) { -+ stringBuilder.append("\n"); -+ } -+ -+ if ((i & 255) == 255) { -+ stringBuilder.append("\n"); -+ } -+ } -+ } else { -+ stringBuilder.append("null"); -+ } -+ -+ return stringBuilder.toString(); -+ } -+ -+ public SaveState getSaveState() { -+ synchronized (this) { -+ final int state = this.stateVisible; -+ final byte[] data = this.storageVisible; -+ if (state == INIT_STATE_NULL) { -+ return null; -+ } -+ if (state == INIT_STATE_UNINIT) { -+ return new SaveState(null, state); -+ } -+ final boolean zero = isAllZero(data); -+ if (zero) { -+ return state == INIT_STATE_INIT ? new SaveState(null, INIT_STATE_UNINIT) : null; -+ } else { -+ return new SaveState(data.clone(), state); -+ } -+ } -+ } -+ -+ protected static boolean isAllZero(final byte[] data) { -+ for (int i = 0; i < (ARRAY_SIZE >>> 4); ++i) { -+ byte whole = data[i << 4]; -+ -+ for (int k = 1; k < (1 << 4); ++k) { -+ whole |= data[(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.updatingDirty) { -+ if (this.storageUpdating != null) { -+ into = this.storageUpdating = allocateBytes(); -+ } else { -+ this.storageUpdating = into = allocateBytes(); -+ this.stateUpdating = INIT_STATE_INIT; -+ } -+ this.updatingDirty = true; -+ } else { -+ into = this.storageUpdating; -+ } -+ -+ 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() { -+ if (this.stateUpdating != INIT_STATE_HIDDEN) { -+ 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() { -+ if (this.stateUpdating != INIT_STATE_HIDDEN) { -+ 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_HIDDEN) { -+ this.stateUpdating = INIT_STATE_INIT; -+ return; -+ } -+ 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 void setHidden() { -+ if (this.stateUpdating == INIT_STATE_HIDDEN) { -+ return; -+ } -+ if (this.stateUpdating != INIT_STATE_INIT) { -+ this.setNull(); -+ } else { -+ this.stateUpdating = INIT_STATE_HIDDEN; -+ } -+ } -+ -+ // 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 -+ public boolean isHiddenUpdating() { -+ return this.stateUpdating == INIT_STATE_HIDDEN; -+ } -+ -+ // operation type: updating -+ public boolean isHiddenVisible() { -+ return this.stateVisible == INIT_STATE_HIDDEN; -+ } -+ -+ // 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); -+ } -+ -+ if (this.stateUpdating != INIT_STATE_HIDDEN) { -+ 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 { -+ if (this.storageUpdating != this.storageVisible) { -+ System.arraycopy(this.storageUpdating, 0, this.storageVisible, 0, ARRAY_SIZE); -+ } -+ } -+ -+ if (this.storageUpdating != this.storageVisible) { -+ freeBytes(this.storageUpdating); -+ } -+ this.storageUpdating = this.storageVisible; -+ } -+ this.updatingDirty = false; -+ this.stateVisible = this.stateUpdating; -+ } -+ -+ return true; -+ } -+ -+ // operation type: visible -+ public DataLayer toVanillaNibble() { -+ synchronized (this) { -+ switch (this.stateVisible) { -+ case INIT_STATE_HIDDEN: -+ case INIT_STATE_NULL: -+ return null; -+ case INIT_STATE_UNINIT: -+ return new DataLayer(); -+ case INIT_STATE_INIT: -+ return new DataLayer(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) { -+ // 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)); -+ } -+ -+ public static final class SaveState { -+ -+ public final byte[] data; -+ public final int state; -+ -+ public SaveState(final byte[] data, final int state) { -+ this.data = data; -+ this.state = state; -+ } -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/starlight/common/light/SkyStarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/common/light/SkyStarLightEngine.java -new file mode 100644 -index 0000000000000000000000000000000000000000..43a2cce467d29f81ba57d77c03608e57857dd579 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/starlight/common/light/SkyStarLightEngine.java -@@ -0,0 +1,709 @@ -+package ca.spottedleaf.starlight.common.light; -+ -+import ca.spottedleaf.starlight.common.util.WorldUtil; -+import it.unimi.dsi.fastutil.shorts.ShortCollection; -+import it.unimi.dsi.fastutil.shorts.ShortIterator; -+import net.minecraft.core.BlockPos; -+import net.minecraft.world.level.BlockGetter; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.level.chunk.ChunkAccess; -+import net.minecraft.world.level.chunk.status.ChunkStatus; -+import net.minecraft.world.level.chunk.LevelChunkSection; -+import net.minecraft.world.level.chunk.LightChunkGetter; -+import net.minecraft.world.phys.shapes.Shapes; -+import net.minecraft.world.phys.shapes.VoxelShape; -+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 Level world) { -+ super(true, world); -+ this.nullPropagationCheckCache = new boolean[WorldUtil.getTotalLightSections(world)]; -+ } -+ -+ @Override -+ protected void initNibble(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.initNibble(nibble, chunkX, chunkY, chunkZ, extrude); -+ } -+ -+ @Override -+ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) { -+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); -+ if (nibble != null) { -+ nibble.setNull(); -+ } -+ } -+ -+ protected final void initNibble(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 LevelChunkSection current = this.getChunkSection(chunkX, currY, chunkZ); -+ if (current == null || current.hasOnlyAir()) { -+ 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 -+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); -+ nibble.setNonNull(); -+ nibble.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.setNonNull(); -+ currNibble.extrudeLower(nibble); -+ break; -+ } -+ } -+ } else { -+ currNibble.setNonNull(); -+ } -+ } -+ -+ protected final void rewriteNibbleCacheForSkylight(final ChunkAccess 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.initNibble(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 ChunkAccess chunk) { -+ return chunk.getSkyEmptinessMap(); -+ } -+ -+ @Override -+ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) { -+ chunk.setSkyEmptinessMap(to); -+ } -+ -+ @Override -+ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) { -+ return chunk.getSkyNibbles(); -+ } -+ -+ @Override -+ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) { -+ chunk.setSkyNibbles(to); -+ } -+ -+ @Override -+ protected boolean canUseChunk(final ChunkAccess chunk) { -+ // can only use chunks for sky stuff if their sections have been init'd -+ return chunk.getStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect()); -+ } -+ -+ @Override -+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess 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 LightChunkGetter lightAccess, final ChunkAccess 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); -+ } -+ -+ @Override -+ protected void checkBlock(final LightChunkGetter 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)) -+ ); -+ } -+ -+ protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos(); -+ protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos(); -+ -+ @Override -+ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, -+ final int expect) { -+ if (expect == 15) { -+ return expect; -+ } -+ -+ final int sectionOffset = this.chunkSectionIndexOffset; -+ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ); -+ int opacity = centerState.getOpacityIfCached(); -+ -+ final BlockState conditionallyOpaqueState; -+ if (opacity < 0) { -+ this.recalcCenterPos.set(worldX, worldY, worldZ); -+ opacity = Math.max(1, centerState.getLightBlock(lightAccess.getLevel(), 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 BlockState 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.set(offX, offY, offZ); -+ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms); -+ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms); -+ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) { -+ // not allowed to propagate -+ continue; -+ } -+ } -+ -+ final int calculated = neighbourLevel - opacity; -+ level = Math.max(calculated, level); -+ if (level > expect) { -+ return level; -+ } -+ } -+ -+ return level; -+ } -+ -+ @Override -+ protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions) { -+ this.rewriteNibbleCacheForSkylight(atChunk); -+ Arrays.fill(this.nullPropagationCheckCache, false); -+ -+ final BlockGetter world = lightAccess.getLevel(); -+ 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 BlockPos 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 BlockPos pos : positions) { -+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); -+ } -+ -+ this.performLightDecrease(lightAccess); -+ } -+ -+ protected final int[] heightMapGen = new int[32 * 32]; -+ -+ @Override -+ protected void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) { -+ this.rewriteNibbleCacheForSkylight(chunk); -+ Arrays.fill(this.nullPropagationCheckCache, false); -+ -+ final BlockGetter world = lightAccess.getLevel(); -+ final ChunkPos chunkPos = chunk.getPos(); -+ final int chunkX = chunkPos.x; -+ final int chunkZ = chunkPos.z; -+ -+ final LevelChunkSection[] sections = chunk.getSections(); -+ -+ int highestNonEmptySection = this.maxSection; -+ while (highestNonEmptySection == (this.minSection - 1) || -+ sections[highestNonEmptySection - this.minSection] == null || sections[highestNonEmptySection - this.minSection].hasOnlyAir()) { -+ 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 BlockGetter world, final int worldX, int startY, final int worldZ, -+ final boolean extrudeInitialised, final boolean delayLightSet) { -+ final BlockPos.MutableBlockPos 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); -+ -+ BlockState above = this.getBlockState(worldX, startY + 1, worldZ); -+ -+ 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); -+ } -+ final BlockState current = this.getBlockState(worldX, startY, worldZ); -+ -+ final VoxelShape fromShape; -+ if (above.isConditionallyFullOpaque()) { -+ this.mutablePos2.set(worldX, startY + 1, worldZ); -+ fromShape = above.getFaceOcclusionShape(world, this.mutablePos2, AxisDirection.NEGATIVE_Y.nms); -+ if (Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { -+ // above wont let us propagate -+ break; -+ } -+ } else { -+ fromShape = Shapes.empty(); -+ } -+ -+ 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.set(worldX, startY, worldZ); -+ long flags = 0L; -+ if (current.isConditionallyFullOpaque()) { -+ final VoxelShape cullingFace = current.getFaceOcclusionShape(world, mutablePos, AxisDirection.POSITIVE_Y.nms); -+ -+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { -+ // can't propagate here, we're done on this column. -+ break; -+ } -+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; -+ } -+ -+ final int opacity = current.getLightBlock(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/ca/spottedleaf/starlight/common/light/StarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightEngine.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ad1eeebe6de219143492b94da309cb54ae9e0a5b ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightEngine.java -@@ -0,0 +1,1572 @@ -+package ca.spottedleaf.starlight.common.light; -+ -+import ca.spottedleaf.starlight.common.util.CoordinateUtils; -+import ca.spottedleaf.starlight.common.util.IntegerUtil; -+import ca.spottedleaf.starlight.common.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.core.BlockPos; -+import net.minecraft.core.Direction; -+import net.minecraft.core.SectionPos; -+import net.minecraft.world.level.BlockGetter; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.LevelHeightAccessor; -+import net.minecraft.world.level.LightLayer; -+import net.minecraft.world.level.block.Blocks; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.level.chunk.ChunkAccess; -+import net.minecraft.world.level.chunk.LevelChunkSection; -+import net.minecraft.world.level.chunk.LightChunkGetter; -+import net.minecraft.world.phys.shapes.Shapes; -+import net.minecraft.world.phys.shapes.VoxelShape; -+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 BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState(); -+ -+ 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 Direction 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 = Direction.fromDelta(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 LevelChunkSection[] 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. -+ // index = x + (z * 5) -+ protected final ChunkAccess[] chunkCache = new ChunkAccess[5 * 5]; -+ -+ // index = x + (z * 5) -+ protected final boolean[][] emptinessMapCache = new boolean[5 * 5][]; -+ -+ protected final BlockPos.MutableBlockPos mutablePos1 = new BlockPos.MutableBlockPos(); -+ protected final BlockPos.MutableBlockPos mutablePos2 = new BlockPos.MutableBlockPos(); -+ protected final BlockPos.MutableBlockPos mutablePos3 = new BlockPos.MutableBlockPos(); -+ -+ 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 final boolean isClientSide; -+ -+ protected final Level world; -+ protected final int minLightSection; -+ protected final int maxLightSection; -+ protected final int minSection; -+ protected final int maxSection; -+ -+ protected StarLightEngine(final boolean skylightPropagator, final Level world) { -+ this.skylightPropagator = skylightPropagator; -+ this.emittedLightMask = skylightPropagator ? 0 : 0xF; -+ this.isClientSide = world.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 LevelChunkSection[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 LightChunkGetter 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 ChunkAccess chunk = (ChunkAccess)chunkProvider.getChunkForLighting(cx, cz); -+ -+ 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 ChunkAccess 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 ChunkAccess chunk) { -+ this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = chunk; -+ } -+ -+ protected final LevelChunkSection 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 LevelChunkSection section) { -+ this.sectionCache[chunkX + 5*chunkZ + 5*5*chunkY + this.chunkSectionIndexOffset] = section; -+ } -+ -+ protected final void setBlocksForChunkInCache(final int chunkX, final int chunkZ, final LevelChunkSection[] 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)); -+ } -+ } -+ -+ 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 LightChunkGetter 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 ySections = (this.maxSection - this.minSection) + 1; -+ final int chunkY = ((index / (5*5)) % (ySections + 2 + 2)) - this.chunkOffsetY; -+ if ((nibble != null && nibble.updateVisible()) || this.notifyUpdateCache[index]) { -+ lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(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 BlockState getBlockState(final int worldX, final int worldY, final int worldZ) { -+ final LevelChunkSection section = this.sectionCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset]; -+ -+ if (section != null) { -+ return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.getBlockState(worldX & 15, worldY & 15, worldZ & 15); -+ } -+ -+ return AIR_BLOCK_STATE; -+ } -+ -+ protected final BlockState getBlockState(final int sectionIndex, final int localIndex) { -+ final LevelChunkSection section = this.sectionCache[sectionIndex]; -+ -+ if (section != null) { -+ return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.states.get(localIndex); -+ } -+ -+ return AIR_BLOCK_STATE; -+ } -+ -+ 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; -+ } -+ -+ public static SWMRNibbleArray[] getFilledEmptyLight(final LevelHeightAccessor 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 ChunkAccess chunk); -+ -+ protected abstract void setEmptinessMap(final ChunkAccess chunk, final boolean[] to); -+ -+ protected abstract SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk); -+ -+ protected abstract void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to); -+ -+ protected abstract boolean canUseChunk(final ChunkAccess chunk); -+ -+ public final void blocksChangedInChunk(final LightChunkGetter 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 ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); -+ if (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 LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions); -+ -+ protected abstract void checkBlock(final LightChunkGetter 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 LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, -+ final int expect); -+ -+ protected final int[] chunkCheckDelayedUpdatesCenter = new int[16 * 16]; -+ protected final int[] chunkCheckDelayedUpdatesNeighbour = new int[16 * 16]; -+ -+ protected void checkChunkEdge(final LightChunkGetter lightAccess, final ChunkAccess 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; -+ } -+ -+ 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) != currentLevel) { -+ this.chunkCheckDelayedUpdatesCenter[centerDelayedChecks++] = currentIndex; -+ } -+ -+ if (this.calculateLightValue(lightAccess, neighbourX, currY, neighbourZ, neighbourLevel) != 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 LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) { -+ final ChunkPos 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 LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) { -+ final ChunkPos 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 LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) { -+ final ChunkPos 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 ChunkAccess chunk) { -+ final LevelChunkSection[] sections = chunk.getSections(); -+ final Boolean[] ret = new Boolean[sections.length]; -+ -+ for (int i = 0; i < sections.length; ++i) { -+ if (sections[i] == null || sections[i].hasOnlyAir()) { -+ ret[i] = Boolean.TRUE; -+ } else { -+ ret[i] = Boolean.FALSE; -+ } -+ } -+ -+ return ret; -+ } -+ -+ public final void forceHandleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess 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 LightChunkGetter lightAccess, final int chunkX, final int chunkZ, -+ final Boolean[] emptinessChanges) { -+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); -+ try { -+ final ChunkAccess 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(); -+ } -+ } -+ -+ protected abstract void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles); -+ -+ protected abstract void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ); -+ -+ // 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 final boolean[] handleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, -+ final Boolean[] emptinessChanges, final boolean unlit) { -+ final Level world = (Level)lightAccess.getLevel(); -+ 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) { -+ Boolean valueBoxed = emptinessChanges[sectionIndex]; -+ if (valueBoxed == null) { -+ if (!needsInit) { -+ continue; -+ } -+ final LevelChunkSection section = this.getChunkSection(chunkX, sectionIndex + this.minSection, chunkZ); -+ emptinessChanges[sectionIndex] = valueBoxed = section == null || section.hasOnlyAir() ? Boolean.TRUE : Boolean.FALSE; -+ } -+ 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.initNibble(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) { -+ // 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 LevelChunkSection section = this.getChunkSection(dx + dx2 + chunkX, y, dz + dz2 + chunkZ); -+ if (section != null && !section.hasOnlyAir()) { -+ 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 -+ this.setNibbleNull(dx + chunkX, sectionY, dz + chunkZ); -+ } else if (!allEmpty) { -+ // must init -+ final boolean extrude = (dx | dz) != 0 || !unlit; -+ this.initNibble(dx + chunkX, sectionY, dz + chunkZ, extrude, false); -+ } -+ } -+ } -+ } -+ -+ return ret; -+ } -+ -+ public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ) { -+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false); -+ try { -+ final ChunkAccess 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 LightChunkGetter lightAccess, final int chunkX, final int chunkZ, final ShortCollection sections) { -+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false); -+ try { -+ final ChunkAccess 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 LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks); -+ -+ public final void light(final LightChunkGetter lightAccess, final ChunkAccess 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); -+ this.setNibbles(chunk, nibbles); -+ this.updateVisible(lightAccess); -+ } finally { -+ this.destroyCaches(); -+ } -+ } -+ -+ public final void relightChunks(final LightChunkGetter 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 ChunkPos chunkPos : chunks) { -+ final int chunkX = chunkPos.x; -+ final int chunkZ = chunkPos.z; -+ final ChunkAccess chunk = (ChunkAccess)lightAccess.getChunkForLighting(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 ChunkAccess neighbour = (ChunkAccess)lightAccess.getChunkForLighting(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 ChunkAccess neighbour2 = (ChunkAccess)lightAccess.getChunkForLighting(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 ChunkPos(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.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, y, chunkX)); -+ } -+ -+ // now do callback -+ if (chunkLightCallback != null) { -+ chunkLightCallback.accept(chunkPos); -+ } -+ ++lightCalls; -+ } -+ -+ if (onComplete != null) { -+ onComplete.accept(lightCalls); -+ } -+ } -+ -+ // 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 3 bits: state flags -+ // state flags: -+ // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading light -+ // updates for block sources -+ protected static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 2; -+ // 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 LightChunkGetter lightAccess) { -+ final BlockGetter world = lightAccess.getLevel(); -+ 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; -+ } -+ } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) { -+ // these are used to restore block sources after a propagation decrease -+ this.setLightLevel(posX, posY, posZ, propagatedLightLevel); -+ } -+ -+ 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 BlockState 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.set(offX, offY, offZ); -+ long flags = 0; -+ if (blockState.isConditionallyFullOpaque()) { -+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); -+ -+ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { -+ continue; -+ } -+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; -+ } -+ -+ final int opacity = blockState.getLightBlock(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 BlockState fromBlock = this.getBlockState(posX, posY, posZ); -+ this.mutablePos2.set(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.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty(); -+ -+ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), 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 BlockState 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.set(offX, offY, offZ); -+ long flags = 0; -+ if (blockState.isConditionallyFullOpaque()) { -+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); -+ -+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { -+ continue; -+ } -+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; -+ } -+ -+ final int opacity = blockState.getLightBlock(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 LightChunkGetter lightAccess) { -+ final BlockGetter world = lightAccess.getLevel(); -+ 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; -+ -+ 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 BlockState 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 = blockState.getLightEmission() & emittedMask; -+ if (emittedLight != 0) { -+ // re-propagate source -+ // note: do not set recheck level, or else the propagation will fail -+ 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_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL); -+ } -+ -+ currentNibble.set(localIndex, 0); -+ 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.set(offX, offY, offZ); -+ long flags = 0; -+ if (blockState.isConditionallyFullOpaque()) { -+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); -+ -+ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { -+ continue; -+ } -+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; -+ } -+ -+ final int opacity = blockState.getLightBlock(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 = blockState.getLightEmission() & emittedMask; -+ if (emittedLight != 0) { -+ // re-propagate source -+ // note: do not set recheck level, or else the propagation will fail -+ 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 | FLAG_WRITE_LEVEL); -+ } -+ -+ currentNibble.set(localIndex, 0); -+ 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 BlockState fromBlock = this.getBlockState(posX, posY, posZ); -+ this.mutablePos2.set(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.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty(); -+ -+ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), 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 BlockState 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 = blockState.getLightEmission() & emittedMask; -+ if (emittedLight != 0) { -+ // re-propagate source -+ // note: do not set recheck level, or else the propagation will fail -+ 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_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL); -+ } -+ -+ currentNibble.set(localIndex, 0); -+ 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.set(offX, offY, offZ); -+ long flags = 0; -+ if (blockState.isConditionallyFullOpaque()) { -+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); -+ -+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { -+ continue; -+ } -+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; -+ } -+ -+ final int opacity = blockState.getLightBlock(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 = blockState.getLightEmission() & emittedMask; -+ if (emittedLight != 0) { -+ // re-propagate source -+ // note: do not set recheck level, or else the propagation will fail -+ 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 | FLAG_WRITE_LEVEL); -+ } -+ -+ currentNibble.set(localIndex, 0); -+ 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/ca/spottedleaf/starlight/common/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e0338db4d6fa359029ed5edeacc3646aa98701f5 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java -@@ -0,0 +1,674 @@ -+package ca.spottedleaf.starlight.common.light; -+ -+import ca.spottedleaf.starlight.common.util.CoordinateUtils; -+import ca.spottedleaf.starlight.common.util.WorldUtil; -+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; -+import it.unimi.dsi.fastutil.shorts.ShortCollection; -+import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; -+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.SectionPos; -+import net.minecraft.server.level.ServerChunkCache; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.TicketType; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.chunk.ChunkAccess; -+import net.minecraft.world.level.chunk.status.ChunkStatus; -+import net.minecraft.world.level.chunk.DataLayer; -+import net.minecraft.world.level.chunk.LevelChunk; -+import net.minecraft.world.level.chunk.LightChunkGetter; -+import net.minecraft.world.level.lighting.LayerLightEventListener; -+import net.minecraft.world.level.lighting.LevelLightEngine; -+import java.util.ArrayDeque; -+import java.util.ArrayList; -+import java.util.List; -+import java.util.Set; -+import java.util.concurrent.CompletableFuture; -+import java.util.function.Consumer; -+import java.util.function.IntConsumer; -+ -+public final class StarLightInterface { -+ -+ public static final TicketType CHUNK_WORK_TICKET = TicketType.create("starlight_chunk_work_ticket", (p1, p2) -> Long.compare(p1.toLong(), p2.toLong())); -+ -+ /** -+ * Can be {@code null}, indicating the light is all empty. -+ */ -+ protected final Level world; -+ protected final LightChunkGetter lightAccess; -+ -+ protected final ArrayDeque cachedSkyPropagators; -+ protected final ArrayDeque cachedBlockPropagators; -+ -+ protected final LightQueue lightQueue = new LightQueue(this); -+ -+ protected final LayerLightEventListener skyReader; -+ protected final LayerLightEventListener blockReader; -+ protected final boolean isClientSide; -+ -+ protected final int minSection; -+ protected final int maxSection; -+ protected final int minLightSection; -+ protected final int maxLightSection; -+ -+ public final LevelLightEngine lightEngine; -+ -+ private final boolean hasBlockLight; -+ private final boolean hasSkyLight; -+ -+ public StarLightInterface(final LightChunkGetter lightAccess, final boolean hasSkyLight, final boolean hasBlockLight, final LevelLightEngine lightEngine) { -+ this.lightAccess = lightAccess; -+ this.world = lightAccess == null ? null : (Level)lightAccess.getLevel(); -+ this.cachedSkyPropagators = hasSkyLight && lightAccess != null ? new ArrayDeque<>() : null; -+ this.cachedBlockPropagators = hasBlockLight && lightAccess != null ? new ArrayDeque<>() : null; -+ this.isClientSide = !(this.world instanceof ServerLevel); -+ if (this.world == null) { -+ this.minSection = -4; -+ this.maxSection = 19; -+ this.minLightSection = -5; -+ this.maxLightSection = 20; -+ } 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.lightEngine = lightEngine; -+ this.hasBlockLight = hasBlockLight; -+ this.hasSkyLight = hasSkyLight; -+ this.skyReader = !hasSkyLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() { -+ @Override -+ public void checkBlock(final BlockPos blockPos) { -+ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable()); -+ } -+ -+ @Override -+ public void propagateLightSources(final ChunkPos chunkPos) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public boolean hasLightWork() { -+ // not really correct... -+ return StarLightInterface.this.hasUpdates(); -+ } -+ -+ @Override -+ public int runLightUpdates() { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public DataLayer getDataLayerData(final SectionPos pos) { -+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ()); -+ if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect()) || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { -+ return null; -+ } -+ -+ final int sectionY = pos.getY(); -+ -+ if (sectionY > StarLightInterface.this.maxLightSection || sectionY < StarLightInterface.this.minLightSection) { -+ return null; -+ } -+ -+ if (chunk.getSkyEmptinessMap() == null) { -+ return null; -+ } -+ -+ return chunk.getSkyNibbles()[sectionY - StarLightInterface.this.minLightSection].toVanillaNibble(); -+ } -+ -+ @Override -+ public int getLightValue(final BlockPos blockPos) { -+ return StarLightInterface.this.getSkyLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4)); -+ } -+ -+ @Override -+ public void updateSectionStatus(final SectionPos pos, final boolean notReady) { -+ StarLightInterface.this.sectionChange(pos, notReady); -+ } -+ }; -+ this.blockReader = !hasBlockLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() { -+ @Override -+ public void checkBlock(final BlockPos blockPos) { -+ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable()); -+ } -+ -+ @Override -+ public void propagateLightSources(final ChunkPos chunkPos) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public boolean hasLightWork() { -+ // not really correct... -+ return StarLightInterface.this.hasUpdates(); -+ } -+ -+ @Override -+ public int runLightUpdates() { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public DataLayer getDataLayerData(final SectionPos pos) { -+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ()); -+ -+ if (chunk == null || pos.getY() < StarLightInterface.this.minLightSection || pos.getY() > StarLightInterface.this.maxLightSection) { -+ return null; -+ } -+ -+ return chunk.getBlockNibbles()[pos.getY() - StarLightInterface.this.minLightSection].toVanillaNibble(); -+ } -+ -+ @Override -+ public int getLightValue(final BlockPos blockPos) { -+ return StarLightInterface.this.getBlockLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4)); -+ } -+ -+ @Override -+ public void updateSectionStatus(final SectionPos pos, final boolean notReady) { -+ StarLightInterface.this.sectionChange(pos, notReady); -+ } -+ }; -+ } -+ -+ public boolean hasSkyLight() { -+ return this.hasSkyLight; -+ } -+ -+ public boolean hasBlockLight() { -+ return this.hasBlockLight; -+ } -+ -+ public int getSkyLightValue(final BlockPos blockPos, final ChunkAccess chunk) { -+ if (!this.hasSkyLight) { -+ return 0; -+ } -+ final int x = blockPos.getX(); -+ int y = blockPos.getY(); -+ final int z = blockPos.getZ(); -+ -+ final int minSection = this.minSection; -+ final int maxSection = this.maxSection; -+ final int minLightSection = this.minLightSection; -+ final int maxLightSection = this.maxLightSection; -+ -+ if (chunk == null || (!this.isClientSide && !chunk.isLightCorrect()) || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { -+ return 15; -+ } -+ -+ int sectionY = y >> 4; -+ -+ if (sectionY > maxLightSection) { -+ return 15; -+ } -+ -+ if (sectionY < minLightSection) { -+ sectionY = minLightSection; -+ y = sectionY << 4; -+ } -+ -+ final SWMRNibbleArray[] nibbles = chunk.getSkyNibbles(); -+ final SWMRNibbleArray immediate = nibbles[sectionY - minLightSection]; -+ -+ if (!immediate.isNullNibbleVisible()) { -+ return immediate.getVisible(x, y, z); -+ } -+ -+ final boolean[] emptinessMap = chunk.getSkyEmptinessMap(); -+ -+ if (emptinessMap == null) { -+ return 15; -+ } -+ -+ // are we above this chunk's lowest empty section? -+ int lowestY = minLightSection - 1; -+ for (int currY = maxSection; currY >= minSection; --currY) { -+ if (emptinessMap[currY - 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 <= maxLightSection; ++currY) { -+ final SWMRNibbleArray nibble = nibbles[currY - minLightSection]; -+ if (!nibble.isNullNibbleVisible()) { -+ return nibble.getVisible(x, 0, z); -+ } -+ } -+ -+ // should never reach here -+ return 15; -+ } -+ -+ public int getBlockLightValue(final BlockPos blockPos, final ChunkAccess chunk) { -+ if (!this.hasBlockLight) { -+ return 0; -+ } -+ final int y = blockPos.getY(); -+ final int cy = y >> 4; -+ -+ final int minLightSection = this.minLightSection; -+ final int maxLightSection = this.maxLightSection; -+ -+ if (cy < minLightSection || cy > maxLightSection) { -+ return 0; -+ } -+ -+ if (chunk == null) { -+ return 0; -+ } -+ -+ final SWMRNibbleArray nibble = chunk.getBlockNibbles()[cy - minLightSection]; -+ return nibble.getVisible(blockPos.getX(), y, blockPos.getZ()); -+ } -+ -+ public int getRawBrightness(final BlockPos pos, final int ambientDarkness) { -+ final ChunkAccess chunk = this.getAnyChunkNow(pos.getX() >> 4, pos.getZ() >> 4); -+ -+ final int sky = this.getSkyLightValue(pos, chunk) - ambientDarkness; -+ // Don't fetch the block light level if the skylight level is 15, since the value will never be higher. -+ if (sky == 15) return 15; -+ final int block = this.getBlockLightValue(pos, chunk); -+ return Math.max(sky, block); -+ } -+ -+ public LayerLightEventListener getSkyReader() { -+ return this.skyReader; -+ } -+ -+ public LayerLightEventListener getBlockReader() { -+ return this.blockReader; -+ } -+ -+ public boolean isClientSide() { -+ return this.isClientSide; -+ } -+ -+ public ChunkAccess getAnyChunkNow(final int chunkX, final int chunkZ) { -+ if (this.world == null) { -+ // empty world -+ return null; -+ } -+ -+ final ServerChunkCache chunkProvider = ((ServerLevel)this.world).getChunkSource(); -+ final LevelChunk fullLoaded = chunkProvider.getChunkAtIfLoadedImmediately(chunkX, chunkZ); -+ if (fullLoaded != null) { -+ return fullLoaded; -+ } -+ -+ return chunkProvider.getChunkAtImmediately(chunkX, chunkZ); -+ } -+ -+ public boolean hasUpdates() { -+ return !this.lightQueue.isEmpty(); -+ } -+ -+ public Level getWorld() { -+ return this.world; -+ } -+ -+ public LightChunkGetter 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 LightQueue.ChunkTasks blockChange(final BlockPos pos) { -+ if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world -+ return null; -+ } -+ -+ return this.lightQueue.queueBlockChange(pos); -+ } -+ -+ public LightQueue.ChunkTasks sectionChange(final SectionPos pos, final boolean newEmptyValue) { -+ if (this.world == null) { // empty world -+ return null; -+ } -+ -+ return this.lightQueue.queueSectionChange(pos, newEmptyValue); -+ } -+ -+ public void forceLoadInChunk(final ChunkAccess 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 ChunkAccess 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 scheduleChunkLight(final ChunkPos pos, final Runnable run) { -+ this.lightQueue.queueChunkLighting(pos, run); -+ } -+ -+ public void removeChunkTasks(final ChunkPos pos) { -+ this.lightQueue.removeChunk(pos); -+ } -+ -+ public void propagateChanges() { -+ if (this.lightQueue.isEmpty()) { -+ return; -+ } -+ -+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); -+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); -+ -+ try { -+ LightQueue.ChunkTasks task; -+ while ((task = this.lightQueue.removeFirstTask()) != null) { -+ if (task.lightTasks != null) { -+ for (final Runnable run : task.lightTasks) { -+ run.run(); -+ } -+ } -+ -+ final long coordinate = task.chunkCoordinate; -+ final int chunkX = CoordinateUtils.getChunkX(coordinate); -+ final int chunkZ = CoordinateUtils.getChunkZ(coordinate); -+ -+ final Set positions = task.changedPositions; -+ final Boolean[] sectionChanges = task.changedSectionSet; -+ -+ if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) { -+ skyEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); -+ } -+ if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) { -+ blockEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); -+ } -+ -+ if (skyEngine != null && task.queuedEdgeChecksSky != null) { -+ skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksSky); -+ } -+ if (blockEngine != null && task.queuedEdgeChecksBlock != null) { -+ blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksBlock); -+ } -+ -+ task.onComplete.complete(null); -+ } -+ } finally { -+ this.releaseSkyLightEngine(skyEngine); -+ this.releaseBlockLightEngine(blockEngine); -+ } -+ } -+ -+ public static final class LightQueue { -+ -+ protected final Long2ObjectLinkedOpenHashMap chunkTasks = new Long2ObjectLinkedOpenHashMap<>(); -+ protected final StarLightInterface manager; -+ -+ public LightQueue(final StarLightInterface manager) { -+ this.manager = manager; -+ } -+ -+ public synchronized boolean isEmpty() { -+ return this.chunkTasks.isEmpty(); -+ } -+ -+ public synchronized LightQueue.ChunkTasks queueBlockChange(final BlockPos pos) { -+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); -+ tasks.changedPositions.add(pos.immutable()); -+ return tasks; -+ } -+ -+ public synchronized LightQueue.ChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) { -+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); -+ -+ if (tasks.changedSectionSet == null) { -+ tasks.changedSectionSet = new Boolean[this.manager.maxSection - this.manager.minSection + 1]; -+ } -+ tasks.changedSectionSet[pos.getY() - this.manager.minSection] = Boolean.valueOf(newEmptyValue); -+ -+ return tasks; -+ } -+ -+ public synchronized LightQueue.ChunkTasks queueChunkLighting(final ChunkPos pos, final Runnable lightTask) { -+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); -+ if (tasks.lightTasks == null) { -+ tasks.lightTasks = new ArrayList<>(); -+ } -+ tasks.lightTasks.add(lightTask); -+ -+ return tasks; -+ } -+ -+ public synchronized LightQueue.ChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) { -+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); -+ -+ ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksSky; -+ if (queuedEdges == null) { -+ queuedEdges = tasks.queuedEdgeChecksSky = new ShortOpenHashSet(); -+ } -+ queuedEdges.addAll(sections); -+ -+ return tasks; -+ } -+ -+ public synchronized LightQueue.ChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) { -+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); -+ -+ ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksBlock; -+ if (queuedEdges == null) { -+ queuedEdges = tasks.queuedEdgeChecksBlock = new ShortOpenHashSet(); -+ } -+ queuedEdges.addAll(sections); -+ -+ return tasks; -+ } -+ -+ public void removeChunk(final ChunkPos pos) { -+ final ChunkTasks tasks; -+ synchronized (this) { -+ tasks = this.chunkTasks.remove(CoordinateUtils.getChunkKey(pos)); -+ } -+ if (tasks != null) { -+ tasks.onComplete.complete(null); -+ } -+ } -+ -+ public synchronized ChunkTasks removeFirstTask() { -+ if (this.chunkTasks.isEmpty()) { -+ return null; -+ } -+ return this.chunkTasks.removeFirst(); -+ } -+ -+ public static final class ChunkTasks { -+ -+ public final Set changedPositions = new ObjectOpenHashSet<>(); -+ public Boolean[] changedSectionSet; -+ public ShortOpenHashSet queuedEdgeChecksSky; -+ public ShortOpenHashSet queuedEdgeChecksBlock; -+ public List lightTasks; -+ -+ public boolean isTicketAdded = false; -+ public final CompletableFuture onComplete = new CompletableFuture<>(); -+ -+ public final long chunkCoordinate; -+ -+ public ChunkTasks(final long chunkCoordinate) { -+ this.chunkCoordinate = chunkCoordinate; -+ } -+ } -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/starlight/common/util/CoordinateUtils.java b/src/main/java/ca/spottedleaf/starlight/common/util/CoordinateUtils.java -new file mode 100644 -index 0000000000000000000000000000000000000000..16a4a14e7ccf9e4d7fdf1166674fe8f529c06d39 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/starlight/common/util/CoordinateUtils.java -@@ -0,0 +1,128 @@ -+package ca.spottedleaf.starlight.common.util; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.SectionPos; -+import net.minecraft.util.Mth; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.level.ChunkPos; -+ -+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 BlockPos pos) { -+ return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL); -+ } -+ -+ public static long getChunkKey(final Entity entity) { -+ return ((long)(Mth.floor(entity.getZ()) >> 4) << 32) | ((Mth.floor(entity.getX()) >> 4) & 0xFFFFFFFFL); -+ } -+ -+ public static long getChunkKey(final ChunkPos pos) { -+ return ((long)pos.z << 32) | (pos.x & 0xFFFFFFFFL); -+ } -+ -+ public static long getChunkKey(final SectionPos 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 Mth.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 SectionPos 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 ChunkPos 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 BlockPos 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 ((Mth.lfloor(entity.getX()) << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) | -+ ((Mth.lfloor(entity.getY()) >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) | -+ ((Mth.lfloor(entity.getZ()) << (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 Mth.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 BlockPos 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.getX() & 0x7FFFFFF) | (((long)entity.getZ() & 0x7FFFFFF) << 27) | ((long)entity.getY() << 54); -+ } -+ -+ private CoordinateUtils() { -+ throw new RuntimeException(); -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/starlight/common/util/IntegerUtil.java b/src/main/java/ca/spottedleaf/starlight/common/util/IntegerUtil.java -new file mode 100644 -index 0000000000000000000000000000000000000000..fabf1e97c019c7365212f40018dcd08d3b828113 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/starlight/common/util/IntegerUtil.java -@@ -0,0 +1,242 @@ -+package ca.spottedleaf.starlight.common.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 = 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 -+ -+ /* -+ Signed usage: -+ int number; -+ long magic = getDivisorNumbers(div); -+ long mul = magic >>> 32; -+ int sign = number >> 31; -+ int result = (int)(((long)number * mul) >>> magic) - sign; -+ */ -+ /* -+ Unsigned usage: -+ int number; -+ long magic = getDivisorNumbers(div); -+ long mul = magic >>> 32; -+ int result = (int)(((long)number * mul) >>> magic); -+ */ -+ -+ int p = 31; -+ -+ // all these variables are UNSIGNED! -+ int t = two31 + (d >>> 31); -+ int anc = t - 1 - (int)((t & mask)%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; -+ 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/ca/spottedleaf/starlight/common/util/SaveUtil.java b/src/main/java/ca/spottedleaf/starlight/common/util/SaveUtil.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c2903150c8fc6955f4f4f71acc932b6c2ac83484 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/starlight/common/util/SaveUtil.java -@@ -0,0 +1,192 @@ -+package ca.spottedleaf.starlight.common.util; -+ -+import ca.spottedleaf.starlight.common.light.SWMRNibbleArray; -+import ca.spottedleaf.starlight.common.light.StarLightEngine; -+import com.mojang.logging.LogUtils; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.nbt.ListTag; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.chunk.ChunkAccess; -+import net.minecraft.world.level.chunk.status.ChunkStatus; -+import org.slf4j.Logger; -+ -+public final class SaveUtil { -+ -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ -+ private static final int STARLIGHT_LIGHT_VERSION = 9; -+ -+ public static int getLightVersion() { -+ return STARLIGHT_LIGHT_VERSION; -+ } -+ -+ private static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state"; -+ private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state"; -+ private static final String STARLIGHT_VERSION_TAG = "starlight.light_version"; -+ -+ public static void saveLightHook(final Level world, final ChunkAccess chunk, final CompoundTag nbt) { -+ try { -+ saveLightHookReal(world, chunk, nbt); -+ } catch (final Throwable ex) { -+ // failing to inject is not fatal so we catch anything here. if it fails, it will have correctly set lit to false -+ // for Vanilla to relight on load and it will not set our lit tag so we will relight on load -+ if (ex instanceof ThreadDeath) { -+ throw (ThreadDeath)ex; -+ } -+ LOGGER.warn("Failed to inject light data into save data for chunk " + chunk.getPos() + ", chunk light will be recalculated on its next load", ex); -+ } -+ } -+ -+ private static void saveLightHookReal(final Level world, final ChunkAccess chunk, final CompoundTag tag) { -+ if (tag == null) { -+ return; -+ } -+ -+ final int minSection = WorldUtil.getMinLightSection(world); -+ final int maxSection = WorldUtil.getMaxLightSection(world); -+ -+ SWMRNibbleArray[] blockNibbles = chunk.getBlockNibbles(); -+ SWMRNibbleArray[] skyNibbles = chunk.getSkyNibbles(); -+ -+ boolean lit = chunk.isLightCorrect() || !(world instanceof ServerLevel); -+ // diff start - store our tag for whether light data is init'd -+ if (lit) { -+ tag.putBoolean("isLightOn", false); -+ } -+ // diff end - store our tag for whether light data is init'd -+ ChunkStatus status = ChunkStatus.byName(tag.getString("Status")); -+ -+ CompoundTag[] sections = new CompoundTag[maxSection - minSection + 1]; -+ -+ ListTag sectionsStored = tag.getList("sections", 10); -+ -+ for (int i = 0; i < sectionsStored.size(); ++i) { -+ CompoundTag sectionStored = sectionsStored.getCompound(i); -+ int k = sectionStored.getByte("Y"); -+ -+ // strip light data -+ sectionStored.remove("BlockLight"); -+ sectionStored.remove("SkyLight"); -+ -+ if (!sectionStored.isEmpty()) { -+ sections[k - minSection] = sectionStored; -+ } -+ } -+ -+ if (lit && status.isOrAfter(ChunkStatus.LIGHT)) { -+ for (int i = minSection; i <= maxSection; ++i) { -+ SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection].getSaveState(); -+ SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection].getSaveState(); -+ if (blockNibble != null || skyNibble != null) { -+ CompoundTag section = sections[i - minSection]; -+ if (section == null) { -+ section = new CompoundTag(); -+ section.putByte("Y", (byte)i); -+ sections[i - minSection] = section; -+ } -+ -+ // we store under the same key so mod programs editing nbt -+ // can still read the data, hopefully. -+ // however, for compatibility we store chunks as unlit so vanilla -+ // is forced to re-light them if it encounters our data. It's too much of a burden -+ // to try and maintain compatibility with a broken and inferior skylight management system. -+ -+ if (blockNibble != null) { -+ if (blockNibble.data != null) { -+ section.putByteArray("BlockLight", blockNibble.data); -+ } -+ section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state); -+ } -+ -+ if (skyNibble != null) { -+ if (skyNibble.data != null) { -+ section.putByteArray("SkyLight", skyNibble.data); -+ } -+ section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state); -+ } -+ } -+ } -+ } -+ -+ // rewrite section list -+ sectionsStored.clear(); -+ for (CompoundTag section : sections) { -+ if (section != null) { -+ sectionsStored.add(section); -+ } -+ } -+ tag.put("sections", sectionsStored); -+ if (lit) { -+ tag.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // only mark as fully lit after we have successfully injected our data -+ } -+ } -+ -+ public static void loadLightHook(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) { -+ try { -+ loadLightHookReal(world, pos, tag, into); -+ } catch (final Throwable ex) { -+ // failing to inject is not fatal so we catch anything here. if it fails, then we simply relight. Not a problem, we get correct -+ // lighting in both cases. -+ if (ex instanceof ThreadDeath) { -+ throw (ThreadDeath)ex; -+ } -+ LOGGER.warn("Failed to load light for chunk " + pos + ", light will be recalculated", ex); -+ } -+ } -+ -+ private static void loadLightHookReal(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) { -+ if (into == null) { -+ return; -+ } -+ final int minSection = WorldUtil.getMinLightSection(world); -+ final int maxSection = WorldUtil.getMaxLightSection(world); -+ -+ into.setLightCorrect(false); // mark as unlit in case we fail parsing -+ -+ SWMRNibbleArray[] blockNibbles = StarLightEngine.getFilledEmptyLight(world); -+ SWMRNibbleArray[] skyNibbles = StarLightEngine.getFilledEmptyLight(world); -+ -+ -+ // start copy from the original method -+ boolean lit = tag.get("isLightOn") != null && tag.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION; -+ boolean canReadSky = world.dimensionType().hasSkyLight(); -+ ChunkStatus status = ChunkStatus.byName(tag.getString("Status")); -+ if (lit && status.isOrAfter(ChunkStatus.LIGHT)) { // diff - we add the status check here -+ ListTag sections = tag.getList("sections", 10); -+ -+ for (int i = 0; i < sections.size(); ++i) { -+ CompoundTag sectionData = sections.getCompound(i); -+ int y = sectionData.getByte("Y"); -+ -+ if (sectionData.contains("BlockLight", 7)) { -+ // this is where our diff is -+ blockNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety -+ } else { -+ blockNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG)); -+ } -+ -+ if (canReadSky) { -+ if (sectionData.contains("SkyLight", 7)) { -+ // we store under the same key so mod programs editing nbt -+ // can still read the data, hopefully. -+ // however, for compatibility we store chunks as unlit so vanilla -+ // is forced to re-light them if it encounters our data. It's too much of a burden -+ // to try and maintain compatibility with a broken and inferior skylight management system. -+ skyNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety -+ } else { -+ skyNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG)); -+ } -+ } -+ } -+ } -+ // end copy from vanilla -+ -+ into.setBlockNibbles(blockNibbles); -+ into.setSkyNibbles(skyNibbles); -+ into.setLightCorrect(lit); // now we set lit here, only after we've correctly parsed data -+ } -+ -+ private SaveUtil() {} -+} -diff --git a/src/main/java/ca/spottedleaf/starlight/common/util/WorldUtil.java b/src/main/java/ca/spottedleaf/starlight/common/util/WorldUtil.java -new file mode 100644 -index 0000000000000000000000000000000000000000..dd995e25ae620ae36cd5eecb2fe10ad034ba50d2 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/starlight/common/util/WorldUtil.java -@@ -0,0 +1,47 @@ -+package ca.spottedleaf.starlight.common.util; -+ -+import net.minecraft.world.level.LevelHeightAccessor; -+ -+public final class WorldUtil { -+ -+ // min, max are inclusive -+ -+ public static int getMaxSection(final LevelHeightAccessor world) { -+ return world.getMaxSection() - 1; // getMaxSection() is exclusive -+ } -+ -+ public static int getMinSection(final LevelHeightAccessor world) { -+ return world.getMinSection(); -+ } -+ -+ public static int getMaxLightSection(final LevelHeightAccessor world) { -+ return getMaxSection(world) + 1; -+ } -+ -+ public static int getMinLightSection(final LevelHeightAccessor world) { -+ return getMinSection(world) - 1; -+ } -+ -+ -+ -+ public static int getTotalSections(final LevelHeightAccessor world) { -+ return getMaxSection(world) - getMinSection(world) + 1; -+ } -+ -+ public static int getTotalLightSections(final LevelHeightAccessor world) { -+ return getMaxLightSection(world) - getMinLightSection(world) + 1; -+ } -+ -+ public static int getMinBlockY(final LevelHeightAccessor world) { -+ return getMinSection(world) << 4; -+ } -+ -+ public static int getMaxBlockY(final LevelHeightAccessor world) { -+ return (getMaxSection(world) << 4) | 15; -+ } -+ -+ private WorldUtil() { -+ throw new RuntimeException(); -+ } -+ -+} -diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java -index 534d9c380f26d6cce3c99fa88ad2e15410535094..e47fb2aa5e885162cae5cbfc9f33ff7864bf538e 100644 ---- a/src/main/java/io/papermc/paper/command/PaperCommand.java -+++ b/src/main/java/io/papermc/paper/command/PaperCommand.java -@@ -42,6 +42,7 @@ public final class PaperCommand extends Command { - commands.put(Set.of("dumpitem"), new DumpItemCommand()); - commands.put(Set.of("mobcaps", "playermobcaps"), new MobcapsCommand()); - commands.put(Set.of("dumplisteners"), new DumpListenersCommand()); -+ commands.put(Set.of("fixlight"), new FixLightCommand()); - - return commands.entrySet().stream() - .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue()))) -diff --git a/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java -new file mode 100644 -index 0000000000000000000000000000000000000000..56524cbe4303901007e1e7fb3703a19efbf79ae7 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java -@@ -0,0 +1,109 @@ -+package io.papermc.paper.command.subcommands; -+ -+import io.papermc.paper.command.PaperSubcommand; -+import io.papermc.paper.util.MCUtil; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.server.level.ThreadedLevelLightEngine; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.chunk.ChunkAccess; -+import org.bukkit.command.CommandSender; -+import org.bukkit.craftbukkit.entity.CraftPlayer; -+import org.bukkit.entity.Player; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.checker.nullness.qual.Nullable; -+import org.checkerframework.framework.qual.DefaultQualifier; -+ -+import static net.kyori.adventure.text.Component.text; -+import static net.kyori.adventure.text.format.NamedTextColor.BLUE; -+import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA; -+import static net.kyori.adventure.text.format.NamedTextColor.RED; -+ -+@DefaultQualifier(NonNull.class) -+public final class FixLightCommand implements PaperSubcommand { -+ @Override -+ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { -+ this.doFixLight(sender, args); -+ return true; -+ } -+ -+ private void doFixLight(final CommandSender sender, final String[] args) { -+ if (!(sender instanceof Player)) { -+ sender.sendMessage(text("Only players can use this command", RED)); -+ return; -+ } -+ @Nullable Runnable post = null; -+ int radius = 2; -+ if (args.length > 0) { -+ try { -+ final int parsed = Integer.parseInt(args[0]); -+ if (parsed < 0) { -+ sender.sendMessage(text("Radius cannot be negative!", RED)); -+ return; -+ } -+ final int maxRadius = 32; -+ radius = Math.min(maxRadius, parsed); -+ if (radius != parsed) { -+ post = () -> sender.sendMessage(text("Radius '" + parsed + "' was not in the required range [0, " + maxRadius + "], it was lowered to the maximum (" + maxRadius + " chunks).", RED)); -+ } -+ } catch (final Exception e) { -+ sender.sendMessage(text("'" + args[0] + "' is not a valid number.", RED)); -+ return; -+ } -+ } -+ -+ CraftPlayer player = (CraftPlayer) sender; -+ ServerPlayer handle = player.getHandle(); -+ ServerLevel world = (ServerLevel) handle.level(); -+ ThreadedLevelLightEngine lightengine = world.getChunkSource().getLightEngine(); -+ this.starlightFixLight(handle, world, lightengine, radius, post); -+ } -+ -+ private void starlightFixLight( -+ final ServerPlayer sender, -+ final ServerLevel world, -+ final ThreadedLevelLightEngine lightengine, -+ final int radius, -+ final @Nullable Runnable done -+ ) { -+ final long start = System.nanoTime(); -+ final java.util.LinkedHashSet chunks = new java.util.LinkedHashSet<>(MCUtil.getSpiralOutChunks(sender.blockPosition(), radius)); // getChunkCoordinates is actually just bad mappings, this function rets position as blockpos -+ -+ final int[] pending = new int[1]; -+ for (java.util.Iterator iterator = chunks.iterator(); iterator.hasNext(); ) { -+ final ChunkPos chunkPos = iterator.next(); -+ -+ final @Nullable ChunkAccess chunk = (ChunkAccess) world.getChunkSource().getChunkForLighting(chunkPos.x, chunkPos.z); -+ if (chunk == null || !chunk.isLightCorrect() || !chunk.getStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) { -+ // cannot relight this chunk -+ iterator.remove(); -+ continue; -+ } -+ -+ ++pending[0]; -+ } -+ -+ final int[] relitChunks = new int[1]; -+ lightengine.relight(chunks, -+ (final ChunkPos chunkPos) -> { -+ ++relitChunks[0]; -+ sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append( -+ text("Relit chunk ", BLUE), text(chunkPos.toString()), -+ text(", progress: ", BLUE), text((int) (Math.round(100.0 * (double) (relitChunks[0]) / (double) pending[0])) + "%") -+ )); -+ }, -+ (final int totalRelit) -> { -+ final long end = System.nanoTime(); -+ final long diff = Math.round(1.0e-6 * (end - start)); -+ sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append( -+ text("Relit ", BLUE), text(totalRelit), -+ text(" chunks. Took ", BLUE), text(diff + "ms") -+ )); -+ if (done != null) { -+ done.run(); -+ } -+ } -+ ); -+ sender.getBukkitEntity().sendMessage(text().color(BLUE).append(text("Relighting "), text(pending[0], DARK_AQUA), text(" chunks"))); -+ } -+} -diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java -index b12921579cb9ab3cbf5607841cc84f2f843624ea..88729d92878f98729eb5669cce5ae5b1418865a1 100644 ---- a/src/main/java/net/minecraft/server/level/ChunkHolder.java -+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java -@@ -51,7 +51,7 @@ public class ChunkHolder { - private volatile CompletableFuture> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage - private volatile CompletableFuture> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage - private volatile CompletableFuture> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage -- private CompletableFuture chunkToSave; -+ public CompletableFuture chunkToSave; // Paper - public - @Nullable - private final DebugBuffer chunkToSaveHistory; - public int oldTicketLevel; -@@ -261,6 +261,12 @@ public class ChunkHolder { - } - } - -+ // Paper start - starlight -+ public void broadcast(Packet packet, boolean onChunkViewEdge) { -+ this.broadcast(this.playerProvider.getPlayers(this.pos, onChunkViewEdge), packet); -+ } -+ // Paper end - starlight -+ - public void broadcastChanges(LevelChunk chunk) { - if (this.hasChangedSections || !this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty()) { - Level world = chunk.getLevel(); -diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java -index 35f627c58e93c03ee58b44877398432bba57dc2d..d3f63185edd1db9fab3887ea3f08982435b3a23c 100644 ---- a/src/main/java/net/minecraft/server/level/ChunkMap.java -+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java -@@ -128,7 +128,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - private final LongSet entitiesInLevel; - public final ServerLevel level; - private final ThreadedLevelLightEngine lightEngine; -- private final BlockableEventLoop mainThreadExecutor; -+ public final BlockableEventLoop mainThreadExecutor; // Paper - public - public ChunkGenerator generator; - private final RandomState randomState; - private final ChunkGeneratorStructureState chunkGeneratorState; -diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java -index c473cb1888e9ab0e91ba44f1439b81742758304e..7a48ae2ba962ff56d0abff581b51f28b48bd9aae 100644 ---- a/src/main/java/net/minecraft/server/level/DistanceManager.java -+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java -@@ -379,7 +379,7 @@ public abstract class DistanceManager { - } - - public void removeTicketsOnClosing() { -- ImmutableSet> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.LIGHT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve -+ ImmutableSet> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.LIGHT, TicketType.FUTURE_AWAIT, TicketType.CHUNK_RELIGHT, ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET); // Paper - add additional tickets to preserve - ObjectIterator>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); - - while (objectiterator.hasNext()) { -diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java -index 1dfae40ec19c4df0a97359941cf2c948cd1c9cb2..f206df06a7d8895175db31d4a840d7467ffe826f 100644 ---- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java -+++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java -@@ -23,6 +23,17 @@ import net.minecraft.world.level.chunk.LightChunkGetter; - import net.minecraft.world.level.lighting.LevelLightEngine; - import org.slf4j.Logger; - -+// Paper start -+import ca.spottedleaf.starlight.common.light.StarLightEngine; -+import io.papermc.paper.util.CoordinateUtils; -+import java.util.function.Supplier; -+import net.minecraft.world.level.lighting.LayerLightEventListener; -+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; -+import it.unimi.dsi.fastutil.longs.LongArrayList; -+import it.unimi.dsi.fastutil.longs.LongIterator; -+import net.minecraft.world.level.chunk.status.ChunkStatus; -+// Paper end -+ - public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable { - public static final int DEFAULT_BATCH_SIZE = 1000; - private static final Logger LOGGER = LogUtils.getLogger(); -@@ -33,6 +44,12 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - private final int taskPerBatch = 1000; - private final AtomicBoolean scheduled = new AtomicBoolean(); - -+ // Paper start - replace light engine impl -+ protected final ca.spottedleaf.starlight.common.light.StarLightInterface theLightEngine; -+ public final boolean hasBlockLight; -+ public final boolean hasSkyLight; -+ // Paper end - replace light engine impl -+ - public ThreadedLevelLightEngine( - LightChunkGetter chunkProvider, - ChunkMap chunkStorage, -@@ -40,11 +57,153 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - ProcessorMailbox processor, - ProcessorHandle> executor - ) { -- super(chunkProvider, true, hasBlockLight); -+ super(chunkProvider, false, false); // Paper - destroy vanilla light engine state - this.chunkMap = chunkStorage; - this.sorterMailbox = executor; - this.taskMailbox = processor; -+ // Paper start - replace light engine impl -+ this.hasBlockLight = true; -+ this.hasSkyLight = hasBlockLight; // Nice variable name. -+ this.theLightEngine = new ca.spottedleaf.starlight.common.light.StarLightInterface(chunkProvider, this.hasSkyLight, this.hasBlockLight, this); -+ // Paper end - replace light engine impl -+ } -+ -+ // Paper start - replace light engine impl -+ protected final ChunkAccess getChunk(final int chunkX, final int chunkZ) { -+ return ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().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 ChunkPos chunkPos = iterator.next(); -+ -+ final ChunkAccess chunk = (ChunkAccess)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().getChunkForLighting(chunkPos.x, chunkPos.z); -+ if (chunk == null || !chunk.isLightCorrect() || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { -+ // cannot relight this chunk -+ iterator.remove(); -+ continue; -+ } -+ -+ final Long id = Long.valueOf(this.relightCounter++); -+ -+ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().addTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, io.papermc.paper.util.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), id); -+ ticketIds.put(chunkPos, id); -+ -+ ++totalChunks; -+ } -+ -+ this.taskMailbox.tell(() -> { -+ this.theLightEngine.relightChunks(chunks, (ChunkPos chunkPos) -> { -+ chunkLightCallback.accept(chunkPos); -+ ((java.util.concurrent.Executor)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().mainThreadProcessor).execute(() -> { -+ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().chunkMap.getUpdatingChunkIfPresent(chunkPos.toLong()).broadcast(new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket(chunkPos, ThreadedLevelLightEngine.this, null, null), false); -+ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().removeTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, io.papermc.paper.util.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), ticketIds.get(chunkPos)); -+ }); -+ }, onComplete); -+ }); -+ this.tryScheduleUpdate(); -+ -+ return totalChunks; -+ } -+ -+ private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap(); -+ -+ private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ, -+ final Supplier runnable) { -+ final ServerLevel world = (ServerLevel)this.theLightEngine.getWorld(); -+ -+ final ChunkAccess center = this.theLightEngine.getAnyChunkNow(chunkX, chunkZ); -+ if (center == null || !center.getStatus().isOrAfter(ChunkStatus.LIGHT)) { -+ // do not accept updates in unlit chunks, unless we might be generating a chunk. thanks to the amazing -+ // chunk scheduling, we could be lighting and generating a chunk at the same time -+ return; -+ } -+ -+ if (center.getStatus() != ChunkStatus.FULL) { -+ // do not keep chunk loaded, we are probably in a gen thread -+ // if we proceed to add a ticket the chunk will be loaded, which is not what we want (avoid cascading gen) -+ runnable.get(); -+ return; -+ } -+ -+ if (!world.getChunkSource().chunkMap.mainThreadExecutor.isSameThread()) { -+ // ticket logic is not safe to run off-main, re-schedule -+ world.getChunkSource().chunkMap.mainThreadExecutor.execute(() -> { -+ this.queueTaskForSection(chunkX, chunkY, chunkZ, runnable); -+ }); -+ return; -+ } -+ -+ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ -+ final ca.spottedleaf.starlight.common.light.StarLightInterface.LightQueue.ChunkTasks updateFuture = runnable.get(); -+ -+ if (updateFuture == null) { -+ // not scheduled -+ return; -+ } -+ -+ if (updateFuture.isTicketAdded) { -+ // ticket already added -+ return; -+ } -+ updateFuture.isTicketAdded = true; -+ -+ final int references = this.chunksBeingWorkedOn.addTo(key, 1); -+ if (references == 0) { -+ final ChunkPos pos = new ChunkPos(chunkX, chunkZ); -+ world.getChunkSource().addRegionTicket(ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); -+ } -+ -+ updateFuture.onComplete.thenAcceptAsync((final Void ignore) -> { -+ final int newReferences = this.chunksBeingWorkedOn.get(key); -+ if (newReferences == 1) { -+ this.chunksBeingWorkedOn.remove(key); -+ final ChunkPos pos = new ChunkPos(chunkX, chunkZ); -+ world.getChunkSource().removeRegionTicket(ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); -+ } else { -+ this.chunksBeingWorkedOn.put(key, newReferences - 1); -+ } -+ }, world.getChunkSource().chunkMap.mainThreadExecutor).whenComplete((final Void ignore, final Throwable thr) -> { -+ if (thr != null) { -+ LOGGER.error("Failed to remove ticket level for post chunk task " + new ChunkPos(chunkX, chunkZ), thr); -+ } -+ }); -+ } -+ -+ @Override -+ public boolean hasLightWork() { -+ // route to new light engine -+ return this.theLightEngine.hasUpdates(); -+ } -+ -+ @Override -+ public LayerLightEventListener getLayerListener(final LightLayer lightType) { -+ return lightType == LightLayer.BLOCK ? this.theLightEngine.getBlockReader() : this.theLightEngine.getSkyReader(); -+ } -+ -+ @Override -+ public int getRawBrightness(final BlockPos pos, final int ambientDarkness) { -+ // need to use new light hooks for this -+ final int sky = this.theLightEngine.getSkyReader().getLightValue(pos) - ambientDarkness; -+ // Don't fetch the block light level if the skylight level is 15, since the value will never be higher. -+ if (sky == 15) return 15; -+ final int block = this.theLightEngine.getBlockReader().getLightValue(pos); -+ return Math.max(sky, block); - } -+ // Paper end - replace light engine imp - - @Override - public void close() { -@@ -57,16 +216,16 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - - @Override - public void checkBlock(BlockPos pos) { -- BlockPos blockPos = pos.immutable(); -- this.addTask( -- SectionPos.blockToSectionCoord(pos.getX()), -- SectionPos.blockToSectionCoord(pos.getZ()), -- ThreadedLevelLightEngine.TaskType.PRE_UPDATE, -- Util.name(() -> super.checkBlock(blockPos), () -> "checkBlock " + blockPos) -- ); -+ // Paper start - replace light engine impl -+ final BlockPos posCopy = pos.immutable(); -+ this.queueTaskForSection(posCopy.getX() >> 4, posCopy.getY() >> 4, posCopy.getZ() >> 4, () -> { -+ return this.theLightEngine.blockChange(posCopy); -+ }); -+ // Paper end - replace light engine impl - } - - protected void updateChunkStatus(ChunkPos pos) { -+ if (true) return; // Paper - replace light engine impl - this.addTask(pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { - super.retainData(pos, false); - super.setLightEnabled(pos, false); -@@ -84,17 +243,16 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - - @Override - public void updateSectionStatus(SectionPos pos, boolean notReady) { -- this.addTask( -- pos.x(), -- pos.z(), -- () -> 0, -- ThreadedLevelLightEngine.TaskType.PRE_UPDATE, -- Util.name(() -> super.updateSectionStatus(pos, notReady), () -> "updateSectionStatus " + pos + " " + notReady) -- ); -+ // Paper start - replace light engine impl -+ this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> { -+ return this.theLightEngine.sectionChange(pos, notReady); -+ }); -+ // Paper end - replace light engine impl - } - - @Override - public void propagateLightSources(ChunkPos chunkPos) { -+ if (true) return; // Paper - replace light engine impl - this.addTask( - chunkPos.x, - chunkPos.z, -@@ -105,6 +263,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - - @Override - public void setLightEnabled(ChunkPos pos, boolean retainData) { -+ if (true) return; // Paper - replace light engine impl - this.addTask( - pos.x, - pos.z, -@@ -115,6 +274,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - - @Override - public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles) { -+ if (true) return; // Paper - replace light engine impl - this.addTask( - pos.x(), - pos.z(), -@@ -139,12 +299,14 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - - @Override - public void retainData(ChunkPos pos, boolean retainData) { -+ if (true) return; // Paper - replace light engine impl - this.addTask( - pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> super.retainData(pos, retainData), () -> "retainData " + pos) - ); - } - - public CompletableFuture initializeLight(ChunkAccess chunk, boolean bl) { -+ if (true) return CompletableFuture.completedFuture(chunk); // Paper - replace light engine impl - ChunkPos chunkPos = chunk.getPos(); - this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { - LevelChunkSection[] levelChunkSections = chunk.getSections(); -@@ -165,6 +327,37 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - } - - public CompletableFuture lightChunk(ChunkAccess chunk, boolean excludeBlocks) { -+ // Paper start - replace light engine impl -+ if (true) { -+ boolean lit = excludeBlocks; -+ final ChunkPos chunkPos = chunk.getPos(); -+ -+ return CompletableFuture.supplyAsync(() -> { -+ final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(chunk); -+ if (!lit) { -+ chunk.setLightCorrect(false); -+ this.theLightEngine.lightChunk(chunk, emptySections); -+ chunk.setLightCorrect(true); -+ } else { -+ this.theLightEngine.forceLoadInChunk(chunk, 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(chunkPos.x, chunkPos.z); -+ } -+ -+ this.chunkMap.releaseLightTicket(chunkPos); -+ return chunk; -+ }, (runnable) -> { -+ this.theLightEngine.scheduleChunkLight(chunkPos, runnable); -+ this.tryScheduleUpdate(); -+ }).whenComplete((final ChunkAccess c, final Throwable throwable) -> { -+ if (throwable != null) { -+ LOGGER.error("Failed to light chunk " + chunkPos, throwable); -+ } -+ }); -+ } -+ // Paper end - replace light engine impl - ChunkPos chunkPos = chunk.getPos(); - chunk.setLightCorrect(false); - this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { -@@ -180,7 +373,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - } - - public void tryScheduleUpdate() { -- if ((!this.lightTasks.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) { -+ if (this.hasLightWork() && this.scheduled.compareAndSet(false, true)) { // Paper // Paper - rewrite light engine - this.taskMailbox.tell(() -> { - this.runUpdate(); - this.scheduled.set(false); -@@ -201,7 +394,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - } - - objectListIterator.back(j); -- super.runLightUpdates(); -+ this.theLightEngine.propagateChanges(); // Paper - rewrite light engine - - for (int var5 = 0; objectListIterator.hasNext() && var5 < i; var5++) { - Pair pair2 = objectListIterator.next(); -diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java -index 0d536d72ac918fbd403397ff369d10143ee9c204..6051e5f272838ef23276a90e21c2fc821ca155d1 100644 ---- a/src/main/java/net/minecraft/server/level/TicketType.java -+++ b/src/main/java/net/minecraft/server/level/TicketType.java -@@ -26,6 +26,7 @@ public class TicketType { - public static final TicketType UNKNOWN = TicketType.create("unknown", Comparator.comparingLong(ChunkPos::toLong), 1); - public static final TicketType PLUGIN = TicketType.create("plugin", (a, b) -> 0); // CraftBukkit - public static final TicketType PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit -+ public static final TicketType CHUNK_RELIGHT = create("light_update", Long::compareTo); // Paper - ensure chunks stay loaded for lighting - - public static TicketType create(String name, Comparator argumentComparator) { - return new TicketType<>(name, argumentComparator, 0L); -diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java -index 333a02e08cccf5cb0efa2076582cbd69e95ff0c0..ca4c8e256047a4af45811c3e772b5a959e2ae941 100644 ---- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java -+++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java -@@ -109,6 +109,27 @@ public class WorldGenRegion implements WorldGenLevel { - } - } - -+ // Paper start - starlight -+ @Override -+ public int getBrightness(final net.minecraft.world.level.LightLayer lightLayer, final BlockPos blockPos) { -+ final ChunkAccess chunk = this.getChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4); -+ if (!chunk.isLightCorrect()) { -+ return 0; -+ } -+ return this.getLightEngine().getLayerListener(lightLayer).getLightValue(blockPos); -+ } -+ -+ -+ @Override -+ public int getRawBrightness(final BlockPos blockPos, final int subtract) { -+ final ChunkAccess chunk = this.getChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4); -+ if (!chunk.isLightCorrect()) { -+ return 0; -+ } -+ return this.getLightEngine().getRawBrightness(blockPos, subtract); -+ } -+ // Paper end - starlight -+ - public boolean isOldChunkAround(ChunkPos chunkPos, int checkRadius) { - return this.level.getChunkSource().chunkMap.isOldChunkAround(chunkPos, checkRadius); - } -diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -index f87d9cb38caf3bf92fd32f2118f76799ede418db..c7da359c525522b55763e594a1db0c26a026b73f 100644 ---- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -+++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -@@ -812,6 +812,7 @@ public abstract class BlockBehaviour implements FeatureElement { - this.spawnTerrainParticles = blockbase_info.spawnTerrainParticles; - this.instrument = blockbase_info.instrument; - this.replaceable = blockbase_info.replaceable; -+ this.conditionallyFullOpaque = this.canOcclude & this.useShapeForLightOcclusion; // Paper - } - // Paper start - Perf: impl cached craft block data, lazy load to fix issue with loading at the wrong time - private org.bukkit.craftbukkit.block.data.CraftBlockData cachedCraftBlockData; -@@ -848,6 +849,18 @@ public abstract class BlockBehaviour implements FeatureElement { - return this.shapeExceedsCube; - } - // Paper end -+ // Paper start - starlight -+ 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; -+ } -+ // Paper end - starlight - - public void initCache() { - this.fluidState = ((Block) this.owner).getFluidState(this.asState()); -@@ -856,6 +869,7 @@ public abstract class BlockBehaviour implements FeatureElement { - this.cache = new BlockBehaviour.BlockStateBase.Cache(this.asState()); - } - this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here -+ this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque() ? -1 : this.cache.lightBlock; // Paper - starlight - cache opacity for light - - this.legacySolid = this.calculateSolid(); - } -diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java -index f4e3bd2ae4f63e6d3d25463a3635b8f89fecc068..1f8c72b6c7d8683d67880fa175843c73b3d39b78 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java -+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java -@@ -77,7 +77,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom - @Nullable - protected BlendingData blendingData; - public final Map heightmaps = Maps.newEnumMap(Heightmap.Types.class); -- protected ChunkSkyLightSources skyLightSources; -+ // Paper - starlight - remove skyLightSources - private final Map structureStarts = Maps.newHashMap(); - private final Map structuresRefences = Maps.newHashMap(); - protected final Map pendingBlockEntities = Maps.newHashMap(); -@@ -89,8 +89,55 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom - private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry(); - public org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer(ChunkAccess.DATA_TYPE_REGISTRY); - // CraftBukkit end -+ // Paper start - rewrite light engine -+ private volatile ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] blockNibbles; -+ -+ private volatile ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] skyNibbles; -+ -+ private volatile boolean[] skyEmptinessMap; -+ -+ private volatile boolean[] blockEmptinessMap; -+ -+ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getBlockNibbles() { -+ return this.blockNibbles; -+ } -+ -+ public void setBlockNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) { -+ this.blockNibbles = nibbles; -+ } -+ -+ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getSkyNibbles() { -+ return this.skyNibbles; -+ } -+ -+ public void setSkyNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) { -+ this.skyNibbles = nibbles; -+ } -+ -+ public boolean[] getSkyEmptinessMap() { -+ return this.skyEmptinessMap; -+ } -+ -+ public void setSkyEmptinessMap(final boolean[] emptinessMap) { -+ this.skyEmptinessMap = emptinessMap; -+ } -+ -+ public boolean[] getBlockEmptinessMap() { -+ return this.blockEmptinessMap; -+ } -+ -+ public void setBlockEmptinessMap(final boolean[] emptinessMap) { -+ this.blockEmptinessMap = emptinessMap; -+ } -+ // Paper end - rewrite light engine - - public ChunkAccess(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry biomeRegistry, long inhabitedTime, @Nullable LevelChunkSection[] sectionArray, @Nullable BlendingData blendingData) { -+ // Paper start - rewrite light engine -+ if (!(this instanceof ImposterProtoChunk)) { -+ this.setBlockNibbles(ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(heightLimitView)); -+ this.setSkyNibbles(ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(heightLimitView)); -+ } -+ // Paper end - rewrite light engine - this.locX = pos.x; this.locZ = pos.z; // Paper - reduce need for field lookups - this.chunkPos = pos; this.coordinateKey = ChunkPos.asLong(locX, locZ); // Paper - cache long key - this.upgradeData = upgradeData; -@@ -99,7 +146,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom - this.inhabitedTime = inhabitedTime; - this.postProcessing = new ShortList[heightLimitView.getSectionsCount()]; - this.blendingData = blendingData; -- this.skyLightSources = new ChunkSkyLightSources(heightLimitView); -+ // Paper - starlight - remove skyLightSources - if (sectionArray != null) { - if (this.sections.length == sectionArray.length) { - System.arraycopy(sectionArray, 0, this.sections, 0, this.sections.length); -@@ -510,12 +557,12 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom - } - - public void initializeLightSources() { -- this.skyLightSources.fillFrom(this); -+ // Paper - starlight - remove skyLightSources - } - - @Override - public ChunkSkyLightSources getSkyLightSources() { -- return this.skyLightSources; -+ return null; // Paper - starlight - remove skyLightSources - } - - public static record TicksToSave(SerializableTickContainer blocks, SerializableTickContainer fluids) { -diff --git a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java -index 2ee1658532cb00d7bcd1d11e03f19d21ca7f2a9e..ac754827172a4de600d0a57a7d11853481a2dbf2 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java -@@ -21,6 +21,40 @@ public class EmptyLevelChunk extends LevelChunk { - this.biome = biomeEntry; - } - -+ // Paper start - starlight -+ @Override -+ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getBlockNibbles() { -+ return ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(this.getLevel()); -+ } -+ -+ @Override -+ public void setBlockNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) {} -+ -+ @Override -+ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getSkyNibbles() { -+ return ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(this.getLevel()); -+ } -+ -+ @Override -+ public void setSkyNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) {} -+ -+ @Override -+ public boolean[] getSkyEmptinessMap() { -+ return null; -+ } -+ -+ @Override -+ public void setSkyEmptinessMap(final boolean[] emptinessMap) {} -+ -+ @Override -+ public boolean[] getBlockEmptinessMap() { -+ return null; -+ } -+ -+ @Override -+ public void setBlockEmptinessMap(final boolean[] emptinessMap) {} -+ // Paper end - starlight -+ - @Override - public BlockState getBlockState(BlockPos pos) { - return Blocks.VOID_AIR.defaultBlockState(); -diff --git a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java -index 2953e93965aa688be8fc1620580701ba0c9d907e..aa5dee839d4c0dbc3c2abee9b501ec250c575cb3 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java -@@ -47,6 +47,48 @@ public class ImposterProtoChunk extends ProtoChunk { - this.allowWrites = propagateToWrapped; - } - -+ // Paper start - rewrite light engine -+ @Override -+ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getBlockNibbles() { -+ return this.wrapped.getBlockNibbles(); -+ } -+ -+ @Override -+ public void setBlockNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) { -+ this.wrapped.setBlockNibbles(nibbles); -+ } -+ -+ @Override -+ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getSkyNibbles() { -+ return this.wrapped.getSkyNibbles(); -+ } -+ -+ @Override -+ public void setSkyNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) { -+ this.wrapped.setSkyNibbles(nibbles); -+ } -+ -+ @Override -+ public boolean[] getSkyEmptinessMap() { -+ return this.wrapped.getSkyEmptinessMap(); -+ } -+ -+ @Override -+ public void setSkyEmptinessMap(final boolean[] emptinessMap) { -+ this.wrapped.setSkyEmptinessMap(emptinessMap); -+ } -+ -+ @Override -+ public boolean[] getBlockEmptinessMap() { -+ return this.wrapped.getBlockEmptinessMap(); -+ } -+ -+ @Override -+ public void setBlockEmptinessMap(final boolean[] emptinessMap) { -+ this.wrapped.setBlockEmptinessMap(emptinessMap); -+ } -+ // Paper end - rewrite light engine -+ - @Nullable - @Override - public BlockEntity getBlockEntity(BlockPos pos) { -diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -index 8de6ad8b131061b2dae440dff71e2e6e7af2de39..bac191f92ea3735df19c68d5568c2c7962c8680f 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -@@ -222,6 +222,12 @@ public class LevelChunk extends ChunkAccess { - - public LevelChunk(ServerLevel world, ProtoChunk protoChunk, @Nullable LevelChunk.PostLoadProcessor entityLoader) { - this(world, protoChunk.getPos(), protoChunk.getUpgradeData(), protoChunk.unpackBlockTicks(), protoChunk.unpackFluidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), entityLoader, protoChunk.getBlendingData()); -+ // Paper start - rewrite light engine -+ this.setBlockNibbles(protoChunk.getBlockNibbles()); -+ this.setSkyNibbles(protoChunk.getSkyNibbles()); -+ this.setSkyEmptinessMap(protoChunk.getSkyEmptinessMap()); -+ this.setBlockEmptinessMap(protoChunk.getBlockEmptinessMap()); -+ // Paper end - rewrite light engine - Iterator iterator = protoChunk.getBlockEntities().values().iterator(); - - while (iterator.hasNext()) { -@@ -248,7 +254,7 @@ public class LevelChunk extends ChunkAccess { - } - } - -- this.skyLightSources = protoChunk.skyLightSources; -+ // Paper - starlight - remove skyLightSources - this.setLightCorrect(protoChunk.isLightCorrect()); - this.unsaved = true; - this.needsDecoration = true; // CraftBukkit -@@ -437,7 +443,7 @@ public class LevelChunk extends ChunkAccess { - ProfilerFiller gameprofilerfiller = this.level.getProfiler(); - - gameprofilerfiller.push("updateSkyLightSources"); -- this.skyLightSources.update(this, j, i, l); -+ // Paper - starlight - remove skyLightSources - gameprofilerfiller.popPush("queueCheckLight"); - this.level.getChunkSource().getLightEngine().checkBlock(blockposition); - gameprofilerfiller.pop(); -diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java -index 2fa0097a9374a89177e4f1068d1bfed30b8ff122..fa9df6ebcd90d4e9e5836a37212b1f60665783b1 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java -+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java -@@ -155,7 +155,7 @@ public class PalettedContainer implements PaletteResize, PalettedContainer - return this.get(this.strategy.getIndex(x, y, z)); - } - -- protected T get(int index) { -+ public T get(int index) { // Paper - public - PalettedContainer.Data data = this.data; - return data.palette.valueFor(data.storage.get(index)); - } -diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java -index bcc70883d23d38c408130ffe778205e371ff4e8a..576ae0cb138b265c8a3995de7b5ebc827d50949d 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java -@@ -143,7 +143,7 @@ public class ProtoChunk extends ChunkAccess { - } - - if (LightEngine.hasDifferentLightProperties(this, pos, blockState, state)) { -- this.skyLightSources.update(this, m, j, o); -+ // Paper - starlight - remove skyLightSources - this.lightEngine.checkBlock(pos); - } - } -diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java -index ae992aeb8b836e8c2e5bab338ae46cc31c317245..95318092f8281d98132d1d3ceb4a5c36cf32eb05 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java -+++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java -@@ -117,6 +117,18 @@ public class ChunkStatus { - private final ChunkType chunkType; - private final EnumSet heightmapsAfter; - -+ // Paper start - starlight -+ public static ChunkStatus getStatus(String name) { -+ try { -+ // We need this otherwise we return EMPTY for invalid names -+ ResourceLocation key = new ResourceLocation(name); -+ return BuiltInRegistries.CHUNK_STATUS.getOptional(key).orElse(null); -+ } catch (Exception ex) { -+ return null; // invalid name -+ } -+ } -+ // Paper end - starlight -+ - private static ChunkStatus register( - String id, - @Nullable ChunkStatus previous, -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java -index 88f0aca2da0e14ed5ec0513944fa0ba28b73b5d1..01d6b8683a9fa30d05b03ebfef8ee2dca4e83a56 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java -@@ -90,6 +90,14 @@ public class ChunkSerializer { - private static final int CURRENT_DATA_VERSION = net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion(); - private static final boolean JUST_CORRUPT_IT = Boolean.getBoolean("Paper.ignoreWorldDataVersion"); - // Paper end - Do not let the server load chunks from newer versions -+ // Paper start - replace light engine impl -+ private static final int STARLIGHT_LIGHT_VERSION = 9; -+ -+ private static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state"; -+ private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state"; -+ private static final String STARLIGHT_VERSION_TAG = "starlight.light_version"; -+ // Paper end - replace light engine impl -+ - public ChunkSerializer() {} - - // Paper start - guard against serializing mismatching coordinates -@@ -121,19 +129,26 @@ public class ChunkSerializer { - } - - UpgradeData chunkconverter = nbt.contains("UpgradeData", 10) ? new UpgradeData(nbt.getCompound("UpgradeData"), world) : UpgradeData.EMPTY; -- boolean flag = nbt.getBoolean("isLightOn"); -+ boolean flag = getStatus(nbt) != null && getStatus(nbt).isOrAfter(ChunkStatus.LIGHT) && nbt.get("isLightOn") != null && nbt.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION; // Paper - ListTag nbttaglist = nbt.getList("sections", 10); - int i = world.getSectionsCount(); - LevelChunkSection[] achunksection = new LevelChunkSection[i]; - boolean flag1 = world.dimensionType().hasSkyLight(); - ServerChunkCache chunkproviderserver = world.getChunkSource(); - LevelLightEngine levellightengine = chunkproviderserver.getLightEngine(); -+ // Paper start -+ ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] blockNibbles = ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(world); -+ ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] skyNibbles = ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(world); -+ final int minSection = io.papermc.paper.util.WorldUtil.getMinLightSection(world); -+ final int maxSection = io.papermc.paper.util.WorldUtil.getMaxLightSection(world); -+ boolean canReadSky = world.dimensionType().hasSkyLight(); -+ // Paper end - Registry iregistry = world.registryAccess().registryOrThrow(Registries.BIOME); - Codec>> codec = ChunkSerializer.makeBiomeCodecRW(iregistry); // CraftBukkit - read/write - boolean flag2 = false; - - for (int j = 0; j < nbttaglist.size(); ++j) { -- CompoundTag nbttagcompound1 = nbttaglist.getCompound(j); -+ CompoundTag nbttagcompound1 = nbttaglist.getCompound(j); CompoundTag sectionData = nbttagcompound1; // Paper - byte b0 = nbttagcompound1.getByte("Y"); - int k = world.getSectionIndexFromSectionY(b0); - -@@ -169,19 +184,39 @@ public class ChunkSerializer { - boolean flag3 = nbttagcompound1.contains("BlockLight", 7); - boolean flag4 = flag1 && nbttagcompound1.contains("SkyLight", 7); - -- if (flag3 || flag4) { -- if (!flag2) { -- levellightengine.retainData(chunkPos, true); -- flag2 = true; -- } -- -+ // Paper start - rewrite the light engine -+ if (flag) { -+ try { -+ int y = sectionData.getByte("Y"); -+ // Paper end - rewrite the light engine - if (flag3) { -- levellightengine.queueSectionData(LightLayer.BLOCK, SectionPos.of(chunkPos, b0), new DataLayer(nbttagcompound1.getByteArray("BlockLight"))); -+ // Paper start - rewrite the light engine -+ // this is where our diff is -+ blockNibbles[y - minSection] = new ca.spottedleaf.starlight.common.light.SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety -+ } else { -+ blockNibbles[y - minSection] = new ca.spottedleaf.starlight.common.light.SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG)); -+ // Paper end - rewrite the light engine - } - - if (flag4) { -- levellightengine.queueSectionData(LightLayer.SKY, SectionPos.of(chunkPos, b0), new DataLayer(nbttagcompound1.getByteArray("SkyLight"))); -+ // Paper start - rewrite the light engine -+ // we store under the same key so mod programs editing nbt -+ // can still read the data, hopefully. -+ // however, for compatibility we store chunks as unlit so vanilla -+ // is forced to re-light them if it encounters our data. It's too much of a burden -+ // to try and maintain compatibility with a broken and inferior skylight management system. -+ skyNibbles[y - minSection] = new ca.spottedleaf.starlight.common.light.SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety -+ } else if (flag1) { -+ skyNibbles[y - minSection] = new ca.spottedleaf.starlight.common.light.SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG)); -+ // Paper end - rewrite the light engine -+ } -+ -+ // Paper start - rewrite the light engine -+ } catch (Exception ex) { -+ LOGGER.warn("Failed to load light data for chunk " + chunkPos + " in world '" + world.getWorld().getName() + "', light will be regenerated", ex); -+ flag = false; - } -+ // Paper end - rewrite light engine - } - } - -@@ -211,6 +246,8 @@ public class ChunkSerializer { - }, chunkPos); - - object1 = new LevelChunk(world.getLevel(), chunkPos, chunkconverter, levelchunkticks, levelchunkticks1, l, achunksection, ChunkSerializer.postLoadChunk(world, nbt), blendingdata); -+ ((LevelChunk)object1).setBlockNibbles(blockNibbles); // Paper - replace light impl -+ ((LevelChunk)object1).setSkyNibbles(skyNibbles); // Paper - replace light impl - } else { - ProtoChunkTicks protochunkticklist = ProtoChunkTicks.load(nbt.getList("block_ticks", 10), (s) -> { - return BuiltInRegistries.BLOCK.getOptional(ResourceLocation.tryParse(s)); -@@ -219,6 +256,8 @@ public class ChunkSerializer { - return BuiltInRegistries.FLUID.getOptional(ResourceLocation.tryParse(s)); - }, chunkPos); - ProtoChunk protochunk = new ProtoChunk(chunkPos, chunkconverter, achunksection, protochunkticklist, protochunkticklist1, world, iregistry, blendingdata); -+ protochunk.setBlockNibbles(blockNibbles); // Paper - replace light impl -+ protochunk.setSkyNibbles(skyNibbles); // Paper - replace light impl - - object1 = protochunk; - protochunk.setInhabitedTime(l); -@@ -340,6 +379,12 @@ public class ChunkSerializer { - // CraftBukkit end - - public static CompoundTag write(ServerLevel world, ChunkAccess chunk) { -+ // Paper start - rewrite light impl -+ final int minSection = io.papermc.paper.util.WorldUtil.getMinLightSection(world); -+ final int maxSection = io.papermc.paper.util.WorldUtil.getMaxLightSection(world); -+ ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] blockNibbles = chunk.getBlockNibbles(); -+ ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] skyNibbles = chunk.getSkyNibbles(); -+ // Paper end - rewrite light impl - ChunkPos chunkcoordintpair = chunk.getPos(); - CompoundTag nbttagcompound = NbtUtils.addCurrentDataVersion(new CompoundTag()); - -@@ -389,11 +434,14 @@ public class ChunkSerializer { - for (int i = lightenginethreaded.getMinLightSection(); i < lightenginethreaded.getMaxLightSection(); ++i) { - int j = chunk.getSectionIndexFromSectionY(i); - boolean flag1 = j >= 0 && j < achunksection.length; -- DataLayer nibblearray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); -- DataLayer nibblearray1 = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); -+ // Paper - replace light engine - -- if (flag1 || nibblearray != null || nibblearray1 != null) { -- CompoundTag nbttagcompound1 = new CompoundTag(); -+ // Paper start - replace light engine -+ ca.spottedleaf.starlight.common.light.SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection].getSaveState(); -+ ca.spottedleaf.starlight.common.light.SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection].getSaveState(); -+ if (flag1 || blockNibble != null || skyNibble != null) { -+ // Paper end - replace light engine -+ CompoundTag nbttagcompound1 = new CompoundTag(); CompoundTag section = nbttagcompound1; // Paper - - if (flag1) { - LevelChunkSection chunksection = achunksection[j]; -@@ -402,13 +450,27 @@ public class ChunkSerializer { - nbttagcompound1.put("biomes", (Tag) codec.encodeStart(NbtOps.INSTANCE, chunksection.getBiomes()).getOrThrow()); - } - -- if (nibblearray != null && !nibblearray.isEmpty()) { -- nbttagcompound1.putByteArray("BlockLight", nibblearray.getData()); -+ // Paper start -+ // we store under the same key so mod programs editing nbt -+ // can still read the data, hopefully. -+ // however, for compatibility we store chunks as unlit so vanilla -+ // is forced to re-light them if it encounters our data. It's too much of a burden -+ // to try and maintain compatibility with a broken and inferior skylight management system. -+ -+ if (blockNibble != null) { -+ if (blockNibble.data != null) { -+ section.putByteArray("BlockLight", blockNibble.data); -+ } -+ section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state); - } - -- if (nibblearray1 != null && !nibblearray1.isEmpty()) { -- nbttagcompound1.putByteArray("SkyLight", nibblearray1.getData()); -+ if (skyNibble != null) { -+ if (skyNibble.data != null) { -+ section.putByteArray("SkyLight", skyNibble.data); -+ } -+ section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state); - } -+ // Paper end - - if (!nbttagcompound1.isEmpty()) { - nbttagcompound1.putByte("Y", (byte) i); -@@ -419,7 +481,8 @@ public class ChunkSerializer { - - nbttagcompound.put("sections", nbttaglist); - if (flag) { -- nbttagcompound.putBoolean("isLightOn", true); -+ nbttagcompound.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // Paper -+ nbttagcompound.putBoolean("isLightOn", false); // Paper - set to false but still store, this allows us to detect --eraseCache (as eraseCache _removes_) - } - - ListTag nbttaglist1 = new ListTag(); -@@ -493,6 +556,17 @@ public class ChunkSerializer { - })); - } - -+ // Paper start -+ public static @Nullable ChunkStatus getStatus(@Nullable CompoundTag compound) { -+ if (compound == null) { -+ return null; -+ } -+ -+ // Note: Copied from below -+ return ChunkStatus.getStatus(compound.getString("Status")); -+ } -+ // Paper end -+ - public static ChunkType getChunkTypeFromTag(@Nullable CompoundTag nbt) { - return nbt != null ? ChunkStatus.byName(nbt.getString("Status")).getChunkType() : ChunkType.PROTOCHUNK; - } -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index 34f03dd227181a03fa90845067424a26382bab9b..4b6a04e47f5d4c071607516519098fab317dcf12 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -@@ -507,12 +507,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { - } - } - -- for (final ChunkPos pos : chunksToRelight) { -- final ChunkAccess chunk = serverChunkCache.getChunk(pos.x, pos.z, false); -- if (chunk != null) { -- serverChunkCache.getLightEngine().lightChunk(chunk, false); -- } -- } -+ serverChunkCache.getLightEngine().relight(chunksToRelight, pos -> {}, relit -> {}); // Paper - Starlight - - return true; - // Paper end - implement regenerate chunk method diff --git a/patches/server/0986-Rewrite-chunk-system.patch b/patches/server/0986-Rewrite-chunk-system.patch deleted file mode 100644 index d1a25169df..0000000000 --- a/patches/server/0986-Rewrite-chunk-system.patch +++ /dev/null @@ -1,21905 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Thu, 11 Mar 2021 02:32:30 -0800 -Subject: [PATCH] Rewrite chunk system - -Rebased patches: - -New player chunk loader system - -Make ChunkStatus.EMPTY not rely on the main thread for completion - -In order to do this, we need to push the POI consistency checks -to a later status. Since FULL is the only other status that -uses the main thread, it can go there. - -The consistency checks are only really for when a desync occurs, -and so that delaying the check only matters when the chunk data -has desync'd. As long as the desync is sorted before the -chunk is full loaded (i.e before setBlock can occur on -a chunk), it should not matter. - -This change is primarily due to behavioural changes -in the chunk task queue brought by region threading - -which is to split the queue into separate regions. As such, -it is required that in order for the sync load to complete -that the region owning the chunk drain and execute the task -while ticking. However, that is not always possible in -region threading. Thus, removing the main thread reliance allows -the chunk to progress without requiring a tick thread. -Specifically, this allows far sync loads (outside of a specific -regions bounds) to occur without issue - namely with structure -searching. - -Increase parallelism for neighbour writing chunk statuses - -Namely, everything after FEATURES. By creating a dependency -chain indicating what chunks are in use, we can safely -schedule completely independent tasks in parallel. This -will allow the chunk system to scale beyond 10 threads -per world. - -Properly cancel chunk load tasks that were not scheduled - -Since the chunk load task was not scheduled, the entity/poi load -task fields will not be set, but the task complete counter -will not be adjusted. Thus, the chunk load task will not complete. - -To resolve this, detect when the entity/poi tasks were not scheduled -and decrement the task complete counter in such cases. - -Mark POI/Entity load tasks as completed before releasing scheduling lock - -It must be marked as completed during that lock hold since the -waiters field is set to null. Thus, any other thread attempting -a cancellation will fail to remove from waiters. Also, any -other thread attempting to cancel may set the completed field -to true which would cause accept() to fail as well. - -Completion was always designed to happen while holding the -scheduling lock to prevent these race conditions. The code -was originally set up to complete while not holding the -scheduling lock to avoid invoking callbacks while holding the -lock, however the access to the completion field was not -considered. - -Resolve this by marking the callback as completed during the -lock, but invoking the accept() function after releasing -the lock. This will prevent any cancellation attempts to be -blocked, and allow the current thread to complete the callback -without any issues. - -Cache whether region files do not exist - -The repeated I/O of creating the directory for the regionfile -or for checking if the file exists can be heavy in -when pushing chunk generation extremely hard - as each chunk gen -request may effectively go through to the I/O thread. - -Use coordinate-based locking to increase chunk system parallelism - -A significant overhead in Folia comes from the chunk system's -locks, the ticket lock and the scheduling lock. The public -test server, which had ~330 players, had signficant performance -problems with these locks: ~80% of the time spent ticking -was _waiting_ for the locks to free. Given that it used -around 15 cores total at peak, this is a complete and utter loss -of potential. - -To address this issue, I have replaced the ticket lock and scheduling -lock with two ReentrantAreaLocks. The ReentrantAreaLock takes a -shift, which is used internally to group positions into sections. -This grouping is neccessary, as the possible radius of area that -needs to be acquired for any given lock usage is up to 64. As such, -the shift is critical to reduce the number of areas required to lock -for any lock operation. Currently, it is set to a shift of 6, which -is identical to the ticket level propagation shift (and, it must be -at least the ticket level propagation shift AND the region shift). - -The chunk system locking changes required a complete rewrite of the -chunk system tick, chunk system unload, and chunk system ticket level -propagation - as all of the previous logic only works with a single -global lock. - -This does introduce two other section shifts: the lock shift, and the -ticket shift. The lock shift is simply what shift the area locks use, -and the ticket shift represents the size of the ticket sections. -Currently, these values are just set to the region shift for simplicity. -However, they are not arbitrary: the lock shift must be at least the size -of the ticket shift and must be at least the size of the region shift. -The ticket shift must also be >= the ceil(log2(max ticket level source)). - -The chunk system's ticket propagator is now global state, instead of -region state. This cleans up the logic for ticket levels significantly, -and removes usage of the region lock in this area, but it also means -that the addition of a ticket no longer creates a region. To alleviate -the side effects of this change, the global tick thread now processes -ticket level updates for each world every tick to guarantee eventual -ticket level processing. The chunk system also provides a hook to -process ticket level changes in a given _section_, so that the -region queue can guarantee that after adding its reference counter -that the region section is created/exists/wont be destroyed. - -The ticket propagator operates by updating the sources in a single ticket -section, and propagating the updates to its 1 radius neighbours. This -allows the ticket updates to occur in parallel or selectively (see above). -Currently, the process ticket level update function operates by -polling from a concurrent queue of sections to update and simply -invoking the single section update logic. This allows the function -to operate completely in parallel, provided the queue is ordered right. -Additionally, this limits the area used in the ticket/scheduling lock -when processing updates, which should massively increase parallelism compared -to before. - -The chunk system ticket addition for expirable ticket types has been modified -to no longer track exact tick deadlines, as this relies on what region the -ticket is in. Instead, the chunk system tracks a map of -lock section -> (chunk coordinate -> expire ticket count) and every ticket -has been changed to have a removeDelay count that is decremented each tick. -Each region searches its own sections to find tickets to try to expire. - -Chunk system unloading has been modified to track unloads by lock section. -The ordering is determined by which section a chunk resides in. -The unload process now removes from unload sections and processes -the full unload stages (1, 2, 3) before moving to the next section, if possible. -This allows the unload logic to only hold one lock section at a time for -each lock, which is a massive parallelism increase. - -In stress testing, these changes lowered the locking overhead to only 5% -from ~70%, which completely fix the original problem as described. - -== AT == -public net.minecraft.server.level.ChunkHolder pos -public net.minecraft.server.level.ChunkMap overworldDataStorage -public-f net.minecraft.world.level.chunk.storage.RegionFileStorage -public net.minecraft.server.level.ChunkMap getPoiManager()Lnet/minecraft/world/entity/ai/village/poi/PoiManager; - -diff --git a/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java b/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4fd9a0cd8f1e6ae1a97e963dc7731a80bc6fac5b ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java -@@ -0,0 +1,395 @@ -+package ca.spottedleaf.concurrentutil.lock; -+ -+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; -+import it.unimi.dsi.fastutil.HashCommon; -+import java.util.ArrayList; -+import java.util.List; -+import java.util.concurrent.ConcurrentHashMap; -+import java.util.concurrent.locks.LockSupport; -+ -+public final class ReentrantAreaLock { -+ -+ public final int coordinateShift; -+ -+ // aggressive load factor to reduce contention -+ private final ConcurrentHashMap nodes = new ConcurrentHashMap<>(128, 0.2f); -+ -+ public ReentrantAreaLock(final int coordinateShift) { -+ this.coordinateShift = coordinateShift; -+ } -+ -+ public boolean isHeldByCurrentThread(final int x, final int z) { -+ final Thread currThread = Thread.currentThread(); -+ final int shift = this.coordinateShift; -+ final int sectionX = x >> shift; -+ final int sectionZ = z >> shift; -+ -+ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ)); -+ final Node node = this.nodes.get(coordinate); -+ -+ return node != null && node.thread == currThread; -+ } -+ -+ public boolean isHeldByCurrentThread(final int centerX, final int centerZ, final int radius) { -+ return this.isHeldByCurrentThread(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius); -+ } -+ -+ public boolean isHeldByCurrentThread(final int fromX, final int fromZ, final int toX, final int toZ) { -+ if (fromX > toX || fromZ > toZ) { -+ throw new IllegalArgumentException(); -+ } -+ -+ final Thread currThread = Thread.currentThread(); -+ final int shift = this.coordinateShift; -+ final int fromSectionX = fromX >> shift; -+ final int fromSectionZ = fromZ >> shift; -+ final int toSectionX = toX >> shift; -+ final int toSectionZ = toZ >> shift; -+ -+ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) { -+ for (int currX = fromSectionX; currX <= toSectionX; ++currX) { -+ final Coordinate coordinate = new Coordinate(Coordinate.key(currX, currZ)); -+ -+ final Node node = this.nodes.get(coordinate); -+ -+ if (node == null || node.thread != currThread) { -+ return false; -+ } -+ } -+ } -+ -+ return true; -+ } -+ -+ public Node tryLock(final int x, final int z) { -+ return this.tryLock(x, z, x, z); -+ } -+ -+ public Node tryLock(final int centerX, final int centerZ, final int radius) { -+ return this.tryLock(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius); -+ } -+ -+ public Node tryLock(final int fromX, final int fromZ, final int toX, final int toZ) { -+ if (fromX > toX || fromZ > toZ) { -+ throw new IllegalArgumentException(); -+ } -+ -+ final Thread currThread = Thread.currentThread(); -+ final int shift = this.coordinateShift; -+ final int fromSectionX = fromX >> shift; -+ final int fromSectionZ = fromZ >> shift; -+ final int toSectionX = toX >> shift; -+ final int toSectionZ = toZ >> shift; -+ -+ final List areaAffected = new ArrayList<>(); -+ -+ final Node ret = new Node(this, areaAffected, currThread); -+ -+ boolean failed = false; -+ -+ // try to fast acquire area -+ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) { -+ for (int currX = fromSectionX; currX <= toSectionX; ++currX) { -+ final Coordinate coordinate = new Coordinate(Coordinate.key(currX, currZ)); -+ -+ final Node prev = this.nodes.putIfAbsent(coordinate, ret); -+ -+ if (prev == null) { -+ areaAffected.add(coordinate); -+ continue; -+ } -+ -+ if (prev.thread != currThread) { -+ failed = true; -+ break; -+ } -+ } -+ } -+ -+ if (!failed) { -+ return ret; -+ } -+ -+ // failed, undo logic -+ if (!areaAffected.isEmpty()) { -+ for (int i = 0, len = areaAffected.size(); i < len; ++i) { -+ final Coordinate key = areaAffected.get(i); -+ -+ if (this.nodes.remove(key) != ret) { -+ throw new IllegalStateException(); -+ } -+ } -+ -+ areaAffected.clear(); -+ -+ // since we inserted, we need to drain waiters -+ Thread unpark; -+ while ((unpark = ret.pollOrBlockAdds()) != null) { -+ LockSupport.unpark(unpark); -+ } -+ } -+ -+ return null; -+ } -+ -+ public Node lock(final int x, final int z) { -+ final Thread currThread = Thread.currentThread(); -+ final int shift = this.coordinateShift; -+ final int sectionX = x >> shift; -+ final int sectionZ = z >> shift; -+ -+ final List areaAffected = new ArrayList<>(1); -+ -+ final Node ret = new Node(this, areaAffected, currThread); -+ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ)); -+ -+ for (long failures = 0L;;) { -+ final Node park; -+ -+ // try to fast acquire area -+ { -+ final Node prev = this.nodes.putIfAbsent(coordinate, ret); -+ -+ if (prev == null) { -+ areaAffected.add(coordinate); -+ return ret; -+ } else if (prev.thread != currThread) { -+ park = prev; -+ } else { -+ // only one node we would want to acquire, and it's owned by this thread already -+ return ret; -+ } -+ } -+ -+ ++failures; -+ -+ if (failures > 128L && park.add(currThread)) { -+ LockSupport.park(); -+ } else { -+ // high contention, spin wait -+ if (failures < 128L) { -+ for (long i = 0; i < failures; ++i) { -+ Thread.onSpinWait(); -+ } -+ failures = failures << 1; -+ } else if (failures < 1_200L) { -+ LockSupport.parkNanos(1_000L); -+ failures = failures + 1L; -+ } else { // scale 0.1ms (100us) per failure -+ Thread.yield(); -+ LockSupport.parkNanos(100_000L * failures); -+ failures = failures + 1L; -+ } -+ } -+ } -+ } -+ -+ public Node lock(final int centerX, final int centerZ, final int radius) { -+ return this.lock(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius); -+ } -+ -+ public Node lock(final int fromX, final int fromZ, final int toX, final int toZ) { -+ if (fromX > toX || fromZ > toZ) { -+ throw new IllegalArgumentException(); -+ } -+ -+ final Thread currThread = Thread.currentThread(); -+ final int shift = this.coordinateShift; -+ final int fromSectionX = fromX >> shift; -+ final int fromSectionZ = fromZ >> shift; -+ final int toSectionX = toX >> shift; -+ final int toSectionZ = toZ >> shift; -+ -+ if (((fromSectionX ^ toSectionX) | (fromSectionZ ^ toSectionZ)) == 0) { -+ return this.lock(fromX, fromZ); -+ } -+ -+ final List areaAffected = new ArrayList<>(); -+ -+ final Node ret = new Node(this, areaAffected, currThread); -+ -+ for (long failures = 0L;;) { -+ Node park = null; -+ boolean addedToArea = false; -+ boolean alreadyOwned = false; -+ boolean allOwned = true; -+ -+ // try to fast acquire area -+ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) { -+ for (int currX = fromSectionX; currX <= toSectionX; ++currX) { -+ final Coordinate coordinate = new Coordinate(Coordinate.key(currX, currZ)); -+ -+ final Node prev = this.nodes.putIfAbsent(coordinate, ret); -+ -+ if (prev == null) { -+ addedToArea = true; -+ allOwned = false; -+ areaAffected.add(coordinate); -+ continue; -+ } -+ -+ if (prev.thread != currThread) { -+ park = prev; -+ alreadyOwned = true; -+ break; -+ } -+ } -+ } -+ -+ if (park == null) { -+ if (alreadyOwned && !allOwned) { -+ throw new IllegalStateException("Improper lock usage: Should never acquire intersecting areas"); -+ } -+ return ret; -+ } -+ -+ // failed, undo logic -+ if (addedToArea) { -+ for (int i = 0, len = areaAffected.size(); i < len; ++i) { -+ final Coordinate key = areaAffected.get(i); -+ -+ if (this.nodes.remove(key) != ret) { -+ throw new IllegalStateException(); -+ } -+ } -+ -+ areaAffected.clear(); -+ -+ // since we inserted, we need to drain waiters -+ Thread unpark; -+ while ((unpark = ret.pollOrBlockAdds()) != null) { -+ LockSupport.unpark(unpark); -+ } -+ } -+ -+ ++failures; -+ -+ if (failures > 128L && park.add(currThread)) { -+ LockSupport.park(park); -+ } else { -+ // high contention, spin wait -+ if (failures < 128L) { -+ for (long i = 0; i < failures; ++i) { -+ Thread.onSpinWait(); -+ } -+ failures = failures << 1; -+ } else if (failures < 1_200L) { -+ LockSupport.parkNanos(1_000L); -+ failures = failures + 1L; -+ } else { // scale 0.1ms (100us) per failure -+ Thread.yield(); -+ LockSupport.parkNanos(100_000L * failures); -+ failures = failures + 1L; -+ } -+ } -+ -+ if (addedToArea) { -+ // try again, so we need to allow adds so that other threads can properly block on us -+ ret.allowAdds(); -+ } -+ } -+ } -+ -+ public void unlock(final Node node) { -+ if (node.lock != this) { -+ throw new IllegalStateException("Unlock target lock mismatch"); -+ } -+ -+ final List areaAffected = node.areaAffected; -+ -+ if (areaAffected.isEmpty()) { -+ // here we are not in the node map, and so do not need to remove from the node map or unblock any waiters -+ return; -+ } -+ -+ // remove from node map; allowing other threads to lock -+ for (int i = 0, len = areaAffected.size(); i < len; ++i) { -+ final Coordinate coordinate = areaAffected.get(i); -+ if (this.nodes.remove(coordinate) != node) { -+ throw new IllegalStateException(); -+ } -+ } -+ -+ Thread unpark; -+ while ((unpark = node.pollOrBlockAdds()) != null) { -+ LockSupport.unpark(unpark); -+ } -+ } -+ -+ public static final class Node extends MultiThreadedQueue { -+ -+ private final ReentrantAreaLock lock; -+ private final List areaAffected; -+ private final Thread thread; -+ //private final Throwable WHO_CREATED_MY_ASS = new Throwable(); -+ -+ private Node(final ReentrantAreaLock lock, final List areaAffected, final Thread thread) { -+ this.lock = lock; -+ this.areaAffected = areaAffected; -+ this.thread = thread; -+ } -+ -+ @Override -+ public String toString() { -+ return "Node{" + -+ "areaAffected=" + this.areaAffected + -+ ", thread=" + this.thread + -+ '}'; -+ } -+ } -+ -+ private static final class Coordinate implements Comparable { -+ -+ public final long key; -+ -+ public Coordinate(final long key) { -+ this.key = key; -+ } -+ -+ public Coordinate(final int x, final int z) { -+ this.key = key(x, z); -+ } -+ -+ public static long key(final int x, final int z) { -+ return ((long)z << 32) | (x & 0xFFFFFFFFL); -+ } -+ -+ public static int x(final long key) { -+ return (int)key; -+ } -+ -+ public static int z(final long key) { -+ return (int)(key >>> 32); -+ } -+ -+ @Override -+ public int hashCode() { -+ return (int)HashCommon.mix(this.key); -+ } -+ -+ @Override -+ public boolean equals(final Object obj) { -+ if (this == obj) { -+ return true; -+ } -+ -+ if (!(obj instanceof Coordinate other)) { -+ return false; -+ } -+ -+ return this.key == other.key; -+ } -+ -+ // This class is intended for HashMap/ConcurrentHashMap usage, which do treeify bin nodes if the chain -+ // is too large. So we should implement compareTo to help. -+ @Override -+ public int compareTo(final Coordinate other) { -+ return Long.compare(this.key, other.key); -+ } -+ -+ @Override -+ public String toString() { -+ return "[" + x(this.key) + "," + z(this.key) + "]"; -+ } -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/concurrentutil/lock/SyncReentrantAreaLock.java b/src/main/java/ca/spottedleaf/concurrentutil/lock/SyncReentrantAreaLock.java -new file mode 100644 -index 0000000000000000000000000000000000000000..64b5803d002b2968841a5ddee987f98b72964e87 ---- /dev/null -+++ b/src/main/java/ca/spottedleaf/concurrentutil/lock/SyncReentrantAreaLock.java -@@ -0,0 +1,217 @@ -+package ca.spottedleaf.concurrentutil.lock; -+ -+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; -+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; -+import it.unimi.dsi.fastutil.longs.LongArrayList; -+import java.util.concurrent.locks.LockSupport; -+ -+// not concurrent, unlike ReentrantAreaLock -+// no incorrect lock usage detection (acquiring intersecting areas) -+// this class is nothing more than a performance reference for ReentrantAreaLock -+public final class SyncReentrantAreaLock { -+ -+ private final int coordinateShift; -+ -+ // aggressive load factor to reduce contention -+ private final Long2ReferenceOpenHashMap nodes = new Long2ReferenceOpenHashMap<>(128, 0.2f); -+ -+ public SyncReentrantAreaLock(final int coordinateShift) { -+ this.coordinateShift = coordinateShift; -+ } -+ -+ private static long key(final int x, final int z) { -+ return ((long)z << 32) | (x & 0xFFFFFFFFL); -+ } -+ -+ public Node lock(final int x, final int z) { -+ final Thread currThread = Thread.currentThread(); -+ final int shift = this.coordinateShift; -+ final int sectionX = x >> shift; -+ final int sectionZ = z >> shift; -+ -+ final LongArrayList areaAffected = new LongArrayList(); -+ -+ final Node ret = new Node(this, areaAffected, currThread); -+ -+ final long coordinate = key(sectionX, sectionZ); -+ -+ for (long failures = 0L;;) { -+ final Node park; -+ -+ synchronized (this) { -+ // try to fast acquire area -+ final Node prev = this.nodes.putIfAbsent(coordinate, ret); -+ -+ if (prev == null) { -+ areaAffected.add(coordinate); -+ return ret; -+ } else if (prev.thread != currThread) { -+ park = prev; -+ } else { -+ // only one node we would want to acquire, and it's owned by this thread already -+ return ret; -+ } -+ } -+ -+ ++failures; -+ -+ if (failures > 128L && park.add(currThread)) { -+ LockSupport.park(); -+ } else { -+ // high contention, spin wait -+ if (failures < 128L) { -+ for (long i = 0; i < failures; ++i) { -+ Thread.onSpinWait(); -+ } -+ failures = failures << 1; -+ } else if (failures < 1_200L) { -+ LockSupport.parkNanos(1_000L); -+ failures = failures + 1L; -+ } else { // scale 0.1ms (100us) per failure -+ Thread.yield(); -+ LockSupport.parkNanos(100_000L * failures); -+ failures = failures + 1L; -+ } -+ } -+ } -+ } -+ -+ public Node lock(final int centerX, final int centerZ, final int radius) { -+ return this.lock(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius); -+ } -+ -+ public Node lock(final int fromX, final int fromZ, final int toX, final int toZ) { -+ if (fromX > toX || fromZ > toZ) { -+ throw new IllegalArgumentException(); -+ } -+ -+ final Thread currThread = Thread.currentThread(); -+ final int shift = this.coordinateShift; -+ final int fromSectionX = fromX >> shift; -+ final int fromSectionZ = fromZ >> shift; -+ final int toSectionX = toX >> shift; -+ final int toSectionZ = toZ >> shift; -+ -+ final LongArrayList areaAffected = new LongArrayList(); -+ -+ final Node ret = new Node(this, areaAffected, currThread); -+ -+ for (long failures = 0L;;) { -+ Node park = null; -+ boolean addedToArea = false; -+ -+ synchronized (this) { -+ // try to fast acquire area -+ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) { -+ for (int currX = fromSectionX; currX <= toSectionX; ++currX) { -+ final long coordinate = key(currX, currZ); -+ -+ final Node prev = this.nodes.putIfAbsent(coordinate, ret); -+ -+ if (prev == null) { -+ addedToArea = true; -+ areaAffected.add(coordinate); -+ continue; -+ } -+ -+ if (prev.thread != currThread) { -+ park = prev; -+ break; -+ } -+ } -+ } -+ -+ if (park == null) { -+ return ret; -+ } -+ -+ // failed, undo logic -+ if (!areaAffected.isEmpty()) { -+ for (int i = 0, len = areaAffected.size(); i < len; ++i) { -+ final long key = areaAffected.getLong(i); -+ -+ if (!this.nodes.remove(key, ret)) { -+ throw new IllegalStateException(); -+ } -+ } -+ } -+ } -+ -+ if (addedToArea) { -+ areaAffected.clear(); -+ // since we inserted, we need to drain waiters -+ Thread unpark; -+ while ((unpark = ret.pollOrBlockAdds()) != null) { -+ LockSupport.unpark(unpark); -+ } -+ } -+ -+ ++failures; -+ -+ if (failures > 128L && park.add(currThread)) { -+ LockSupport.park(); -+ } else { -+ // high contention, spin wait -+ if (failures < 128L) { -+ for (long i = 0; i < failures; ++i) { -+ Thread.onSpinWait(); -+ } -+ failures = failures << 1; -+ } else if (failures < 1_200L) { -+ LockSupport.parkNanos(1_000L); -+ failures = failures + 1L; -+ } else { // scale 0.1ms (100us) per failure -+ Thread.yield(); -+ LockSupport.parkNanos(100_000L * failures); -+ failures = failures + 1L; -+ } -+ } -+ -+ if (addedToArea) { -+ // try again, so we need to allow adds so that other threads can properly block on us -+ ret.allowAdds(); -+ } -+ } -+ } -+ -+ public void unlock(final Node node) { -+ if (node.lock != this) { -+ throw new IllegalStateException("Unlock target lock mismatch"); -+ } -+ -+ final LongArrayList areaAffected = node.areaAffected; -+ -+ if (areaAffected.isEmpty()) { -+ // here we are not in the node map, and so do not need to remove from the node map or unblock any waiters -+ return; -+ } -+ -+ // remove from node map; allowing other threads to lock -+ synchronized (this) { -+ for (int i = 0, len = areaAffected.size(); i < len; ++i) { -+ final long coordinate = areaAffected.getLong(i); -+ if (!this.nodes.remove(coordinate, node)) { -+ throw new IllegalStateException(); -+ } -+ } -+ } -+ -+ Thread unpark; -+ while ((unpark = node.pollOrBlockAdds()) != null) { -+ LockSupport.unpark(unpark); -+ } -+ } -+ -+ public static final class Node extends MultiThreadedQueue { -+ -+ private final SyncReentrantAreaLock lock; -+ private final LongArrayList areaAffected; -+ private final Thread thread; -+ -+ private Node(final SyncReentrantAreaLock lock, final LongArrayList areaAffected, final Thread thread) { -+ this.lock = lock; -+ this.areaAffected = areaAffected; -+ this.thread = thread; -+ } -+ } -+} -diff --git a/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java -index e0338db4d6fa359029ed5edeacc3646aa98701f5..c03dbb4a74d00d794be4139f0f7c4b5ff1b01d38 100644 ---- a/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java -+++ b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java -@@ -41,14 +41,14 @@ public final class StarLightInterface { - protected final ArrayDeque cachedSkyPropagators; - protected final ArrayDeque cachedBlockPropagators; - -- protected final LightQueue lightQueue = new LightQueue(this); -+ public final io.papermc.paper.chunk.system.light.LightQueue lightQueue; // Paper - replace light queue - - protected final LayerLightEventListener skyReader; - protected final LayerLightEventListener blockReader; - protected final boolean isClientSide; - -- protected final int minSection; -- protected final int maxSection; -+ public final int minSection; // Paper - public -+ public final int maxSection; // Paper - public - protected final int minLightSection; - protected final int maxLightSection; - -@@ -182,6 +182,7 @@ public final class StarLightInterface { - StarLightInterface.this.sectionChange(pos, notReady); - } - }; -+ this.lightQueue = new io.papermc.paper.chunk.system.light.LightQueue(this); // Paper - replace light queue - } - - public boolean hasSkyLight() { -@@ -333,7 +334,7 @@ public final class StarLightInterface { - return this.lightAccess; - } - -- protected final SkyStarLightEngine getSkyLightEngine() { -+ public final SkyStarLightEngine getSkyLightEngine() { // Paper - public - if (this.cachedSkyPropagators == null) { - return null; - } -@@ -348,7 +349,7 @@ public final class StarLightInterface { - return ret; - } - -- protected final void releaseSkyLightEngine(final SkyStarLightEngine engine) { -+ public final void releaseSkyLightEngine(final SkyStarLightEngine engine) { // Paper - public - if (this.cachedSkyPropagators == null) { - return; - } -@@ -357,7 +358,7 @@ public final class StarLightInterface { - } - } - -- protected final BlockStarLightEngine getBlockLightEngine() { -+ public final BlockStarLightEngine getBlockLightEngine() { // Paper - public - if (this.cachedBlockPropagators == null) { - return null; - } -@@ -372,7 +373,7 @@ public final class StarLightInterface { - return ret; - } - -- protected final void releaseBlockLightEngine(final BlockStarLightEngine engine) { -+ public final void releaseBlockLightEngine(final BlockStarLightEngine engine) { // Paper - public - if (this.cachedBlockPropagators == null) { - return; - } -@@ -381,7 +382,7 @@ public final class StarLightInterface { - } - } - -- public LightQueue.ChunkTasks blockChange(final BlockPos pos) { -+ public io.papermc.paper.chunk.system.light.LightQueue.ChunkTasks blockChange(final BlockPos pos) { // Paper - rewrite chunk system - if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world - return null; - } -@@ -389,7 +390,7 @@ public final class StarLightInterface { - return this.lightQueue.queueBlockChange(pos); - } - -- public LightQueue.ChunkTasks sectionChange(final SectionPos pos, final boolean newEmptyValue) { -+ public io.papermc.paper.chunk.system.light.LightQueue.ChunkTasks sectionChange(final SectionPos pos, final boolean newEmptyValue) { // Paper - rewrite chunk system - if (this.world == null) { // empty world - return null; - } -@@ -519,57 +520,15 @@ public final class StarLightInterface { - } - - public void scheduleChunkLight(final ChunkPos pos, final Runnable run) { -- this.lightQueue.queueChunkLighting(pos, run); -+ throw new UnsupportedOperationException("No longer implemented, use the new lightQueue field to queue tasks"); // Paper - replace light queue - } - - public void removeChunkTasks(final ChunkPos pos) { -- this.lightQueue.removeChunk(pos); -+ throw new UnsupportedOperationException("No longer implemented, use the new lightQueue field to queue tasks"); // Paper - replace light queue - } - - public void propagateChanges() { -- if (this.lightQueue.isEmpty()) { -- return; -- } -- -- final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); -- final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); -- -- try { -- LightQueue.ChunkTasks task; -- while ((task = this.lightQueue.removeFirstTask()) != null) { -- if (task.lightTasks != null) { -- for (final Runnable run : task.lightTasks) { -- run.run(); -- } -- } -- -- final long coordinate = task.chunkCoordinate; -- final int chunkX = CoordinateUtils.getChunkX(coordinate); -- final int chunkZ = CoordinateUtils.getChunkZ(coordinate); -- -- final Set positions = task.changedPositions; -- final Boolean[] sectionChanges = task.changedSectionSet; -- -- if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) { -- skyEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); -- } -- if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) { -- blockEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); -- } -- -- if (skyEngine != null && task.queuedEdgeChecksSky != null) { -- skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksSky); -- } -- if (blockEngine != null && task.queuedEdgeChecksBlock != null) { -- blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksBlock); -- } -- -- task.onComplete.complete(null); -- } -- } finally { -- this.releaseSkyLightEngine(skyEngine); -- this.releaseBlockLightEngine(blockEngine); -- } -+ throw new UnsupportedOperationException("No longer implemented, task draining is now performed by the light thread"); // Paper - replace light queue - } - - public static final class LightQueue { -diff --git a/src/main/java/co/aikar/timings/WorldTimingsHandler.java b/src/main/java/co/aikar/timings/WorldTimingsHandler.java -index 2f0d9b953802dee821cfde82d22b0567cce8ee91..22687667ec69a954261e55e59261286ac1b8b8cd 100644 ---- a/src/main/java/co/aikar/timings/WorldTimingsHandler.java -+++ b/src/main/java/co/aikar/timings/WorldTimingsHandler.java -@@ -59,6 +59,16 @@ public class WorldTimingsHandler { - - public final Timing miscMobSpawning; - -+ public final Timing poiUnload; -+ public final Timing chunkUnload; -+ public final Timing poiSaveDataSerialization; -+ public final Timing chunkSave; -+ public final Timing chunkSaveDataSerialization; -+ public final Timing chunkSaveIOWait; -+ public final Timing chunkUnloadPrepareSave; -+ public final Timing chunkUnloadPOISerialization; -+ public final Timing chunkUnloadDataSave; -+ - public WorldTimingsHandler(Level server) { - String name = ((PrimaryLevelData) server.getLevelData()).getLevelName() + " - "; - -@@ -112,6 +122,16 @@ public class WorldTimingsHandler { - - - miscMobSpawning = Timings.ofSafe(name + "Mob spawning - Misc"); -+ -+ poiUnload = Timings.ofSafe(name + "Chunk unload - POI"); -+ chunkUnload = Timings.ofSafe(name + "Chunk unload - Chunk"); -+ poiSaveDataSerialization = Timings.ofSafe(name + "Chunk save - POI Data serialization"); -+ chunkSave = Timings.ofSafe(name + "Chunk save - Chunk"); -+ chunkSaveDataSerialization = Timings.ofSafe(name + "Chunk save - Chunk Data serialization"); -+ chunkSaveIOWait = Timings.ofSafe(name + "Chunk save - Chunk IO Wait"); -+ chunkUnloadPrepareSave = Timings.ofSafe(name + "Chunk unload - Async Save Prepare"); -+ chunkUnloadPOISerialization = Timings.ofSafe(name + "Chunk unload - POI Data Serialization"); -+ chunkUnloadDataSave = Timings.ofSafe(name + "Chunk unload - Data Serialization"); - } - - public static Timing getTickList(ServerLevel worldserver, String timingsType) { -diff --git a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java -index cff2f04409fab9abca87ceec85a551e1d59f9e7d..e3f56908cc8a9c3f4580def50fcfdc61bd495a71 100644 ---- a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java -+++ b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java -@@ -32,192 +32,41 @@ public final class ChunkSystem { - } - - public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) { -- level.chunkSource.mainThreadProcessor.execute(run); -+ level.chunkTaskScheduler.scheduleChunkTask(chunkX, chunkZ, run, priority); // Paper - rewrite chunk system - } - - public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen, - final ChunkStatus toStatus, final boolean addTicket, final PrioritisedExecutor.Priority priority, - final Consumer onComplete) { -- if (gen) { -- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); -- return; -- } -- scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> { -- if (chunk == null) { -- onComplete.accept(null); -- } else { -- if (chunk.getStatus().isOrAfter(toStatus)) { -- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); -- } else { -- onComplete.accept(null); -- } -- } -- }); -+ level.chunkTaskScheduler.scheduleChunkLoad(chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete); // Paper - rewrite chunk system - } - -- static final TicketType CHUNK_LOAD = TicketType.create("chunk_load", Long::compareTo); -- -- private static long chunkLoadCounter = 0L; -+ // Paper - rewrite chunk system - public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus, - final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer onComplete) { -- if (!Bukkit.isPrimaryThread()) { -- scheduleChunkTask(level, chunkX, chunkZ, () -> { -- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); -- }, priority); -- return; -- } -- -- final int minLevel = 33 + ChunkStatus.getDistance(toStatus); -- final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null; -- final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); -- -- if (addTicket) { -- level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); -- } -- level.chunkSource.runDistanceManagerUpdates(); -- -- final Consumer loadCallback = (final ChunkAccess chunk) -> { -- try { -- if (onComplete != null) { -- onComplete.accept(chunk); -- } -- } catch (final ThreadDeath death) { -- throw death; -- } catch (final Throwable thr) { -- LOGGER.error("Exception handling chunk load callback", thr); -- SneakyThrow.sneaky(thr); -- } finally { -- if (addTicket) { -- level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); -- level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); -- } -- } -- }; -- -- final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -- -- if (holder == null || holder.getTicketLevel() > minLevel) { -- loadCallback.accept(null); -- return; -- } -- -- final CompletableFuture> loadFuture = holder.getOrScheduleFuture(toStatus, level.chunkSource.chunkMap); -- -- if (loadFuture.isDone()) { -- loadCallback.accept(loadFuture.join().left().orElse(null)); -- return; -- } -- -- loadFuture.whenCompleteAsync((final Either either, final Throwable thr) -> { -- if (thr != null) { -- loadCallback.accept(null); -- return; -- } -- loadCallback.accept(either.left().orElse(null)); -- }, (final Runnable r) -> { -- scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST); -- }); -+ level.chunkTaskScheduler.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); // Paper - rewrite chunk system - } - - public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ, - final FullChunkStatus toStatus, final boolean addTicket, - final PrioritisedExecutor.Priority priority, final Consumer onComplete) { -- // This method goes unused until the chunk system rewrite -- if (toStatus == FullChunkStatus.INACCESSIBLE) { -- throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status"); -- } -- -- if (!Bukkit.isPrimaryThread()) { -- scheduleChunkTask(level, chunkX, chunkZ, () -> { -- scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); -- }, priority); -- return; -- } -- -- final int minLevel = 33 - (toStatus.ordinal() - 1); -- final int radius = toStatus.ordinal() - 1; -- final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null; -- final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); -- -- if (addTicket) { -- level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); -- } -- level.chunkSource.runDistanceManagerUpdates(); -- -- final Consumer loadCallback = (final LevelChunk chunk) -> { -- try { -- if (onComplete != null) { -- onComplete.accept(chunk); -- } -- } catch (final ThreadDeath death) { -- throw death; -- } catch (final Throwable thr) { -- LOGGER.error("Exception handling chunk load callback", thr); -- SneakyThrow.sneaky(thr); -- } finally { -- if (addTicket) { -- level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos); -- level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference); -- } -- } -- }; -- -- final ChunkHolder holder = level.chunkSource.chunkMap.updatingChunkMap.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -- -- if (holder == null || holder.getTicketLevel() > minLevel) { -- loadCallback.accept(null); -- return; -- } -- -- final CompletableFuture> tickingState; -- switch (toStatus) { -- case FULL: { -- tickingState = holder.getFullChunkFuture(); -- break; -- } -- case BLOCK_TICKING: { -- tickingState = holder.getTickingChunkFuture(); -- break; -- } -- case ENTITY_TICKING: { -- tickingState = holder.getEntityTickingChunkFuture(); -- break; -- } -- default: { -- throw new IllegalStateException("Cannot reach here"); -- } -- } -- -- if (tickingState.isDone()) { -- loadCallback.accept(tickingState.join().left().orElse(null)); -- return; -- } -- -- tickingState.whenCompleteAsync((final Either either, final Throwable thr) -> { -- if (thr != null) { -- loadCallback.accept(null); -- return; -- } -- loadCallback.accept(either.left().orElse(null)); -- }, (final Runnable r) -> { -- scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST); -- }); -+ level.chunkTaskScheduler.scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); // Paper - rewrite chunk system - } - - public static List getVisibleChunkHolders(final ServerLevel level) { -- return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values()); -+ return level.chunkTaskScheduler.chunkHolderManager.getOldChunkHolders(); // Paper - rewrite chunk system - } - - public static List getUpdatingChunkHolders(final ServerLevel level) { -- return new ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values()); -+ return level.chunkTaskScheduler.chunkHolderManager.getOldChunkHolders(); // Paper - rewrite chunk system - } - - public static int getVisibleChunkHolderCount(final ServerLevel level) { -- return level.chunkSource.chunkMap.visibleChunkMap.size(); -+ return level.chunkTaskScheduler.chunkHolderManager.size(); // Paper - rewrite chunk system - } - - public static int getUpdatingChunkHolderCount(final ServerLevel level) { -- return level.chunkSource.chunkMap.updatingChunkMap.size(); -+ return level.chunkTaskScheduler.chunkHolderManager.size(); // Paper - rewrite chunk system - } - - public static boolean hasAnyChunkHolders(final ServerLevel level) { -@@ -244,26 +93,32 @@ public final class ChunkSystem { - - public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { - chunk.playerChunk = holder; -+ chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.FULL; - } - - public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) { -- -+ chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.INACCESSIBLE; - } - - public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) { - chunk.level.getChunkSource().tickingChunks.add(chunk); -+ chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.BLOCK_TICKING; -+ chunk.level.chunkSource.chunkMap.tickingGenerated.incrementAndGet(); - } - - public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { - chunk.level.getChunkSource().tickingChunks.remove(chunk); -+ chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.FULL; - } - - public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { - chunk.level.getChunkSource().entityTickingChunks.add(chunk); -+ chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.ENTITY_TICKING; - } - - public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { - chunk.level.getChunkSource().entityTickingChunks.remove(chunk); -+ chunk.chunkStatus = net.minecraft.server.level.FullChunkStatus.BLOCK_TICKING; - } - - public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) { -@@ -271,23 +126,15 @@ public final class ChunkSystem { - } - - public static int getSendViewDistance(final ServerPlayer player) { -- return getLoadViewDistance(player); -+ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPISendViewDistance(player); - } - - public static int getLoadViewDistance(final ServerPlayer player) { -- final ServerLevel level = player.serverLevel(); -- if (level == null) { -- return Bukkit.getViewDistance(); -- } -- return level.chunkSource.chunkMap.getPlayerViewDistance(player); -+ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getLoadViewDistance(player); - } - - public static int getTickViewDistance(final ServerPlayer player) { -- final ServerLevel level = player.serverLevel(); -- if (level == null) { -- return Bukkit.getSimulationDistance(); -- } -- return level.chunkSource.chunkMap.distanceManager.simulationDistance; -+ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPITickViewDistance(player); - } - - private ChunkSystem() { -diff --git a/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ee58c67cb2bd78159cce19ec75f13dc6168a0e7a ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java -@@ -0,0 +1,1375 @@ -+package io.papermc.paper.chunk.system; -+ -+import ca.spottedleaf.concurrentutil.collection.SRSWLinkedQueue; -+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; -+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; -+import io.papermc.paper.chunk.system.scheduling.ChunkHolderManager; -+import io.papermc.paper.configuration.GlobalConfiguration; -+import io.papermc.paper.util.CoordinateUtils; -+import io.papermc.paper.util.TickThread; -+import io.papermc.paper.util.player.SingleUserAreaMap; -+import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; -+import it.unimi.dsi.fastutil.longs.LongArrayList; -+import it.unimi.dsi.fastutil.longs.LongComparator; -+import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue; -+import it.unimi.dsi.fastutil.longs.LongIterator; -+import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; -+import it.unimi.dsi.fastutil.longs.LongOpenHashSet; -+import net.minecraft.network.protocol.Packet; -+import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket; -+import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket; -+import net.minecraft.network.protocol.game.ClientboundSetSimulationDistancePacket; -+import net.minecraft.server.level.ChunkTrackingView; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.server.level.TicketType; -+import net.minecraft.server.network.PlayerChunkSender; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.GameRules; -+import net.minecraft.world.level.chunk.ChunkAccess; -+import net.minecraft.world.level.chunk.status.ChunkStatus; -+import net.minecraft.world.level.chunk.LevelChunk; -+import net.minecraft.world.level.levelgen.BelowZeroRetrogen; -+import org.bukkit.craftbukkit.entity.CraftPlayer; -+import org.bukkit.entity.Player; -+import java.lang.invoke.VarHandle; -+import java.util.ArrayDeque; -+import java.util.Arrays; -+import java.util.Objects; -+import java.util.concurrent.TimeUnit; -+import java.util.concurrent.atomic.AtomicLong; -+ -+public class RegionizedPlayerChunkLoader { -+ -+ // expected that this list returns for a given radius, the set of chunks ordered -+ // by manhattan distance -+ private static final long[][] SEARCH_RADIUS_ITERATION_LIST = new long[64+2+1][]; -+ static { -+ for (int i = 0; i < SEARCH_RADIUS_ITERATION_LIST.length; ++i) { -+ // a BFS around -x, -z, +x, +z will give increasing manhatten distance -+ SEARCH_RADIUS_ITERATION_LIST[i] = generateBFSOrder(i); -+ } -+ } -+ -+ private static void expandQuadrants(final CustomLongArray input, final int size) { -+ final int len = input.size(); -+ final long[] array = input.elements(); -+ -+ int writeIndex = size - 1; -+ for (int i = len - 1; i >= 0; --i) { -+ final long key = array[i]; -+ final int chunkX = CoordinateUtils.getChunkX(key); -+ final int chunkZ = CoordinateUtils.getChunkZ(key); -+ -+ if ((chunkX | chunkZ) < 0 || (i != 0 && chunkX == 0 && chunkZ == 0)) { -+ throw new IllegalStateException(); -+ } -+ -+ // Q4 -+ if (chunkZ != 0) { -+ array[writeIndex--] = CoordinateUtils.getChunkKey(chunkX, -chunkZ); -+ } -+ // Q3 -+ if (chunkX != 0 && chunkZ != 0) { -+ array[writeIndex--] = CoordinateUtils.getChunkKey(-chunkX, -chunkZ); -+ } -+ // Q2 -+ if (chunkX != 0) { -+ array[writeIndex--] = CoordinateUtils.getChunkKey(-chunkX, chunkZ); -+ } -+ -+ array[writeIndex--] = key; -+ } -+ -+ input.forceSize(size); -+ -+ if (writeIndex != -1) { -+ throw new IllegalStateException(); -+ } -+ } -+ -+ private static long[] generateBFSOrder(final int radius) { -+ // by using only the first quadrant, we can reduce the total element size by 4 when spreading -+ final CustomLongArray[] byDistance = makeQ1BFS(radius); -+ -+ // to increase generation parallelism, we want to space the chunks out so that they are not nearby when generating -+ // this also means we are minimising locality -+ // but, we need to maintain sorted order by manhatten distance -+ -+ // per manhatten distance we transform the chunk list so that each element is maximally spaced out from each other -+ for (int i = 0, len = byDistance.length; i < len; ++i) { -+ final CustomLongArray points = byDistance[i]; -+ final int expectedSize = getDistanceSize(i, radius); -+ -+ final CustomLongArray spread = spread(points, expectedSize); -+ // add in Q2, Q3, Q4 -+ expandQuadrants(spread, expectedSize); -+ -+ byDistance[i] = spread; -+ } -+ -+ // now, rebuild the list so that it still maintains manhatten distance order -+ final CustomLongArray ret = new CustomLongArray((2 * radius + 1) * (2 * radius + 1)); -+ -+ for (final CustomLongArray dist : byDistance) { -+ ret.addAll(dist); -+ } -+ -+ return ret.elements(); -+ } -+ -+ public static final TicketType REGION_PLAYER_TICKET = TicketType.create("region_player_ticket", Long::compareTo); -+ -+ public static final int MIN_VIEW_DISTANCE = 2; -+ public static final int MAX_VIEW_DISTANCE = 32; -+ -+ public static final int TICK_TICKET_LEVEL = 31; -+ public static final int GENERATED_TICKET_LEVEL = 33 + ChunkStatus.getDistance(ChunkStatus.FULL); -+ public static final int LOADED_TICKET_LEVEL = 33 + ChunkStatus.getDistance(ChunkStatus.EMPTY); -+ -+ public static final record ViewDistances( -+ int tickViewDistance, -+ int loadViewDistance, -+ int sendViewDistance -+ ) { -+ public ViewDistances setTickViewDistance(final int distance) { -+ return new ViewDistances(distance, this.loadViewDistance, this.sendViewDistance); -+ } -+ -+ public ViewDistances setLoadViewDistance(final int distance) { -+ return new ViewDistances(this.tickViewDistance, distance, this.sendViewDistance); -+ } -+ -+ -+ public ViewDistances setSendViewDistance(final int distance) { -+ return new ViewDistances(this.tickViewDistance, this.loadViewDistance, distance); -+ } -+ } -+ -+ public static int getAPITickViewDistance(final Player player) { -+ return getAPITickViewDistance(((CraftPlayer)player).getHandle()); -+ } -+ -+ public static int getAPITickViewDistance(final ServerPlayer player) { -+ final ServerLevel level = (ServerLevel)player.level(); -+ final PlayerChunkLoaderData data = player.chunkLoader; -+ if (data == null) { -+ return level.playerChunkLoader.getAPITickDistance(); -+ } -+ return data.lastTickDistance; -+ } -+ -+ public static int getAPIViewDistance(final Player player) { -+ return getAPIViewDistance(((CraftPlayer)player).getHandle()); -+ } -+ -+ public static int getAPIViewDistance(final ServerPlayer player) { -+ final ServerLevel level = (ServerLevel)player.level(); -+ final PlayerChunkLoaderData data = player.chunkLoader; -+ if (data == null) { -+ return level.playerChunkLoader.getAPIViewDistance(); -+ } -+ // view distance = load distance + 1 -+ return data.lastLoadDistance - 1; -+ } -+ -+ public static int getLoadViewDistance(final ServerPlayer player) { -+ final ServerLevel level = (ServerLevel)player.level(); -+ final PlayerChunkLoaderData data = player.chunkLoader; -+ if (data == null) { -+ return level.playerChunkLoader.getAPIViewDistance(); -+ } -+ // view distance = load distance + 1 -+ return data.lastLoadDistance - 1; -+ } -+ -+ public static int getAPISendViewDistance(final Player player) { -+ return getAPISendViewDistance(((CraftPlayer)player).getHandle()); -+ } -+ -+ public static int getAPISendViewDistance(final ServerPlayer player) { -+ final ServerLevel level = (ServerLevel)player.level(); -+ final PlayerChunkLoaderData data = player.chunkLoader; -+ if (data == null) { -+ return level.playerChunkLoader.getAPISendViewDistance(); -+ } -+ return data.lastSendDistance; -+ } -+ -+ private final ServerLevel world; -+ -+ public RegionizedPlayerChunkLoader(final ServerLevel world) { -+ this.world = world; -+ } -+ -+ public void addPlayer(final ServerPlayer player) { -+ TickThread.ensureTickThread(player, "Cannot add player to player chunk loader async"); -+ if (!player.isRealPlayer) { -+ return; -+ } -+ -+ if (player.chunkLoader != null) { -+ throw new IllegalStateException("Player is already added to player chunk loader"); -+ } -+ -+ final PlayerChunkLoaderData loader = new PlayerChunkLoaderData(this.world, player); -+ -+ player.chunkLoader = loader; -+ loader.add(); -+ } -+ -+ public void updatePlayer(final ServerPlayer player) { -+ final PlayerChunkLoaderData loader = player.chunkLoader; -+ if (loader != null) { -+ loader.update(); -+ } -+ } -+ -+ public void removePlayer(final ServerPlayer player) { -+ TickThread.ensureTickThread(player, "Cannot remove player from player chunk loader async"); -+ if (!player.isRealPlayer) { -+ return; -+ } -+ -+ final PlayerChunkLoaderData loader = player.chunkLoader; -+ -+ if (loader == null) { -+ return; -+ } -+ -+ loader.remove(); -+ player.chunkLoader = null; -+ } -+ -+ public void setSendDistance(final int distance) { -+ this.world.setSendViewDistance(distance); -+ } -+ -+ public void setLoadDistance(final int distance) { -+ this.world.setLoadViewDistance(distance); -+ } -+ -+ public void setTickDistance(final int distance) { -+ this.world.setTickViewDistance(distance); -+ } -+ -+ // Note: follow the player chunk loader so everything stays consistent... -+ public int getAPITickDistance() { -+ final ViewDistances distances = this.world.getViewDistances(); -+ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(-1, distances.tickViewDistance); -+ return tickViewDistance; -+ } -+ -+ public int getAPIViewDistance() { -+ final ViewDistances distances = this.world.getViewDistances(); -+ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(-1, distances.tickViewDistance); -+ final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance); -+ -+ // loadDistance = api view distance + 1 -+ return loadDistance - 1; -+ } -+ -+ public int getAPISendViewDistance() { -+ final ViewDistances distances = this.world.getViewDistances(); -+ final int tickViewDistance = PlayerChunkLoaderData.getTickDistance(-1, distances.tickViewDistance); -+ final int loadDistance = PlayerChunkLoaderData.getLoadViewDistance(tickViewDistance, -1, distances.loadViewDistance); -+ final int sendViewDistance = PlayerChunkLoaderData.getSendViewDistance( -+ loadDistance, -1, -1, distances.sendViewDistance -+ ); -+ -+ return sendViewDistance; -+ } -+ -+ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ, final boolean borderOnly) { -+ return borderOnly ? this.isChunkSentBorderOnly(player, chunkX, chunkZ) : this.isChunkSent(player, chunkX, chunkZ); -+ } -+ -+ public boolean isChunkSent(final ServerPlayer player, final int chunkX, final int chunkZ) { -+ final PlayerChunkLoaderData loader = player.chunkLoader; -+ if (loader == null) { -+ return false; -+ } -+ -+ return loader.sentChunks.contains(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ } -+ -+ public boolean isChunkSentBorderOnly(final ServerPlayer player, final int chunkX, final int chunkZ) { -+ final PlayerChunkLoaderData loader = player.chunkLoader; -+ if (loader == null) { -+ return false; -+ } -+ -+ for (int dz = -1; dz <= 1; ++dz) { -+ for (int dx = -1; dx <= 1; ++dx) { -+ if (!loader.sentChunks.contains(CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ))) { -+ return true; -+ } -+ } -+ } -+ -+ return false; -+ } -+ -+ public void tick() { -+ TickThread.ensureTickThread("Cannot tick player chunk loader async"); -+ long currTime = System.nanoTime(); -+ for (final ServerPlayer player : new java.util.ArrayList<>(this.world.players())) { -+ final PlayerChunkLoaderData loader = player.chunkLoader; -+ if (loader == null || loader.world != this.world) { -+ // not our problem anymore -+ continue; -+ } -+ loader.update(); // can't invoke plugin logic -+ loader.updateQueues(currTime); -+ } -+ } -+ -+ public static final class PlayerChunkLoaderData { -+ -+ private static final AtomicLong ID_GENERATOR = new AtomicLong(); -+ private final long id = ID_GENERATOR.incrementAndGet(); -+ private final Long idBoxed = Long.valueOf(this.id); -+ -+ private static final long MAX_RATE = 10_000L; -+ -+ private final ServerPlayer player; -+ private final ServerLevel world; -+ -+ private int lastChunkX = Integer.MIN_VALUE; -+ private int lastChunkZ = Integer.MIN_VALUE; -+ -+ private int lastSendDistance = Integer.MIN_VALUE; -+ private int lastLoadDistance = Integer.MIN_VALUE; -+ private int lastTickDistance = Integer.MIN_VALUE; -+ -+ private int lastSentChunkCenterX = Integer.MIN_VALUE; -+ private int lastSentChunkCenterZ = Integer.MIN_VALUE; -+ -+ private int lastSentChunkRadius = Integer.MIN_VALUE; -+ private int lastSentSimulationDistance = Integer.MIN_VALUE; -+ -+ private boolean canGenerateChunks = true; -+ -+ private final ArrayDeque> delayedTicketOps = new ArrayDeque<>(); -+ private final LongOpenHashSet sentChunks = new LongOpenHashSet(); -+ -+ private static final byte CHUNK_TICKET_STAGE_NONE = 0; -+ private static final byte CHUNK_TICKET_STAGE_LOADING = 1; -+ private static final byte CHUNK_TICKET_STAGE_LOADED = 2; -+ private static final byte CHUNK_TICKET_STAGE_GENERATING = 3; -+ private static final byte CHUNK_TICKET_STAGE_GENERATED = 4; -+ private static final byte CHUNK_TICKET_STAGE_TICK = 5; -+ private static final int[] TICKET_STAGE_TO_LEVEL = new int[] { -+ ChunkHolderManager.MAX_TICKET_LEVEL + 1, -+ LOADED_TICKET_LEVEL, -+ LOADED_TICKET_LEVEL, -+ GENERATED_TICKET_LEVEL, -+ GENERATED_TICKET_LEVEL, -+ TICK_TICKET_LEVEL -+ }; -+ private final Long2ByteOpenHashMap chunkTicketStage = new Long2ByteOpenHashMap(); -+ { -+ this.chunkTicketStage.defaultReturnValue(CHUNK_TICKET_STAGE_NONE); -+ } -+ -+ // rate limiting -+ private final AllocatingRateLimiter chunkSendLimiter = new AllocatingRateLimiter(); -+ private final AllocatingRateLimiter chunkLoadTicketLimiter = new AllocatingRateLimiter(); -+ private final AllocatingRateLimiter chunkGenerateTicketLimiter = new AllocatingRateLimiter(); -+ -+ // queues -+ private final LongComparator CLOSEST_MANHATTAN_DIST = (final long c1, final long c2) -> { -+ final int c1x = CoordinateUtils.getChunkX(c1); -+ final int c1z = CoordinateUtils.getChunkZ(c1); -+ -+ final int c2x = CoordinateUtils.getChunkX(c2); -+ final int c2z = CoordinateUtils.getChunkZ(c2); -+ -+ final int centerX = PlayerChunkLoaderData.this.lastChunkX; -+ final int centerZ = PlayerChunkLoaderData.this.lastChunkZ; -+ -+ return Integer.compare( -+ Math.abs(c1x - centerX) + Math.abs(c1z - centerZ), -+ Math.abs(c2x - centerX) + Math.abs(c2z - centerZ) -+ ); -+ }; -+ private final LongHeapPriorityQueue sendQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); -+ private final LongHeapPriorityQueue tickingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); -+ private final LongHeapPriorityQueue generatingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); -+ private final LongHeapPriorityQueue genQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); -+ private final LongHeapPriorityQueue loadingQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); -+ private final LongHeapPriorityQueue loadQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); -+ -+ private volatile boolean removed; -+ -+ public PlayerChunkLoaderData(final ServerLevel world, final ServerPlayer player) { -+ this.world = world; -+ this.player = player; -+ } -+ -+ private void flushDelayedTicketOps() { -+ if (this.delayedTicketOps.isEmpty()) { -+ return; -+ } -+ this.world.chunkTaskScheduler.chunkHolderManager.performTicketUpdates(this.delayedTicketOps); -+ this.delayedTicketOps.clear(); -+ } -+ -+ private void pushDelayedTicketOp(final ChunkHolderManager.TicketOperation op) { -+ this.delayedTicketOps.addLast(op); -+ } -+ -+ private void sendChunk(final int chunkX, final int chunkZ) { -+ if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { -+ PlayerChunkSender.sendChunk(this.player.connection, this.world, this.world.getChunkIfLoaded(chunkX, chunkZ)); -+ return; -+ } -+ throw new IllegalStateException(); -+ } -+ -+ private void sendUnloadChunk(final int chunkX, final int chunkZ) { -+ if (!this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { -+ return; -+ } -+ this.sendUnloadChunkRaw(chunkX, chunkZ); -+ } -+ -+ private void sendUnloadChunkRaw(final int chunkX, final int chunkZ) { -+ PlayerChunkSender.dropChunkStatic(this.player, new ChunkPos(chunkX, chunkZ)); -+ } -+ -+ private final SingleUserAreaMap broadcastMap = new SingleUserAreaMap<>(this) { -+ @Override -+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { -+ // do nothing, we only care about remove -+ } -+ -+ @Override -+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { -+ parameter.sendUnloadChunk(chunkX, chunkZ); -+ } -+ }; -+ private final SingleUserAreaMap loadTicketCleanup = new SingleUserAreaMap<>(this) { -+ @Override -+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { -+ // do nothing, we only care about remove -+ } -+ -+ @Override -+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { -+ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ final byte ticketStage = parameter.chunkTicketStage.remove(chunk); -+ final int level = TICKET_STAGE_TO_LEVEL[ticketStage]; -+ if (level > ChunkHolderManager.MAX_TICKET_LEVEL) { -+ return; -+ } -+ -+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove( -+ chunk, -+ TicketType.UNKNOWN, level, new ChunkPos(chunkX, chunkZ), -+ REGION_PLAYER_TICKET, level, parameter.idBoxed -+ )); -+ } -+ }; -+ private final SingleUserAreaMap tickMap = new SingleUserAreaMap<>(this) { -+ @Override -+ protected void addCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { -+ // do nothing, we will detect ticking chunks when we try to load them -+ } -+ -+ @Override -+ protected void removeCallback(final PlayerChunkLoaderData parameter, final int chunkX, final int chunkZ) { -+ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ // note: by the time this is called, the tick cleanup should have ran - so, if the chunk is at -+ // the tick stage it was deemed in range for loading. Thus, we need to move it to generated -+ if (!parameter.chunkTicketStage.replace(chunk, CHUNK_TICKET_STAGE_TICK, CHUNK_TICKET_STAGE_GENERATED)) { -+ return; -+ } -+ -+ // Since we are possibly downgrading the ticket level, we add an unknown ticket so that -+ // the level is kept until tick(). -+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addAndRemove( -+ chunk, -+ TicketType.UNKNOWN, TICK_TICKET_LEVEL, new ChunkPos(chunkX, chunkZ), -+ REGION_PLAYER_TICKET, TICK_TICKET_LEVEL, parameter.idBoxed -+ )); -+ // keep chunk at new generated level -+ parameter.pushDelayedTicketOp(ChunkHolderManager.TicketOperation.addOp( -+ chunk, -+ REGION_PLAYER_TICKET, GENERATED_TICKET_LEVEL, parameter.idBoxed -+ )); -+ } -+ }; -+ -+ private static boolean wantChunkLoaded(final int centerX, final int centerZ, final int chunkX, final int chunkZ, -+ final int sendRadius) { -+ // expect sendRadius to be = 1 + target viewable radius -+ return ChunkTrackingView.isWithinDistance(centerX, centerZ, sendRadius, chunkX, chunkZ, true); -+ } -+ -+ private static int getClientViewDistance(final ServerPlayer player) { -+ final Integer vd = player.requestedViewDistance(); -+ return vd == null ? -1 : Math.max(0, vd.intValue()); -+ } -+ -+ private static int getTickDistance(final int playerTickViewDistance, final int worldTickViewDistance) { -+ return playerTickViewDistance < 0 ? worldTickViewDistance : playerTickViewDistance; -+ } -+ -+ private static int getLoadViewDistance(final int tickViewDistance, final int playerLoadViewDistance, -+ final int worldLoadViewDistance) { -+ return Math.max(tickViewDistance + 1, playerLoadViewDistance < 0 ? worldLoadViewDistance : playerLoadViewDistance); -+ } -+ -+ private static int getSendViewDistance(final int loadViewDistance, final int clientViewDistance, -+ final int playerSendViewDistance, final int worldSendViewDistance) { -+ return Math.min( -+ loadViewDistance - 1, -+ playerSendViewDistance < 0 ? (!GlobalConfiguration.get().chunkLoadingAdvanced.autoConfigSendDistance || clientViewDistance < 0 ? (worldSendViewDistance < 0 ? (loadViewDistance - 1) : worldSendViewDistance) : clientViewDistance + 1) : playerSendViewDistance -+ ); -+ } -+ -+ private Packet updateClientChunkRadius(final int radius) { -+ this.lastSentChunkRadius = radius; -+ return new ClientboundSetChunkCacheRadiusPacket(radius); -+ } -+ -+ private Packet updateClientSimulationDistance(final int distance) { -+ this.lastSentSimulationDistance = distance; -+ return new ClientboundSetSimulationDistancePacket(distance); -+ } -+ -+ private Packet updateClientChunkCenter(final int chunkX, final int chunkZ) { -+ this.lastSentChunkCenterX = chunkX; -+ this.lastSentChunkCenterZ = chunkZ; -+ return new ClientboundSetChunkCacheCenterPacket(chunkX, chunkZ); -+ } -+ -+ private boolean canPlayerGenerateChunks() { -+ return !this.player.isSpectator() || this.world.getGameRules().getBoolean(GameRules.RULE_SPECTATORSGENERATECHUNKS); -+ } -+ -+ private double getMaxChunkLoadRate() { -+ final double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkLoadRate; -+ -+ return configRate < 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); -+ } -+ -+ private double getMaxChunkGenRate() { -+ final double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkGenerateRate; -+ -+ return configRate < 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); -+ } -+ -+ private double getMaxChunkSendRate() { -+ final double configRate = GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkSendRate; -+ -+ return configRate < 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); -+ } -+ -+ private long getMaxChunkLoads() { -+ final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L); -+ long configLimit = GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkLoads; -+ if (configLimit == 0L) { -+ // by default, only allow 1/5th of the chunks in the view distance to be concurrently active -+ configLimit = Math.max(5L, radiusChunks / 5L); -+ } else if (configLimit < 0L) { -+ configLimit = Integer.MAX_VALUE; -+ } // else: use the value configured -+ configLimit = configLimit - this.loadingQueue.size(); -+ -+ return configLimit; -+ } -+ -+ private long getMaxChunkGenerates() { -+ final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L); -+ long configLimit = GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkGenerates; -+ if (configLimit == 0L) { -+ // by default, only allow 1/5th of the chunks in the view distance to be concurrently active -+ configLimit = Math.max(5L, radiusChunks / 5L); -+ } else if (configLimit < 0L) { -+ configLimit = Integer.MAX_VALUE; -+ } // else: use the value configured -+ configLimit = configLimit - this.generatingQueue.size(); -+ -+ return configLimit; -+ } -+ -+ private boolean wantChunkSent(final int chunkX, final int chunkZ) { -+ final int dx = this.lastChunkX - chunkX; -+ final int dz = this.lastChunkZ - chunkZ; -+ return (Math.max(Math.abs(dx), Math.abs(dz)) <= (this.lastSendDistance + 1)) && wantChunkLoaded( -+ this.lastChunkX, this.lastChunkZ, chunkX, chunkZ, this.lastSendDistance -+ ); -+ } -+ -+ private boolean wantChunkTicked(final int chunkX, final int chunkZ) { -+ final int dx = this.lastChunkX - chunkX; -+ final int dz = this.lastChunkZ - chunkZ; -+ return Math.max(Math.abs(dx), Math.abs(dz)) <= this.lastTickDistance; -+ } -+ -+ void updateQueues(final long time) { -+ TickThread.ensureTickThread(this.player, "Cannot tick player chunk loader async"); -+ if (this.removed) { -+ throw new IllegalStateException("Ticking removed player chunk loader"); -+ } -+ // update rate limits -+ final double loadRate = this.getMaxChunkLoadRate(); -+ final double genRate = this.getMaxChunkGenRate(); -+ final double sendRate = this.getMaxChunkSendRate(); -+ -+ this.chunkLoadTicketLimiter.tickAllocation(time, loadRate, loadRate); -+ this.chunkGenerateTicketLimiter.tickAllocation(time, genRate, genRate); -+ this.chunkSendLimiter.tickAllocation(time, sendRate, sendRate); -+ -+ // try to progress chunk loads -+ while (!this.loadingQueue.isEmpty()) { -+ final long pendingLoadChunk = this.loadingQueue.firstLong(); -+ final int pendingChunkX = CoordinateUtils.getChunkX(pendingLoadChunk); -+ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingLoadChunk); -+ final ChunkAccess pending = this.world.chunkSource.getChunkAtImmediately(pendingChunkX, pendingChunkZ); -+ if (pending == null) { -+ // nothing to do here -+ break; -+ } -+ // chunk has loaded, so we can take it out of the queue -+ this.loadingQueue.dequeueLong(); -+ -+ // try to move to generate queue -+ final byte prev = this.chunkTicketStage.put(pendingLoadChunk, CHUNK_TICKET_STAGE_LOADED); -+ if (prev != CHUNK_TICKET_STAGE_LOADING) { -+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADING + ", not " + prev); -+ } -+ -+ if (this.canGenerateChunks || this.isLoadedChunkGeneratable(pending)) { -+ this.genQueue.enqueue(pendingLoadChunk); -+ } // else: don't want to generate, so just leave it loaded -+ } -+ -+ // try to push more chunk loads -+ final long maxLoads = Math.max(0L, Math.min(MAX_RATE, Math.min(this.loadQueue.size(), this.getMaxChunkLoads()))); -+ final int maxLoadsThisTick = (int)this.chunkLoadTicketLimiter.takeAllocation(time, loadRate, maxLoads); -+ if (maxLoadsThisTick > 0) { -+ final LongArrayList chunks = new LongArrayList(maxLoadsThisTick); -+ for (int i = 0; i < maxLoadsThisTick; ++i) { -+ final long chunk = this.loadQueue.dequeueLong(); -+ final byte prev = this.chunkTicketStage.put(chunk, CHUNK_TICKET_STAGE_LOADING); -+ if (prev != CHUNK_TICKET_STAGE_NONE) { -+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_NONE + ", not " + prev); -+ } -+ this.pushDelayedTicketOp( -+ ChunkHolderManager.TicketOperation.addOp( -+ chunk, -+ REGION_PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed -+ ) -+ ); -+ chunks.add(chunk); -+ this.loadingQueue.enqueue(chunk); -+ } -+ -+ // here we need to flush tickets, as scheduleChunkLoad requires tickets to be propagated with addTicket = false -+ this.flushDelayedTicketOps(); -+ // we only need to call scheduleChunkLoad because the loaded ticket level is not enough to start the chunk -+ // load - only generate ticket levels start anything, but they start generation... -+ // propagate levels -+ // Note: this CAN call plugin logic, so it is VITAL that our bookkeeping logic is completely done by the time this is invoked -+ this.world.chunkTaskScheduler.chunkHolderManager.processTicketUpdates(); -+ -+ if (this.removed) { -+ // process ticket updates may invoke plugin logic, which may remove this player -+ return; -+ } -+ -+ for (int i = 0; i < maxLoadsThisTick; ++i) { -+ final long queuedLoadChunk = chunks.getLong(i); -+ final int queuedChunkX = CoordinateUtils.getChunkX(queuedLoadChunk); -+ final int queuedChunkZ = CoordinateUtils.getChunkZ(queuedLoadChunk); -+ this.world.chunkTaskScheduler.scheduleChunkLoad( -+ queuedChunkX, queuedChunkZ, ChunkStatus.EMPTY, false, PrioritisedExecutor.Priority.NORMAL, null -+ ); -+ if (this.removed) { -+ return; -+ } -+ } -+ } -+ -+ // try to progress chunk generations -+ while (!this.generatingQueue.isEmpty()) { -+ final long pendingGenChunk = this.generatingQueue.firstLong(); -+ final int pendingChunkX = CoordinateUtils.getChunkX(pendingGenChunk); -+ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingGenChunk); -+ final LevelChunk pending = this.world.chunkSource.getChunkAtIfLoadedMainThreadNoCache(pendingChunkX, pendingChunkZ); -+ if (pending == null) { -+ // nothing to do here -+ break; -+ } -+ -+ // chunk has generated, so we can take it out of queue -+ this.generatingQueue.dequeueLong(); -+ -+ final byte prev = this.chunkTicketStage.put(pendingGenChunk, CHUNK_TICKET_STAGE_GENERATED); -+ if (prev != CHUNK_TICKET_STAGE_GENERATING) { -+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATING + ", not " + prev); -+ } -+ -+ // try to move to send queue -+ if (this.wantChunkSent(pendingChunkX, pendingChunkZ)) { -+ this.sendQueue.enqueue(pendingGenChunk); -+ } -+ // try to move to tick queue -+ if (this.wantChunkTicked(pendingChunkX, pendingChunkZ)) { -+ this.tickingQueue.enqueue(pendingGenChunk); -+ } -+ } -+ -+ // try to push more chunk generations -+ final long maxGens = Math.max(0L, Math.min(MAX_RATE, Math.min(this.genQueue.size(), this.getMaxChunkGenerates()))); -+ final int maxGensThisTick = (int)this.chunkGenerateTicketLimiter.takeAllocation(time, genRate, maxGens); -+ int ratedGensThisTick = 0; -+ while (!this.genQueue.isEmpty()) { -+ final long chunkKey = this.genQueue.firstLong(); -+ final int chunkX = CoordinateUtils.getChunkX(chunkKey); -+ final int chunkZ = CoordinateUtils.getChunkZ(chunkKey); -+ final ChunkAccess chunk = this.world.chunkSource.getChunkAtImmediately(chunkX, chunkZ); -+ if (chunk.getStatus() != ChunkStatus.FULL) { -+ // only rate limit actual generations -+ if ((ratedGensThisTick + 1) > maxGensThisTick) { -+ break; -+ } -+ ++ratedGensThisTick; -+ } -+ -+ this.genQueue.dequeueLong(); -+ -+ final byte prev = this.chunkTicketStage.put(chunkKey, CHUNK_TICKET_STAGE_GENERATING); -+ if (prev != CHUNK_TICKET_STAGE_LOADED) { -+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_LOADED + ", not " + prev); -+ } -+ this.pushDelayedTicketOp( -+ ChunkHolderManager.TicketOperation.addAndRemove( -+ chunkKey, -+ REGION_PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed, -+ REGION_PLAYER_TICKET, LOADED_TICKET_LEVEL, this.idBoxed -+ ) -+ ); -+ this.generatingQueue.enqueue(chunkKey); -+ } -+ -+ // try to pull ticking chunks -+ tick_check_outer: -+ while (!this.tickingQueue.isEmpty()) { -+ final long pendingTicking = this.tickingQueue.firstLong(); -+ final int pendingChunkX = CoordinateUtils.getChunkX(pendingTicking); -+ final int pendingChunkZ = CoordinateUtils.getChunkZ(pendingTicking); -+ -+ final int tickingReq = 2; -+ for (int dz = -tickingReq; dz <= tickingReq; ++dz) { -+ for (int dx = -tickingReq; dx <= tickingReq; ++dx) { -+ if ((dx | dz) == 0) { -+ continue; -+ } -+ final long neighbour = CoordinateUtils.getChunkKey(dx + pendingChunkX, dz + pendingChunkZ); -+ final byte stage = this.chunkTicketStage.get(neighbour); -+ if (stage != CHUNK_TICKET_STAGE_GENERATED && stage != CHUNK_TICKET_STAGE_TICK) { -+ break tick_check_outer; -+ } -+ } -+ } -+ // only gets here if all neighbours were marked as generated or ticking themselves -+ this.tickingQueue.dequeueLong(); -+ this.pushDelayedTicketOp( -+ ChunkHolderManager.TicketOperation.addAndRemove( -+ pendingTicking, -+ REGION_PLAYER_TICKET, TICK_TICKET_LEVEL, this.idBoxed, -+ REGION_PLAYER_TICKET, GENERATED_TICKET_LEVEL, this.idBoxed -+ ) -+ ); -+ // there is no queue to add after ticking -+ final byte prev = this.chunkTicketStage.put(pendingTicking, CHUNK_TICKET_STAGE_TICK); -+ if (prev != CHUNK_TICKET_STAGE_GENERATED) { -+ throw new IllegalStateException("Previous state should be " + CHUNK_TICKET_STAGE_GENERATED + ", not " + prev); -+ } -+ } -+ -+ // try to pull sending chunks -+ final long maxSends = Math.max(0L, Math.min(MAX_RATE, Integer.MAX_VALUE)); // no logic to track concurrent sends -+ final int maxSendsThisTick = Math.min((int)this.chunkSendLimiter.takeAllocation(time, sendRate, maxSends), this.sendQueue.size()); -+ // we do not return sends that we took from the allocation back because we want to limit the max send rate, not target it -+ for (int i = 0; i < maxSendsThisTick; ++i) { -+ final long pendingSend = this.sendQueue.firstLong(); -+ final int pendingSendX = CoordinateUtils.getChunkX(pendingSend); -+ final int pendingSendZ = CoordinateUtils.getChunkZ(pendingSend); -+ final LevelChunk chunk = this.world.chunkSource.getChunkAtIfLoadedMainThreadNoCache(pendingSendX, pendingSendZ); -+ if (!chunk.areNeighboursLoaded(1) || !TickThread.isTickThreadFor(this.world, pendingSendX, pendingSendZ)) { -+ // nothing to do -+ // the target chunk may not be owned by this region, but this should be resolved in the future -+ break; -+ } -+ if (!chunk.isPostProcessingDone) { -+ // not yet post-processed, need to do this so that tile entities can properly be sent to clients -+ chunk.postProcessGeneration(); -+ // check if there was any recursive action -+ if (this.removed || this.sendQueue.isEmpty() || this.sendQueue.firstLong() != pendingSend) { -+ return; -+ } // else: good to dequeue and send, fall through -+ } -+ this.sendQueue.dequeueLong(); -+ -+ this.sendChunk(pendingSendX, pendingSendZ); -+ if (this.removed) { -+ // sendChunk may invoke plugin logic -+ return; -+ } -+ } -+ -+ this.flushDelayedTicketOps(); -+ // we assume propagate ticket levels happens after this call -+ } -+ -+ void add() { -+ TickThread.ensureTickThread(this.player, "Cannot add player asynchronously"); -+ if (this.removed) { -+ throw new IllegalStateException("Adding removed player chunk loader"); -+ } -+ final ViewDistances playerDistances = this.player.getViewDistances(); -+ final ViewDistances worldDistances = this.world.getViewDistances(); -+ final int chunkX = this.player.chunkPosition().x; -+ final int chunkZ = this.player.chunkPosition().z; -+ -+ final int tickViewDistance = getTickDistance(playerDistances.tickViewDistance, worldDistances.tickViewDistance); -+ // load view cannot be less-than tick view + 1 -+ final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance); -+ // send view cannot be greater-than load view -+ final int clientViewDistance = getClientViewDistance(this.player); -+ final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance); -+ -+ // send view distances -+ this.player.connection.send(this.updateClientChunkRadius(sendViewDistance)); -+ this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance)); -+ -+ // add to distance maps -+ this.broadcastMap.add(chunkX, chunkZ, sendViewDistance + 1); -+ this.loadTicketCleanup.add(chunkX, chunkZ, loadViewDistance + 1); -+ this.tickMap.add(chunkX, chunkZ, tickViewDistance); -+ -+ // update chunk center -+ this.player.connection.send(this.updateClientChunkCenter(chunkX, chunkZ)); -+ -+ // now we can update -+ this.update(); -+ } -+ -+ private boolean isLoadedChunkGeneratable(final int chunkX, final int chunkZ) { -+ return this.isLoadedChunkGeneratable(this.world.chunkSource.getChunkAtImmediately(chunkX, chunkZ)); -+ } -+ -+ private boolean isLoadedChunkGeneratable(final ChunkAccess chunkAccess) { -+ final BelowZeroRetrogen belowZeroRetrogen; -+ // see PortalForcer#findPortalAround -+ return chunkAccess != null && ( -+ chunkAccess.getStatus() == ChunkStatus.FULL || -+ ((belowZeroRetrogen = chunkAccess.getBelowZeroRetrogen()) != null && belowZeroRetrogen.targetStatus().isOrAfter(ChunkStatus.SPAWN)) -+ ); -+ } -+ -+ void update() { -+ TickThread.ensureTickThread(this.player, "Cannot update player asynchronously"); -+ if (this.removed) { -+ throw new IllegalStateException("Updating removed player chunk loader"); -+ } -+ final ViewDistances playerDistances = this.player.getViewDistances(); -+ final ViewDistances worldDistances = this.world.getViewDistances(); -+ -+ final int tickViewDistance = getTickDistance(playerDistances.tickViewDistance, worldDistances.tickViewDistance); -+ // load view cannot be less-than tick view + 1 -+ final int loadViewDistance = getLoadViewDistance(tickViewDistance, playerDistances.loadViewDistance, worldDistances.loadViewDistance); -+ // send view cannot be greater-than load view -+ final int clientViewDistance = getClientViewDistance(this.player); -+ final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance); -+ -+ final ChunkPos playerPos = this.player.chunkPosition(); -+ final boolean canGenerateChunks = this.canPlayerGenerateChunks(); -+ final int currentChunkX = playerPos.x; -+ final int currentChunkZ = playerPos.z; -+ -+ final int prevChunkX = this.lastChunkX; -+ final int prevChunkZ = this.lastChunkZ; -+ -+ if ( -+ // has view distance stayed the same? -+ sendViewDistance == this.lastSendDistance -+ && loadViewDistance == this.lastLoadDistance -+ && tickViewDistance == this.lastTickDistance -+ -+ // has our chunk stayed the same? -+ && prevChunkX == currentChunkX -+ && prevChunkZ == currentChunkZ -+ -+ // can we still generate chunks? -+ && this.canGenerateChunks == canGenerateChunks -+ ) { -+ // nothing we care about changed, so we're not re-calculating -+ return; -+ } -+ -+ // update distance maps -+ this.broadcastMap.update(currentChunkX, currentChunkZ, sendViewDistance + 1); -+ this.loadTicketCleanup.update(currentChunkX, currentChunkZ, loadViewDistance + 1); -+ this.tickMap.update(currentChunkX, currentChunkZ, tickViewDistance); -+ if (sendViewDistance > loadViewDistance || tickViewDistance > loadViewDistance) { -+ throw new IllegalStateException(); -+ } -+ -+ // update VDs for client -+ // this should be after the distance map updates, as they will send unload packets -+ if (this.lastSentChunkRadius != sendViewDistance) { -+ this.player.connection.send(this.updateClientChunkRadius(sendViewDistance)); -+ } -+ if (this.lastSentSimulationDistance != tickViewDistance) { -+ this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance)); -+ } -+ -+ this.sendQueue.clear(); -+ this.tickingQueue.clear(); -+ this.generatingQueue.clear(); -+ this.genQueue.clear(); -+ this.loadingQueue.clear(); -+ this.loadQueue.clear(); -+ -+ this.lastChunkX = currentChunkX; -+ this.lastChunkZ = currentChunkZ; -+ this.lastSendDistance = sendViewDistance; -+ this.lastLoadDistance = loadViewDistance; -+ this.lastTickDistance = tickViewDistance; -+ this.canGenerateChunks = canGenerateChunks; -+ -+ // +1 since we need to load chunks +1 around the load view distance... -+ final long[] toIterate = SEARCH_RADIUS_ITERATION_LIST[loadViewDistance + 1]; -+ // the iteration order is by increasing manhattan distance - so, we do NOT need to -+ // sort anything in the queue! -+ for (final long deltaChunk : toIterate) { -+ final int dx = CoordinateUtils.getChunkX(deltaChunk); -+ final int dz = CoordinateUtils.getChunkZ(deltaChunk); -+ final int chunkX = dx + currentChunkX; -+ final int chunkZ = dz + currentChunkZ; -+ final long chunk = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz)); -+ final int manhattanDistance = Math.abs(dx) + Math.abs(dz); -+ -+ // since chunk sending is not by radius alone, we need an extra check here to account for -+ // everything <= sendDistance -+ // Note: Vanilla may want to send chunks outside the send view distance, so we do need -+ // the dist <= view check -+ final boolean sendChunk = (squareDistance <= (sendViewDistance + 1)) -+ && wantChunkLoaded(currentChunkX, currentChunkZ, chunkX, chunkZ, sendViewDistance); -+ final boolean sentChunk = sendChunk ? this.sentChunks.contains(chunk) : this.sentChunks.remove(chunk); -+ -+ if (!sendChunk && sentChunk) { -+ // have sent the chunk, but don't want it anymore -+ // unload it now -+ this.sendUnloadChunkRaw(chunkX, chunkZ); -+ } -+ -+ final byte stage = this.chunkTicketStage.get(chunk); -+ switch (stage) { -+ case CHUNK_TICKET_STAGE_NONE: { -+ // we want the chunk to be at least loaded -+ this.loadQueue.enqueue(chunk); -+ break; -+ } -+ case CHUNK_TICKET_STAGE_LOADING: { -+ this.loadingQueue.enqueue(chunk); -+ break; -+ } -+ case CHUNK_TICKET_STAGE_LOADED: { -+ if (canGenerateChunks || this.isLoadedChunkGeneratable(chunkX, chunkZ)) { -+ this.genQueue.enqueue(chunk); -+ } -+ break; -+ } -+ case CHUNK_TICKET_STAGE_GENERATING: { -+ this.generatingQueue.enqueue(chunk); -+ break; -+ } -+ case CHUNK_TICKET_STAGE_GENERATED: { -+ if (sendChunk && !sentChunk) { -+ this.sendQueue.enqueue(chunk); -+ } -+ if (squareDistance <= tickViewDistance) { -+ this.tickingQueue.enqueue(chunk); -+ } -+ break; -+ } -+ case CHUNK_TICKET_STAGE_TICK: { -+ if (sendChunk && !sentChunk) { -+ this.sendQueue.enqueue(chunk); -+ } -+ break; -+ } -+ default: { -+ throw new IllegalStateException("Unknown stage: " + stage); -+ } -+ } -+ } -+ -+ // update the chunk center -+ // this must be done last so that the client does not ignore any of our unload chunk packets above -+ if (this.lastSentChunkCenterX != currentChunkX || this.lastSentChunkCenterZ != currentChunkZ) { -+ this.player.connection.send(this.updateClientChunkCenter(currentChunkX, currentChunkZ)); -+ } -+ -+ this.flushDelayedTicketOps(); -+ } -+ -+ void remove() { -+ TickThread.ensureTickThread(this.player, "Cannot add player asynchronously"); -+ if (this.removed) { -+ throw new IllegalStateException("Removing removed player chunk loader"); -+ } -+ this.removed = true; -+ // sends the chunk unload packets -+ this.broadcastMap.remove(); -+ // cleans up loading/generating tickets -+ this.loadTicketCleanup.remove(); -+ // cleans up ticking tickets -+ this.tickMap.remove(); -+ -+ // purge queues -+ this.sendQueue.clear(); -+ this.tickingQueue.clear(); -+ this.generatingQueue.clear(); -+ this.genQueue.clear(); -+ this.loadingQueue.clear(); -+ this.loadQueue.clear(); -+ -+ // flush ticket changes -+ this.flushDelayedTicketOps(); -+ -+ // now all tickets should be removed, which is all of our external state -+ } -+ } -+ -+ // TODO rebase into util patch -+ private static final class AllocatingRateLimiter { -+ -+ // max difference granularity in ns -+ private static final long MAX_GRANULARITY = TimeUnit.SECONDS.toNanos(1L); -+ -+ private double allocation; -+ private long lastAllocationUpdate; -+ private double takeCarry; -+ private long lastTakeUpdate; -+ -+ // rate in units/s, and time in ns -+ public void tickAllocation(final long time, final double rate, final double maxAllocation) { -+ final long diff = Math.min(MAX_GRANULARITY, time - this.lastAllocationUpdate); -+ this.lastAllocationUpdate = time; -+ -+ this.allocation = Math.min(maxAllocation - this.takeCarry, this.allocation + rate * (diff*1.0E-9D)); -+ } -+ -+ // rate in units/s, and time in ns -+ public long takeAllocation(final long time, final double rate, final long maxTake) { -+ if (maxTake < 1L) { -+ return 0L; -+ } -+ -+ double ret = this.takeCarry; -+ final long diff = Math.min(MAX_GRANULARITY, time - this.lastTakeUpdate); -+ this.lastTakeUpdate = time; -+ -+ // note: abs(takeCarry) <= 1.0 -+ final double take = Math.min(Math.min((double)maxTake - this.takeCarry, this.allocation), rate * (diff*1.0E-9)); -+ -+ ret += take; -+ this.allocation -= take; -+ -+ final long retInteger = (long)Math.floor(ret); -+ this.takeCarry = ret - (double)retInteger; -+ -+ return retInteger; -+ } -+ } -+ -+ static final class CountedSRSWLinkedQueue { -+ -+ private final SRSWLinkedQueue queue = new SRSWLinkedQueue<>(); -+ private volatile long countAdded; -+ private volatile long countRemoved; -+ -+ private static final VarHandle COUNT_ADDED_HANDLE = ConcurrentUtil.getVarHandle(CountedSRSWLinkedQueue.class, "countAdded", long.class); -+ private static final VarHandle COUNT_REMOVED_HANDLE = ConcurrentUtil.getVarHandle(CountedSRSWLinkedQueue.class, "countRemoved", long.class); -+ -+ private long getCountAddedPlain() { -+ return (long)COUNT_ADDED_HANDLE.get(this); -+ } -+ -+ private long getCountAddedAcquire() { -+ return (long)COUNT_ADDED_HANDLE.getAcquire(this); -+ } -+ -+ private void setCountAddedRelease(final long to) { -+ COUNT_ADDED_HANDLE.setRelease(this, to); -+ } -+ -+ private long getCountRemovedPlain() { -+ return (long)COUNT_REMOVED_HANDLE.get(this); -+ } -+ -+ private long getCountRemovedAcquire() { -+ return (long)COUNT_REMOVED_HANDLE.getAcquire(this); -+ } -+ -+ private void setCountRemovedRelease(final long to) { -+ COUNT_REMOVED_HANDLE.setRelease(this, to); -+ } -+ -+ public void add(final E element) { -+ this.setCountAddedRelease(this.getCountAddedPlain() + 1L); -+ this.queue.addLast(element); -+ } -+ -+ public E poll() { -+ final E ret = this.queue.poll(); -+ if (ret != null) { -+ this.setCountRemovedRelease(this.getCountRemovedPlain() + 1L); -+ } -+ -+ return ret; -+ } -+ -+ public long size() { -+ final long removed = this.getCountRemovedAcquire(); -+ final long added = this.getCountAddedAcquire(); -+ -+ return added - removed; -+ } -+ } -+ -+ private static class CustomLongArray extends LongArrayList { -+ -+ public CustomLongArray() { -+ super(); -+ } -+ -+ public CustomLongArray(final int expected) { -+ super(expected); -+ } -+ -+ public boolean addAll(final CustomLongArray list) { -+ this.addElements(this.size, list.a, 0, list.size); -+ return list.size != 0; -+ } -+ -+ public void addUnchecked(final long value) { -+ this.a[this.size++] = value; -+ } -+ -+ public void forceSize(final int to) { -+ this.size = to; -+ } -+ -+ @Override -+ public int hashCode() { -+ long h = 1L; -+ -+ Objects.checkFromToIndex(0, this.size, this.a.length); -+ -+ for (int i = 0; i < this.size; ++i) { -+ h = it.unimi.dsi.fastutil.HashCommon.mix(h + this.a[i]); -+ } -+ -+ return (int)h; -+ } -+ -+ @Override -+ public boolean equals(final Object o) { -+ if (o == this) { -+ return true; -+ } -+ -+ if (!(o instanceof CustomLongArray other)) { -+ return false; -+ } -+ -+ return this.size == other.size && Arrays.equals(this.a, 0, this.size, other.a, 0, this.size); -+ } -+ } -+ -+ private static int getDistanceSize(final int radius, final int max) { -+ if (radius == 0) { -+ return 1; -+ } -+ final int diff = radius - max; -+ if (diff <= 0) { -+ return 4*radius; -+ } -+ return 4*(max - Math.max(0, diff - 1)); -+ } -+ -+ private static int getQ1DistanceSize(final int radius, final int max) { -+ if (radius == 0) { -+ return 1; -+ } -+ final int diff = radius - max; -+ if (diff <= 0) { -+ return radius+1; -+ } -+ return max - diff + 1; -+ } -+ -+ private static final class BasicFIFOLQueue { -+ -+ private final long[] values; -+ private int head, tail; -+ -+ public BasicFIFOLQueue(final int cap) { -+ if (cap <= 1) { -+ throw new IllegalArgumentException(); -+ } -+ this.values = new long[cap]; -+ } -+ -+ public boolean isEmpty() { -+ return this.head == this.tail; -+ } -+ -+ public long removeFirst() { -+ final long ret = this.values[this.head]; -+ -+ if (this.head == this.tail) { -+ throw new IllegalStateException(); -+ } -+ -+ ++this.head; -+ if (this.head == this.values.length) { -+ this.head = 0; -+ } -+ -+ return ret; -+ } -+ -+ public void addLast(final long value) { -+ this.values[this.tail++] = value; -+ -+ if (this.tail == this.head) { -+ throw new IllegalStateException(); -+ } -+ -+ if (this.tail == this.values.length) { -+ this.tail = 0; -+ } -+ } -+ } -+ -+ private static CustomLongArray[] makeQ1BFS(final int radius) { -+ final CustomLongArray[] ret = new CustomLongArray[2 * radius + 1]; -+ final BasicFIFOLQueue queue = new BasicFIFOLQueue(Math.max(1, 4 * radius) + 1); -+ final LongOpenHashSet seen = new LongOpenHashSet((radius + 1) * (radius + 1)); -+ -+ seen.add(CoordinateUtils.getChunkKey(0, 0)); -+ queue.addLast(CoordinateUtils.getChunkKey(0, 0)); -+ while (!queue.isEmpty()) { -+ final long chunk = queue.removeFirst(); -+ final int chunkX = CoordinateUtils.getChunkX(chunk); -+ final int chunkZ = CoordinateUtils.getChunkZ(chunk); -+ -+ final int index = Math.abs(chunkX) + Math.abs(chunkZ); -+ final CustomLongArray list = ret[index]; -+ if (list != null) { -+ list.addUnchecked(chunk); -+ } else { -+ (ret[index] = new CustomLongArray(getQ1DistanceSize(index, radius))).addUnchecked(chunk); -+ } -+ -+ for (int i = 0; i < 4; ++i) { -+ // 0 -> -1, 0 -+ // 1 -> 0, -1 -+ // 2 -> 1, 0 -+ // 3 -> 0, 1 -+ -+ final int signInv = -(i >>> 1); // 2/3 -> -(1), 0/1 -> -(0) -+ // note: -n = (~n) + 1 -+ // (n ^ signInv) - signInv = signInv == 0 ? ((n ^ 0) - 0 = n) : ((n ^ -1) - (-1) = ~n + 1) -+ -+ final int axis = i & 1; // 0/2 -> 0, 1/3 -> 1 -+ final int dx = ((axis - 1) ^ signInv) - signInv; // 0 -> -1, 1 -> 0 -+ final int dz = (-axis ^ signInv) - signInv; // 0 -> 0, 1 -> -1 -+ -+ final int neighbourX = chunkX + dx; -+ final int neighbourZ = chunkZ + dz; -+ final long neighbour = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); -+ -+ if ((neighbourX | neighbourZ) < 0 || Math.max(Math.abs(neighbourX), Math.abs(neighbourZ)) > radius) { -+ // don't enqueue out of range -+ continue; -+ } -+ -+ if (!seen.add(neighbour)) { -+ continue; -+ } -+ -+ queue.addLast(neighbour); -+ } -+ } -+ -+ return ret; -+ } -+ -+ // doesn't appear worth optimising this function now, even though it's 70% of the call -+ private static CustomLongArray spread(final CustomLongArray input, final int size) { -+ final LongLinkedOpenHashSet notAdded = new LongLinkedOpenHashSet(input); -+ final CustomLongArray added = new CustomLongArray(size); -+ -+ while (!notAdded.isEmpty()) { -+ if (added.isEmpty()) { -+ added.addUnchecked(notAdded.removeLastLong()); -+ continue; -+ } -+ -+ long maxChunk = -1L; -+ int maxDist = 0; -+ -+ // select the chunk from the not yet added set that has the largest minimum distance from -+ // the current set of added chunks -+ -+ for (final LongIterator iterator = notAdded.iterator(); iterator.hasNext();) { -+ final long chunkKey = iterator.nextLong(); -+ final int chunkX = CoordinateUtils.getChunkX(chunkKey); -+ final int chunkZ = CoordinateUtils.getChunkZ(chunkKey); -+ -+ int minDist = Integer.MAX_VALUE; -+ -+ final int len = added.size(); -+ final long[] addedArr = added.elements(); -+ Objects.checkFromToIndex(0, len, addedArr.length); -+ for (int i = 0; i < len; ++i) { -+ final long addedKey = addedArr[i]; -+ final int addedX = CoordinateUtils.getChunkX(addedKey); -+ final int addedZ = CoordinateUtils.getChunkZ(addedKey); -+ -+ // here we use square distance because chunk generation uses neighbours in a square radius -+ final int dist = Math.max(Math.abs(addedX - chunkX), Math.abs(addedZ - chunkZ)); -+ -+ minDist = Math.min(dist, minDist); -+ } -+ -+ if (minDist > maxDist) { -+ maxDist = minDist; -+ maxChunk = chunkKey; -+ } -+ } -+ -+ // move the selected chunk from the not added set to the added set -+ -+ if (!notAdded.remove(maxChunk)) { -+ throw new IllegalStateException(); -+ } -+ -+ added.addUnchecked(maxChunk); -+ } -+ -+ return added; -+ } -+} -diff --git a/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java b/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java -new file mode 100644 -index 0000000000000000000000000000000000000000..15ee41452992714108efe53b708b5a4e1da7c1ff ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/system/entity/EntityLookup.java -@@ -0,0 +1,902 @@ -+package io.papermc.paper.chunk.system.entity; -+ -+import com.destroystokyo.paper.util.maplist.EntityList; -+import com.mojang.logging.LogUtils; -+import io.papermc.paper.util.CoordinateUtils; -+import io.papermc.paper.util.TickThread; -+import io.papermc.paper.util.WorldUtil; -+import io.papermc.paper.world.ChunkEntitySlices; -+import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; -+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -+import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; -+import net.minecraft.core.BlockPos; -+import io.papermc.paper.chunk.system.ChunkSystem; -+import net.minecraft.server.level.ChunkHolder; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.util.AbortableIterationConsumer; -+import net.minecraft.util.Mth; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.entity.EntityType; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.entity.EntityInLevelCallback; -+import net.minecraft.world.level.entity.EntityTypeTest; -+import net.minecraft.world.level.entity.LevelCallback; -+import net.minecraft.server.level.FullChunkStatus; -+import net.minecraft.world.level.entity.LevelEntityGetter; -+import net.minecraft.world.level.entity.Visibility; -+import net.minecraft.world.phys.AABB; -+import net.minecraft.world.phys.Vec3; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.slf4j.Logger; -+import java.util.ArrayList; -+import java.util.Arrays; -+import java.util.Iterator; -+import java.util.List; -+import java.util.NoSuchElementException; -+import java.util.UUID; -+import java.util.concurrent.locks.StampedLock; -+import java.util.function.Consumer; -+import java.util.function.Predicate; -+ -+public final class EntityLookup implements LevelEntityGetter { -+ -+ private static final Logger LOGGER = LogUtils.getClassLogger(); -+ -+ protected static final int REGION_SHIFT = 5; -+ protected static final int REGION_MASK = (1 << REGION_SHIFT) - 1; -+ protected static final int REGION_SIZE = 1 << REGION_SHIFT; -+ -+ public final ServerLevel world; -+ -+ private final StampedLock stateLock = new StampedLock(); -+ protected final Long2ObjectOpenHashMap regions = new Long2ObjectOpenHashMap<>(128, 0.5f); -+ -+ private final int minSection; // inclusive -+ private final int maxSection; // inclusive -+ private final LevelCallback worldCallback; -+ -+ private final StampedLock entityByLock = new StampedLock(); -+ private final Int2ReferenceOpenHashMap entityById = new Int2ReferenceOpenHashMap<>(); -+ private final Object2ReferenceOpenHashMap entityByUUID = new Object2ReferenceOpenHashMap<>(); -+ private final EntityList accessibleEntities = new EntityList(); -+ -+ public EntityLookup(final ServerLevel world, final LevelCallback worldCallback) { -+ this.world = world; -+ this.minSection = WorldUtil.getMinSection(world); -+ this.maxSection = WorldUtil.getMaxSection(world); -+ this.worldCallback = worldCallback; -+ } -+ -+ private static Entity maskNonAccessible(final Entity entity) { -+ if (entity == null) { -+ return null; -+ } -+ final Visibility visibility = EntityLookup.getEntityStatus(entity); -+ return visibility.isAccessible() ? entity : null; -+ } -+ -+ @Nullable -+ @Override -+ public Entity get(final int id) { -+ final long attempt = this.entityByLock.tryOptimisticRead(); -+ if (attempt != 0L) { -+ try { -+ final Entity ret = this.entityById.get(id); -+ -+ if (this.entityByLock.validate(attempt)) { -+ return maskNonAccessible(ret); -+ } -+ } catch (final Error error) { -+ throw error; -+ } catch (final Throwable thr) { -+ // ignore -+ } -+ } -+ -+ this.entityByLock.readLock(); -+ try { -+ return maskNonAccessible(this.entityById.get(id)); -+ } finally { -+ this.entityByLock.tryUnlockRead(); -+ } -+ } -+ -+ @Nullable -+ @Override -+ public Entity get(final UUID id) { -+ final long attempt = this.entityByLock.tryOptimisticRead(); -+ if (attempt != 0L) { -+ try { -+ final Entity ret = this.entityByUUID.get(id); -+ -+ if (this.entityByLock.validate(attempt)) { -+ return maskNonAccessible(ret); -+ } -+ } catch (final Error error) { -+ throw error; -+ } catch (final Throwable thr) { -+ // ignore -+ } -+ } -+ -+ this.entityByLock.readLock(); -+ try { -+ return maskNonAccessible(this.entityByUUID.get(id)); -+ } finally { -+ this.entityByLock.tryUnlockRead(); -+ } -+ } -+ -+ public boolean hasEntity(final UUID uuid) { -+ return this.get(uuid) != null; -+ } -+ -+ public String getDebugInfo() { -+ return "count_id:" + this.entityById.size() + ",count_uuid:" + this.entityByUUID.size() + ",region_count:" + this.regions.size(); -+ } -+ -+ static final class ArrayIterable implements Iterable { -+ -+ private final T[] array; -+ private final int off; -+ private final int length; -+ -+ public ArrayIterable(final T[] array, final int off, final int length) { -+ this.array = array; -+ this.off = off; -+ this.length = length; -+ if (length > array.length) { -+ throw new IllegalArgumentException("Length must be no greater-than the array length"); -+ } -+ } -+ -+ @NotNull -+ @Override -+ public Iterator iterator() { -+ return new ArrayIterator<>(this.array, this.off, this.length); -+ } -+ -+ static final class ArrayIterator implements Iterator { -+ -+ private final T[] array; -+ private int off; -+ private final int length; -+ -+ public ArrayIterator(final T[] array, final int off, final int length) { -+ this.array = array; -+ this.off = off; -+ this.length = length; -+ } -+ -+ @Override -+ public boolean hasNext() { -+ return this.off < this.length; -+ } -+ -+ @Override -+ public T next() { -+ if (this.off >= this.length) { -+ throw new NoSuchElementException(); -+ } -+ return this.array[this.off++]; -+ } -+ -+ @Override -+ public void remove() { -+ throw new UnsupportedOperationException(); -+ } -+ } -+ } -+ -+ @Override -+ public Iterable getAll() { -+ return new ArrayIterable<>(this.accessibleEntities.getRawData(), 0, this.accessibleEntities.size()); -+ } -+ -+ public Entity[] getAllCopy() { -+ return Arrays.copyOf(this.accessibleEntities.getRawData(), this.accessibleEntities.size(), Entity[].class); -+ } -+ -+ @Override -+ public void get(final EntityTypeTest filter, final AbortableIterationConsumer action) { -+ final Int2ReferenceOpenHashMap entityCopy; -+ -+ this.entityByLock.readLock(); -+ try { -+ entityCopy = this.entityById.clone(); -+ } finally { -+ this.entityByLock.tryUnlockRead(); -+ } -+ for (final Entity entity : entityCopy.values()) { -+ final Visibility visibility = EntityLookup.getEntityStatus(entity); -+ if (!visibility.isAccessible()) { -+ continue; -+ } -+ final U casted = filter.tryCast(entity); -+ if (casted != null && action.accept(casted).shouldAbort()) { -+ break; -+ } -+ } -+ } -+ -+ @Override -+ public void get(final AABB box, final Consumer action) { -+ List entities = new ArrayList<>(); -+ this.getEntitiesWithoutDragonParts(null, box, entities, null); -+ for (int i = 0, len = entities.size(); i < len; ++i) { -+ action.accept(entities.get(i)); -+ } -+ } -+ -+ @Override -+ public void get(final EntityTypeTest filter, final AABB box, final AbortableIterationConsumer action) { -+ List entities = new ArrayList<>(); -+ this.getEntitiesWithoutDragonParts(null, box, entities, null); -+ for (int i = 0, len = entities.size(); i < len; ++i) { -+ final U casted = filter.tryCast(entities.get(i)); -+ if (casted != null && action.accept(casted).shouldAbort()) { -+ break; -+ } -+ } -+ } -+ -+ public void entityStatusChange(final Entity entity, final ChunkEntitySlices slices, final Visibility oldVisibility, final Visibility newVisibility, final boolean moved, -+ final boolean created, final boolean destroyed) { -+ TickThread.ensureTickThread(entity, "Entity status change must only happen on the main thread"); -+ -+ if (entity.updatingSectionStatus) { -+ // recursive status update -+ LOGGER.error("Cannot recursively update entity chunk status for entity " + entity, new Throwable()); -+ return; -+ } -+ -+ final boolean entityStatusUpdateBefore = slices == null ? false : slices.startPreventingStatusUpdates(); -+ -+ if (entityStatusUpdateBefore) { -+ LOGGER.error("Cannot update chunk status for entity " + entity + " since entity chunk (" + slices.chunkX + "," + slices.chunkZ + ") is receiving update", new Throwable()); -+ return; -+ } -+ -+ try { -+ final Boolean ticketBlockBefore = this.world.chunkTaskScheduler.chunkHolderManager.blockTicketUpdates(); -+ try { -+ entity.updatingSectionStatus = true; -+ try { -+ if (created) { -+ EntityLookup.this.worldCallback.onCreated(entity); -+ } -+ -+ if (oldVisibility == newVisibility) { -+ if (moved && newVisibility.isAccessible()) { -+ EntityLookup.this.worldCallback.onSectionChange(entity); -+ } -+ return; -+ } -+ -+ if (newVisibility.ordinal() > oldVisibility.ordinal()) { -+ // status upgrade -+ if (!oldVisibility.isAccessible() && newVisibility.isAccessible()) { -+ this.accessibleEntities.add(entity); -+ EntityLookup.this.worldCallback.onTrackingStart(entity); -+ } -+ -+ if (!oldVisibility.isTicking() && newVisibility.isTicking()) { -+ EntityLookup.this.worldCallback.onTickingStart(entity); -+ } -+ } else { -+ // status downgrade -+ if (oldVisibility.isTicking() && !newVisibility.isTicking()) { -+ EntityLookup.this.worldCallback.onTickingEnd(entity); -+ } -+ -+ if (oldVisibility.isAccessible() && !newVisibility.isAccessible()) { -+ this.accessibleEntities.remove(entity); -+ EntityLookup.this.worldCallback.onTrackingEnd(entity); -+ } -+ } -+ -+ if (moved && newVisibility.isAccessible()) { -+ EntityLookup.this.worldCallback.onSectionChange(entity); -+ } -+ -+ if (destroyed) { -+ EntityLookup.this.worldCallback.onDestroyed(entity); -+ } -+ } finally { -+ entity.updatingSectionStatus = false; -+ } -+ } finally { -+ this.world.chunkTaskScheduler.chunkHolderManager.unblockTicketUpdates(ticketBlockBefore); -+ } -+ } finally { -+ if (slices != null) { -+ slices.stopPreventingStatusUpdates(false); -+ } -+ } -+ } -+ -+ public void chunkStatusChange(final int x, final int z, final FullChunkStatus newStatus) { -+ this.getChunk(x, z).updateStatus(newStatus, this); -+ } -+ -+ public void addLegacyChunkEntities(final List entities, final ChunkPos forChunk) { -+ this.addEntityChunk(entities, forChunk, true); -+ } -+ -+ public void addEntityChunkEntities(final List entities, final ChunkPos forChunk) { -+ this.addEntityChunk(entities, forChunk, true); -+ } -+ -+ public void addWorldGenChunkEntities(final List entities, final ChunkPos forChunk) { -+ this.addEntityChunk(entities, forChunk, false); -+ } -+ -+ private void addRecursivelySafe(final Entity root, final boolean fromDisk) { -+ if (!this.addEntity(root, fromDisk)) { -+ // possible we are a passenger, and so should dismount from any valid entity in the world -+ root.stopRiding(true); -+ return; -+ } -+ for (final Entity passenger : root.getPassengers()) { -+ this.addRecursivelySafe(passenger, fromDisk); -+ } -+ } -+ -+ private void addEntityChunk(final List entities, final ChunkPos forChunk, final boolean fromDisk) { -+ for (int i = 0, len = entities.size(); i < len; ++i) { -+ final Entity entity = entities.get(i); -+ if (entity.isPassenger()) { -+ continue; -+ } -+ -+ if (!entity.chunkPosition().equals(forChunk)) { -+ LOGGER.warn("Root entity " + entity + " is outside of serialized chunk " + forChunk); -+ // can't set removed here, as we may not own the chunk position -+ // skip the entity -+ continue; -+ } -+ -+ final Vec3 rootPosition = entity.position(); -+ -+ // always adjust positions before adding passengers in case plugins access the entity, and so that -+ // they are added to the right entity chunk -+ for (final Entity passenger : entity.getIndirectPassengers()) { -+ if (!passenger.chunkPosition().equals(forChunk)) { -+ passenger.setPosRaw(rootPosition.x, rootPosition.y, rootPosition.z, true); -+ } -+ } -+ -+ this.addRecursivelySafe(entity, fromDisk); -+ } -+ } -+ -+ public boolean addNewEntity(final Entity entity) { -+ return this.addEntity(entity, false); -+ } -+ -+ public static Visibility getEntityStatus(final Entity entity) { -+ if (entity.isAlwaysTicking()) { -+ return Visibility.TICKING; -+ } -+ final FullChunkStatus entityStatus = entity.chunkStatus; -+ return Visibility.fromFullChunkStatus(entityStatus == null ? FullChunkStatus.INACCESSIBLE : entityStatus); -+ } -+ -+ private boolean addEntity(final Entity entity, final boolean fromDisk) { -+ final BlockPos pos = entity.blockPosition(); -+ final int sectionX = pos.getX() >> 4; -+ final int sectionY = Mth.clamp(pos.getY() >> 4, this.minSection, this.maxSection); -+ final int sectionZ = pos.getZ() >> 4; -+ TickThread.ensureTickThread(this.world, sectionX, sectionZ, "Cannot add entity off-main thread"); -+ -+ if (entity.isRemoved()) { -+ LOGGER.warn("Refusing to add removed entity: " + entity); -+ return false; -+ } -+ -+ if (entity.updatingSectionStatus) { -+ LOGGER.warn("Entity " + entity + " is currently prevented from being added/removed to world since it is processing section status updates", new Throwable()); -+ return false; -+ } -+ -+ if (fromDisk) { -+ ChunkSystem.onEntityPreAdd(this.world, entity); -+ if (entity.isRemoved()) { -+ // removed from checkDupeUUID call -+ return false; -+ } -+ } -+ -+ this.entityByLock.writeLock(); -+ try { -+ if (this.entityById.containsKey(entity.getId())) { -+ LOGGER.warn("Entity id already exists: " + entity.getId() + ", mapped to " + this.entityById.get(entity.getId()) + ", can't add " + entity); -+ return false; -+ } -+ if (this.entityByUUID.containsKey(entity.getUUID())) { -+ LOGGER.warn("Entity uuid already exists: " + entity.getUUID() + ", mapped to " + this.entityByUUID.get(entity.getUUID()) + ", can't add " + entity); -+ return false; -+ } -+ this.entityById.put(entity.getId(), entity); -+ this.entityByUUID.put(entity.getUUID(), entity); -+ } finally { -+ this.entityByLock.tryUnlockWrite(); -+ } -+ -+ entity.sectionX = sectionX; -+ entity.sectionY = sectionY; -+ entity.sectionZ = sectionZ; -+ final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ); -+ if (!slices.addEntity(entity, sectionY)) { -+ LOGGER.warn("Entity " + entity + " added to world '" + this.world.getWorld().getName() + "', but was already contained in entity chunk (" + sectionX + "," + sectionZ + ")"); -+ } -+ -+ entity.setLevelCallback(new EntityCallback(entity)); -+ -+ this.entityStatusChange(entity, slices, Visibility.HIDDEN, getEntityStatus(entity), false, !fromDisk, false); -+ -+ return true; -+ } -+ -+ public boolean canRemoveEntity(final Entity entity) { -+ if (entity.updatingSectionStatus) { -+ return false; -+ } -+ -+ final int sectionX = entity.sectionX; -+ final int sectionZ = entity.sectionZ; -+ final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ); -+ return slices == null || !slices.isPreventingStatusUpdates(); -+ } -+ -+ private void removeEntity(final Entity entity) { -+ final int sectionX = entity.sectionX; -+ final int sectionY = entity.sectionY; -+ final int sectionZ = entity.sectionZ; -+ TickThread.ensureTickThread(this.world, sectionX, sectionZ, "Cannot remove entity off-main"); -+ if (!entity.isRemoved()) { -+ throw new IllegalStateException("Only call Entity#setRemoved to remove an entity"); -+ } -+ final ChunkEntitySlices slices = this.getChunk(sectionX, sectionZ); -+ // all entities should be in a chunk -+ if (slices == null) { -+ LOGGER.warn("Cannot remove entity " + entity + " from null entity slices (" + sectionX + "," + sectionZ + ")"); -+ } else { -+ if (slices.isPreventingStatusUpdates()) { -+ throw new IllegalStateException("Attempting to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ") that is receiving status updates"); -+ } -+ if (!slices.removeEntity(entity, sectionY)) { -+ LOGGER.warn("Failed to remove entity " + entity + " from entity slices (" + sectionX + "," + sectionZ + ")"); -+ } -+ } -+ entity.sectionX = entity.sectionY = entity.sectionZ = Integer.MIN_VALUE; -+ -+ this.entityByLock.writeLock(); -+ try { -+ if (!this.entityById.remove(entity.getId(), entity)) { -+ LOGGER.warn("Failed to remove entity " + entity + " by id, current entity mapped: " + this.entityById.get(entity.getId())); -+ } -+ if (!this.entityByUUID.remove(entity.getUUID(), entity)) { -+ LOGGER.warn("Failed to remove entity " + entity + " by uuid, current entity mapped: " + this.entityByUUID.get(entity.getUUID())); -+ } -+ } finally { -+ this.entityByLock.tryUnlockWrite(); -+ } -+ } -+ -+ private ChunkEntitySlices moveEntity(final Entity entity) { -+ // ensure we own the entity -+ TickThread.ensureTickThread(entity, "Cannot move entity off-main"); -+ -+ final BlockPos newPos = entity.blockPosition(); -+ final int newSectionX = newPos.getX() >> 4; -+ final int newSectionY = Mth.clamp(newPos.getY() >> 4, this.minSection, this.maxSection); -+ final int newSectionZ = newPos.getZ() >> 4; -+ -+ if (newSectionX == entity.sectionX && newSectionY == entity.sectionY && newSectionZ == entity.sectionZ) { -+ return null; -+ } -+ -+ // ensure the new section is owned by this tick thread -+ TickThread.ensureTickThread(this.world, newSectionX, newSectionZ, "Cannot move entity off-main"); -+ -+ // ensure the old section is owned by this tick thread -+ TickThread.ensureTickThread(this.world, entity.sectionX, entity.sectionZ, "Cannot move entity off-main"); -+ -+ final ChunkEntitySlices old = this.getChunk(entity.sectionX, entity.sectionZ); -+ final ChunkEntitySlices slices = this.getOrCreateChunk(newSectionX, newSectionZ); -+ -+ if (!old.removeEntity(entity, entity.sectionY)) { -+ LOGGER.warn("Could not remove entity " + entity + " from its old chunk section (" + entity.sectionX + "," + entity.sectionY + "," + entity.sectionZ + ") since it was not contained in the section"); -+ } -+ -+ if (!slices.addEntity(entity, newSectionY)) { -+ LOGGER.warn("Could not add entity " + entity + " to its new chunk section (" + newSectionX + "," + newSectionY + "," + newSectionZ + ") as it is already contained in the section"); -+ } -+ -+ entity.sectionX = newSectionX; -+ entity.sectionY = newSectionY; -+ entity.sectionZ = newSectionZ; -+ -+ return slices; -+ } -+ -+ public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List into, final Predicate predicate) { -+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; -+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; -+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; -+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; -+ -+ final int minRegionX = minChunkX >> REGION_SHIFT; -+ final int minRegionZ = minChunkZ >> REGION_SHIFT; -+ final int maxRegionX = maxChunkX >> REGION_SHIFT; -+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; -+ -+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { -+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; -+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; -+ -+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { -+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); -+ -+ if (region == null) { -+ continue; -+ } -+ -+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; -+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; -+ -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); -+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { -+ continue; -+ } -+ -+ chunk.getEntitiesWithoutDragonParts(except, box, into, predicate); -+ } -+ } -+ } -+ } -+ } -+ -+ public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { -+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; -+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; -+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; -+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; -+ -+ final int minRegionX = minChunkX >> REGION_SHIFT; -+ final int minRegionZ = minChunkZ >> REGION_SHIFT; -+ final int maxRegionX = maxChunkX >> REGION_SHIFT; -+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; -+ -+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { -+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; -+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; -+ -+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { -+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); -+ -+ if (region == null) { -+ continue; -+ } -+ -+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; -+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; -+ -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); -+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { -+ continue; -+ } -+ -+ chunk.getEntities(except, box, into, predicate); -+ } -+ } -+ } -+ } -+ } -+ -+ public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { -+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; -+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; -+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; -+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; -+ -+ final int minRegionX = minChunkX >> REGION_SHIFT; -+ final int minRegionZ = minChunkZ >> REGION_SHIFT; -+ final int maxRegionX = maxChunkX >> REGION_SHIFT; -+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; -+ -+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { -+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; -+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; -+ -+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { -+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); -+ -+ if (region == null) { -+ continue; -+ } -+ -+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; -+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; -+ -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); -+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { -+ continue; -+ } -+ -+ chunk.getHardCollidingEntities(except, box, into, predicate); -+ } -+ } -+ } -+ } -+ } -+ -+ public void getEntities(final EntityType type, final AABB box, final List into, -+ final Predicate predicate) { -+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; -+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; -+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; -+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; -+ -+ final int minRegionX = minChunkX >> REGION_SHIFT; -+ final int minRegionZ = minChunkZ >> REGION_SHIFT; -+ final int maxRegionX = maxChunkX >> REGION_SHIFT; -+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; -+ -+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { -+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; -+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; -+ -+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { -+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); -+ -+ if (region == null) { -+ continue; -+ } -+ -+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; -+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; -+ -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); -+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { -+ continue; -+ } -+ -+ chunk.getEntities(type, box, (List)into, (Predicate)predicate); -+ } -+ } -+ } -+ } -+ } -+ -+ public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, -+ final Predicate predicate) { -+ final int minChunkX = (Mth.floor(box.minX) - 2) >> 4; -+ final int minChunkZ = (Mth.floor(box.minZ) - 2) >> 4; -+ final int maxChunkX = (Mth.floor(box.maxX) + 2) >> 4; -+ final int maxChunkZ = (Mth.floor(box.maxZ) + 2) >> 4; -+ -+ final int minRegionX = minChunkX >> REGION_SHIFT; -+ final int minRegionZ = minChunkZ >> REGION_SHIFT; -+ final int maxRegionX = maxChunkX >> REGION_SHIFT; -+ final int maxRegionZ = maxChunkZ >> REGION_SHIFT; -+ -+ for (int currRegionZ = minRegionZ; currRegionZ <= maxRegionZ; ++currRegionZ) { -+ final int minZ = currRegionZ == minRegionZ ? minChunkZ & REGION_MASK : 0; -+ final int maxZ = currRegionZ == maxRegionZ ? maxChunkZ & REGION_MASK : REGION_MASK; -+ -+ for (int currRegionX = minRegionX; currRegionX <= maxRegionX; ++currRegionX) { -+ final ChunkSlicesRegion region = this.getRegion(currRegionX, currRegionZ); -+ -+ if (region == null) { -+ continue; -+ } -+ -+ final int minX = currRegionX == minRegionX ? minChunkX & REGION_MASK : 0; -+ final int maxX = currRegionX == maxRegionX ? maxChunkX & REGION_MASK : REGION_MASK; -+ -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ final ChunkEntitySlices chunk = region.get(currX | (currZ << REGION_SHIFT)); -+ if (chunk == null || !chunk.status.isOrAfter(FullChunkStatus.FULL)) { -+ continue; -+ } -+ -+ chunk.getEntities(clazz, except, box, into, predicate); -+ } -+ } -+ } -+ } -+ } -+ -+ public void entitySectionLoad(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) { -+ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot load in entity section off-main"); -+ synchronized (this) { -+ final ChunkEntitySlices curr = this.getChunk(chunkX, chunkZ); -+ if (curr != null) { -+ this.removeChunk(chunkX, chunkZ); -+ -+ curr.mergeInto(slices); -+ -+ this.addChunk(chunkX, chunkZ, slices); -+ } else { -+ this.addChunk(chunkX, chunkZ, slices); -+ } -+ } -+ } -+ -+ public void entitySectionUnload(final int chunkX, final int chunkZ) { -+ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot unload entity section off-main"); -+ this.removeChunk(chunkX, chunkZ); -+ } -+ -+ public ChunkEntitySlices getChunk(final int chunkX, final int chunkZ) { -+ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); -+ if (region == null) { -+ return null; -+ } -+ -+ return region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT)); -+ } -+ -+ public ChunkEntitySlices getOrCreateChunk(final int chunkX, final int chunkZ) { -+ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); -+ ChunkEntitySlices ret; -+ if (region == null || (ret = region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT))) == null) { -+ // loadInEntityChunk will call addChunk for us -+ return this.world.chunkTaskScheduler.chunkHolderManager.getOrCreateEntityChunk(chunkX, chunkZ, true); -+ } -+ -+ return ret; -+ } -+ -+ public ChunkSlicesRegion getRegion(final int regionX, final int regionZ) { -+ final long key = CoordinateUtils.getChunkKey(regionX, regionZ); -+ final long attempt = this.stateLock.tryOptimisticRead(); -+ if (attempt != 0L) { -+ try { -+ final ChunkSlicesRegion ret = this.regions.get(key); -+ -+ if (this.stateLock.validate(attempt)) { -+ return ret; -+ } -+ } catch (final Error error) { -+ throw error; -+ } catch (final Throwable thr) { -+ // ignore -+ } -+ } -+ -+ this.stateLock.readLock(); -+ try { -+ return this.regions.get(key); -+ } finally { -+ this.stateLock.tryUnlockRead(); -+ } -+ } -+ -+ private synchronized void removeChunk(final int chunkX, final int chunkZ) { -+ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); -+ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); -+ -+ final ChunkSlicesRegion region = this.regions.get(key); -+ final int remaining = region.remove(relIndex); -+ -+ if (remaining == 0) { -+ this.stateLock.writeLock(); -+ try { -+ this.regions.remove(key); -+ } finally { -+ this.stateLock.tryUnlockWrite(); -+ } -+ } -+ } -+ -+ public synchronized void addChunk(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) { -+ final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); -+ final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT); -+ -+ ChunkSlicesRegion region = this.regions.get(key); -+ if (region != null) { -+ region.add(relIndex, slices); -+ } else { -+ region = new ChunkSlicesRegion(); -+ region.add(relIndex, slices); -+ this.stateLock.writeLock(); -+ try { -+ this.regions.put(key, region); -+ } finally { -+ this.stateLock.tryUnlockWrite(); -+ } -+ } -+ } -+ -+ public static final class ChunkSlicesRegion { -+ -+ protected final ChunkEntitySlices[] slices = new ChunkEntitySlices[REGION_SIZE * REGION_SIZE]; -+ protected int sliceCount; -+ -+ public ChunkEntitySlices get(final int index) { -+ return this.slices[index]; -+ } -+ -+ public int remove(final int index) { -+ final ChunkEntitySlices slices = this.slices[index]; -+ if (slices == null) { -+ throw new IllegalStateException(); -+ } -+ -+ this.slices[index] = null; -+ -+ return --this.sliceCount; -+ } -+ -+ public void add(final int index, final ChunkEntitySlices slices) { -+ final ChunkEntitySlices curr = this.slices[index]; -+ if (curr != null) { -+ throw new IllegalStateException(); -+ } -+ -+ this.slices[index] = slices; -+ -+ ++this.sliceCount; -+ } -+ } -+ -+ private final class EntityCallback implements EntityInLevelCallback { -+ -+ public final Entity entity; -+ -+ public EntityCallback(final Entity entity) { -+ this.entity = entity; -+ } -+ -+ @Override -+ public void onMove() { -+ final Entity entity = this.entity; -+ final Visibility oldVisibility = getEntityStatus(entity); -+ final ChunkEntitySlices newSlices = EntityLookup.this.moveEntity(this.entity); -+ if (newSlices == null) { -+ // no new section, so didn't change sections -+ return; -+ } -+ final Visibility newVisibility = getEntityStatus(entity); -+ -+ EntityLookup.this.entityStatusChange(entity, newSlices, oldVisibility, newVisibility, true, false, false); -+ } -+ -+ @Override -+ public void onRemove(final Entity.RemovalReason reason) { -+ final Entity entity = this.entity; -+ TickThread.ensureTickThread(entity, "Cannot remove entity off-main"); // Paper - rewrite chunk system -+ final Visibility tickingState = EntityLookup.getEntityStatus(entity); -+ -+ EntityLookup.this.removeEntity(entity); -+ -+ EntityLookup.this.entityStatusChange(entity, null, tickingState, Visibility.HIDDEN, false, false, reason.shouldDestroy()); -+ -+ this.entity.setLevelCallback(NoOpCallback.INSTANCE); -+ } -+ } -+ -+ private static final class NoOpCallback implements EntityInLevelCallback { -+ -+ public static final NoOpCallback INSTANCE = new NoOpCallback(); -+ -+ @Override -+ public void onMove() {} -+ -+ @Override -+ public void onRemove(final Entity.RemovalReason reason) {} -+ } -+} -diff --git a/src/main/java/io/papermc/paper/chunk/system/io/RegionFileIOThread.java b/src/main/java/io/papermc/paper/chunk/system/io/RegionFileIOThread.java -new file mode 100644 -index 0000000000000000000000000000000000000000..2934f0cf0ef09c84739312b00186c2ef0019a165 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/system/io/RegionFileIOThread.java -@@ -0,0 +1,1343 @@ -+package io.papermc.paper.chunk.system.io; -+ -+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; -+import ca.spottedleaf.concurrentutil.executor.Cancellable; -+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; -+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedQueueExecutorThread; -+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue; -+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; -+import com.mojang.logging.LogUtils; -+import io.papermc.paper.util.CoordinateUtils; -+import io.papermc.paper.util.TickThread; -+import it.unimi.dsi.fastutil.HashCommon; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.chunk.storage.RegionFile; -+import net.minecraft.world.level.chunk.storage.RegionFileStorage; -+import org.slf4j.Logger; -+import java.io.IOException; -+import java.lang.invoke.VarHandle; -+import java.util.concurrent.CompletableFuture; -+import java.util.concurrent.CompletionException; -+import java.util.concurrent.ConcurrentHashMap; -+import java.util.concurrent.atomic.AtomicInteger; -+import java.util.function.BiConsumer; -+import java.util.function.BiFunction; -+import java.util.function.Consumer; -+import java.util.function.Function; -+ -+/** -+ * Prioritised RegionFile I/O executor, responsible for all RegionFile access. -+ *

-+ * All functions provided are MT-Safe, however certain ordering constraints are recommended: -+ *

    -+ *
  • -+ * Chunk saves may not occur for unloaded chunks. -+ *
  • -+ *
  • -+ * Tasks must be scheduled on the chunk scheduler thread. -+ *
  • -+ *
-+ * By following these constraints, no chunk data loss should occur with the exception of underlying I/O problems. -+ */ -+public final class RegionFileIOThread extends PrioritisedQueueExecutorThread { -+ -+ private static final Logger LOGGER = LogUtils.getClassLogger(); -+ -+ /** -+ * The kinds of region files controlled by the region file thread. Add more when needed, and ensure -+ * getControllerFor is updated. -+ */ -+ public static enum RegionFileType { -+ CHUNK_DATA, -+ POI_DATA, -+ ENTITY_DATA; -+ } -+ -+ protected static final RegionFileType[] CACHED_REGIONFILE_TYPES = RegionFileType.values(); -+ -+ private ChunkDataController getControllerFor(final ServerLevel world, final RegionFileType type) { -+ return switch (type) { -+ case CHUNK_DATA -> world.chunkDataControllerNew; -+ case POI_DATA -> world.poiDataControllerNew; -+ case ENTITY_DATA -> world.entityDataControllerNew; -+ default -> throw new IllegalStateException("Unknown controller type " + type); -+ }; -+ } -+ -+ /** -+ * Collects regionfile data for a certain chunk. -+ */ -+ public static final class RegionFileData { -+ -+ private final boolean[] hasResult = new boolean[CACHED_REGIONFILE_TYPES.length]; -+ private final CompoundTag[] data = new CompoundTag[CACHED_REGIONFILE_TYPES.length]; -+ private final Throwable[] throwables = new Throwable[CACHED_REGIONFILE_TYPES.length]; -+ -+ /** -+ * Sets the result associated with the specified regionfile type. Note that -+ * results can only be set once per regionfile type. -+ * -+ * @param type The regionfile type. -+ * @param data The result to set. -+ */ -+ public void setData(final RegionFileType type, final CompoundTag data) { -+ final int index = type.ordinal(); -+ -+ if (this.hasResult[index]) { -+ throw new IllegalArgumentException("Result already exists for type " + type); -+ } -+ this.hasResult[index] = true; -+ this.data[index] = data; -+ } -+ -+ /** -+ * Sets the result associated with the specified regionfile type. Note that -+ * results can only be set once per regionfile type. -+ * -+ * @param type The regionfile type. -+ * @param throwable The result to set. -+ */ -+ public void setThrowable(final RegionFileType type, final Throwable throwable) { -+ final int index = type.ordinal(); -+ -+ if (this.hasResult[index]) { -+ throw new IllegalArgumentException("Result already exists for type " + type); -+ } -+ this.hasResult[index] = true; -+ this.throwables[index] = throwable; -+ } -+ -+ /** -+ * Returns whether there is a result for the specified regionfile type. -+ * -+ * @param type Specified regionfile type. -+ * -+ * @return Whether a result exists for {@code type}. -+ */ -+ public boolean hasResult(final RegionFileType type) { -+ return this.hasResult[type.ordinal()]; -+ } -+ -+ /** -+ * Returns the data result for the regionfile type. -+ * -+ * @param type Specified regionfile type. -+ * -+ * @throws IllegalArgumentException If the result has not been set for {@code type}. -+ * @return The data result for the specified type. If the result is a {@code Throwable}, -+ * then returns {@code null}. -+ */ -+ public CompoundTag getData(final RegionFileType type) { -+ final int index = type.ordinal(); -+ -+ if (!this.hasResult[index]) { -+ throw new IllegalArgumentException("Result does not exist for type " + type); -+ } -+ -+ return this.data[index]; -+ } -+ -+ /** -+ * Returns the throwable result for the regionfile type. -+ * -+ * @param type Specified regionfile type. -+ * -+ * @throws IllegalArgumentException If the result has not been set for {@code type}. -+ * @return The throwable result for the specified type. If the result is an {@code CompoundTag}, -+ * then returns {@code null}. -+ */ -+ public Throwable getThrowable(final RegionFileType type) { -+ final int index = type.ordinal(); -+ -+ if (!this.hasResult[index]) { -+ throw new IllegalArgumentException("Result does not exist for type " + type); -+ } -+ -+ return this.throwables[index]; -+ } -+ } -+ -+ private static final Object INIT_LOCK = new Object(); -+ -+ static RegionFileIOThread[] threads; -+ -+ /* needs to be consistent given a set of parameters */ -+ static RegionFileIOThread selectThread(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { -+ if (threads == null) { -+ throw new IllegalStateException("Threads not initialised"); -+ } -+ -+ final int regionX = chunkX >> 5; -+ final int regionZ = chunkZ >> 5; -+ final int typeOffset = type.ordinal(); -+ -+ return threads[(System.identityHashCode(world) + regionX + regionZ + typeOffset) % threads.length]; -+ } -+ -+ /** -+ * Shuts down the I/O executor(s). Watis for all tasks to complete if specified. -+ * Tasks queued during this call might not be accepted, and tasks queued after will not be accepted. -+ * -+ * @param wait Whether to wait until all tasks have completed. -+ */ -+ public static void close(final boolean wait) { -+ for (int i = 0, len = threads.length; i < len; ++i) { -+ threads[i].close(false, true); -+ } -+ if (wait) { -+ RegionFileIOThread.flush(); -+ } -+ } -+ -+ public static long[] getExecutedTasks() { -+ final long[] ret = new long[threads.length]; -+ for (int i = 0, len = threads.length; i < len; ++i) { -+ ret[i] = threads[i].getTotalTasksExecuted(); -+ } -+ -+ return ret; -+ } -+ -+ public static long[] getTasksScheduled() { -+ final long[] ret = new long[threads.length]; -+ for (int i = 0, len = threads.length; i < len; ++i) { -+ ret[i] = threads[i].getTotalTasksScheduled(); -+ } -+ return ret; -+ } -+ -+ public static void flush() { -+ for (int i = 0, len = threads.length; i < len; ++i) { -+ threads[i].waitUntilAllExecuted(); -+ } -+ } -+ -+ public static void partialFlush(final int totalTasksRemaining) { -+ long failures = 1L; // start out at 0.25ms -+ -+ for (;;) { -+ final long[] executed = getExecutedTasks(); -+ final long[] scheduled = getTasksScheduled(); -+ -+ long sum = 0; -+ for (int i = 0; i < executed.length; ++i) { -+ sum += scheduled[i] - executed[i]; -+ } -+ -+ if (sum <= totalTasksRemaining) { -+ break; -+ } -+ -+ failures = ConcurrentUtil.linearLongBackoff(failures, 250_000L, 5_000_000L); // 500us, 5ms -+ } -+ } -+ -+ /** -+ * Inits the executor with the specified number of threads. -+ * -+ * @param threads Specified number of threads. -+ */ -+ public static void init(final int threads) { -+ synchronized (INIT_LOCK) { -+ if (RegionFileIOThread.threads != null) { -+ throw new IllegalStateException("Already initialised threads"); -+ } -+ -+ RegionFileIOThread.threads = new RegionFileIOThread[threads]; -+ -+ for (int i = 0; i < threads; ++i) { -+ RegionFileIOThread.threads[i] = new RegionFileIOThread(i); -+ RegionFileIOThread.threads[i].start(); -+ } -+ } -+ } -+ -+ private RegionFileIOThread(final int threadNumber) { -+ super(new PrioritisedThreadedTaskQueue(), (int)(1.0e6)); // 1.0ms spinwait time -+ this.setName("RegionFile I/O Thread #" + threadNumber); -+ this.setPriority(Thread.NORM_PRIORITY - 2); // we keep priority close to normal because threads can wait on us -+ this.setUncaughtExceptionHandler((final Thread thread, final Throwable thr) -> { -+ LOGGER.error("Uncaught exception thrown from I/O thread, report this! Thread: " + thread.getName(), thr); -+ }); -+ } -+ -+ /** -+ * Returns whether the current thread is a regionfile I/O executor. -+ * @return Whether the current thread is a regionfile I/O executor. -+ */ -+ public static boolean isRegionFileThread() { -+ return Thread.currentThread() instanceof RegionFileIOThread; -+ } -+ -+ /** -+ * Returns the priority associated with blocking I/O based on the current thread. The goal is to avoid -+ * dumb plugins from taking away priority from threads we consider crucial. -+ * @return The priroity to use with blocking I/O on the current thread. -+ */ -+ public static PrioritisedExecutor.Priority getIOBlockingPriorityForCurrentThread() { -+ if (TickThread.isTickThread()) { -+ return PrioritisedExecutor.Priority.BLOCKING; -+ } -+ return PrioritisedExecutor.Priority.HIGHEST; -+ } -+ -+ /** -+ * Returns the current {@code CompoundTag} pending for write for the specified chunk and regionfile type. -+ * Note that this does not copy the result, so do not modify the result returned. -+ * -+ * @param world Specified world. -+ * @param chunkX Specified chunk x. -+ * @param chunkZ Specified chunk z. -+ * @param type Specified regionfile type. -+ * -+ * @return The compound tag associated for the specified chunk. {@code null} if no write was pending, or if {@code null} is the write pending. -+ */ -+ public static CompoundTag getPendingWrite(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { -+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); -+ return thread.getPendingWriteInternal(world, chunkX, chunkZ, type); -+ } -+ -+ CompoundTag getPendingWriteInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { -+ final ChunkDataController taskController = this.getControllerFor(world, type); -+ final ChunkDataTask task = taskController.tasks.get(Long.valueOf(CoordinateUtils.getChunkKey(chunkX, chunkZ))); -+ -+ if (task == null) { -+ return null; -+ } -+ -+ final CompoundTag ret = task.inProgressWrite; -+ -+ return ret == ChunkDataTask.NOTHING_TO_WRITE ? null : ret; -+ } -+ -+ /** -+ * Returns the priority for the specified regionfile type for the specified chunk. -+ * @param world Specified world. -+ * @param chunkX Specified chunk x. -+ * @param chunkZ Specified chunk z. -+ * @param type Specified regionfile type. -+ * @return The priority for the chunk -+ */ -+ public static PrioritisedExecutor.Priority getPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { -+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); -+ return thread.getPriorityInternal(world, chunkX, chunkZ, type); -+ } -+ -+ PrioritisedExecutor.Priority getPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) { -+ final ChunkDataController taskController = this.getControllerFor(world, type); -+ final ChunkDataTask task = taskController.tasks.get(Long.valueOf(CoordinateUtils.getChunkKey(chunkX, chunkZ))); -+ -+ if (task == null) { -+ return PrioritisedExecutor.Priority.COMPLETING; -+ } -+ -+ return task.prioritisedTask.getPriority(); -+ } -+ -+ /** -+ * Sets the priority for all regionfile types for the specified chunk. Note that great care should -+ * be taken using this method, as there can be multiple tasks tied to the same chunk that want different -+ * priorities. -+ * -+ * @param world Specified world. -+ * @param chunkX Specified chunk x. -+ * @param chunkZ Specified chunk z. -+ * @param priority New priority. -+ * -+ * @see #raisePriority(ServerLevel, int, int, Priority) -+ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) -+ * @see #lowerPriority(ServerLevel, int, int, Priority) -+ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) -+ */ -+ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, -+ final PrioritisedExecutor.Priority priority) { -+ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { -+ RegionFileIOThread.setPriority(world, chunkX, chunkZ, type, priority); -+ } -+ } -+ -+ /** -+ * Sets the priority for the specified regionfile type for the specified chunk. Note that great care should -+ * be taken using this method, as there can be multiple tasks tied to the same chunk that want different -+ * priorities. -+ * -+ * @param world Specified world. -+ * @param chunkX Specified chunk x. -+ * @param chunkZ Specified chunk z. -+ * @param type Specified regionfile type. -+ * @param priority New priority. -+ * -+ * @see #raisePriority(ServerLevel, int, int, Priority) -+ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) -+ * @see #lowerPriority(ServerLevel, int, int, Priority) -+ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) -+ */ -+ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, -+ final PrioritisedExecutor.Priority priority) { -+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); -+ thread.setPriorityInternal(world, chunkX, chunkZ, type, priority); -+ } -+ -+ void setPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, -+ final PrioritisedExecutor.Priority priority) { -+ final ChunkDataController taskController = this.getControllerFor(world, type); -+ final ChunkDataTask task = taskController.tasks.get(Long.valueOf(CoordinateUtils.getChunkKey(chunkX, chunkZ))); -+ -+ if (task != null) { -+ task.prioritisedTask.setPriority(priority); -+ } -+ } -+ -+ /** -+ * Raises the priority for all regionfile types for the specified chunk. -+ * -+ * @param world Specified world. -+ * @param chunkX Specified chunk x. -+ * @param chunkZ Specified chunk z. -+ * @param priority New priority. -+ * -+ * @see #setPriority(ServerLevel, int, int, Priority) -+ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) -+ * @see #lowerPriority(ServerLevel, int, int, Priority) -+ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) -+ */ -+ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, -+ final PrioritisedExecutor.Priority priority) { -+ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { -+ RegionFileIOThread.raisePriority(world, chunkX, chunkZ, type, priority); -+ } -+ } -+ -+ /** -+ * Raises the priority for the specified regionfile type for the specified chunk. -+ * -+ * @param world Specified world. -+ * @param chunkX Specified chunk x. -+ * @param chunkZ Specified chunk z. -+ * @param type Specified regionfile type. -+ * @param priority New priority. -+ * -+ * @see #setPriority(ServerLevel, int, int, Priority) -+ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) -+ * @see #lowerPriority(ServerLevel, int, int, Priority) -+ * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority) -+ */ -+ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, -+ final PrioritisedExecutor.Priority priority) { -+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); -+ thread.raisePriorityInternal(world, chunkX, chunkZ, type, priority); -+ } -+ -+ void raisePriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, -+ final PrioritisedExecutor.Priority priority) { -+ final ChunkDataController taskController = this.getControllerFor(world, type); -+ final ChunkDataTask task = taskController.tasks.get(Long.valueOf(CoordinateUtils.getChunkKey(chunkX, chunkZ))); -+ -+ if (task != null) { -+ task.prioritisedTask.raisePriority(priority); -+ } -+ } -+ -+ /** -+ * Lowers the priority for all regionfile types for the specified chunk. -+ * -+ * @param world Specified world. -+ * @param chunkX Specified chunk x. -+ * @param chunkZ Specified chunk z. -+ * @param priority New priority. -+ * -+ * @see #raisePriority(ServerLevel, int, int, Priority) -+ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) -+ * @see #setPriority(ServerLevel, int, int, Priority) -+ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) -+ */ -+ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, -+ final PrioritisedExecutor.Priority priority) { -+ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) { -+ RegionFileIOThread.lowerPriority(world, chunkX, chunkZ, type, priority); -+ } -+ } -+ -+ /** -+ * Lowers the priority for the specified regionfile type for the specified chunk. -+ * -+ * @param world Specified world. -+ * @param chunkX Specified chunk x. -+ * @param chunkZ Specified chunk z. -+ * @param type Specified regionfile type. -+ * @param priority New priority. -+ * -+ * @see #raisePriority(ServerLevel, int, int, Priority) -+ * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority) -+ * @see #setPriority(ServerLevel, int, int, Priority) -+ * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority) -+ */ -+ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, -+ final PrioritisedExecutor.Priority priority) { -+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); -+ thread.lowerPriorityInternal(world, chunkX, chunkZ, type, priority); -+ } -+ -+ void lowerPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, -+ final PrioritisedExecutor.Priority priority) { -+ final ChunkDataController taskController = this.getControllerFor(world, type); -+ final ChunkDataTask task = taskController.tasks.get(Long.valueOf(CoordinateUtils.getChunkKey(chunkX, chunkZ))); -+ -+ if (task != null) { -+ task.prioritisedTask.lowerPriority(priority); -+ } -+ } -+ -+ /** -+ * Schedules the chunk data to be written asynchronously. -+ *

-+ * Impl notes: -+ *

    -+ *
  • -+ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means -+ * saves must be scheduled before a chunk is unloaded. -+ *
  • -+ *
  • -+ * Writes may be called concurrently, although only the "later" write will go through. -+ *
  • -+ *
-+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param data Chunk's data -+ * @param type The regionfile type to write to. -+ * -+ * @throws IllegalStateException If the file io thread has shutdown. -+ */ -+ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, -+ final RegionFileType type) { -+ RegionFileIOThread.scheduleSave(world, chunkX, chunkZ, data, type, PrioritisedExecutor.Priority.NORMAL); -+ } -+ -+ /** -+ * Schedules the chunk data to be written asynchronously. -+ *

-+ * Impl notes: -+ *

    -+ *
  • -+ * This function presumes a chunk load for the coordinates is not called during this function (anytime after is OK). This means -+ * saves must be scheduled before a chunk is unloaded. -+ *
  • -+ *
  • -+ * Writes may be called concurrently, although only the "later" write will go through. -+ *
  • -+ *
-+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param data Chunk's data -+ * @param type The regionfile type to write to. -+ * @param priority The minimum priority to schedule at. -+ * -+ * @throws IllegalStateException If the file io thread has shutdown. -+ */ -+ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, -+ final RegionFileType type, final PrioritisedExecutor.Priority priority) { -+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); -+ thread.scheduleSaveInternal(world, chunkX, chunkZ, data, type, priority); -+ } -+ -+ void scheduleSaveInternal(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data, -+ final RegionFileType type, final PrioritisedExecutor.Priority priority) { -+ final ChunkDataController taskController = this.getControllerFor(world, type); -+ -+ final boolean[] created = new boolean[1]; -+ final ChunkCoordinate key = new ChunkCoordinate(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ final ChunkDataTask task = taskController.tasks.compute(key, (final ChunkCoordinate keyInMap, final ChunkDataTask taskRunning) -> { -+ if (taskRunning == null || taskRunning.failedWrite) { -+ // no task is scheduled or the previous write failed - meaning we need to overwrite it -+ -+ // create task -+ final ChunkDataTask newTask = new ChunkDataTask(world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority); -+ newTask.inProgressWrite = data; -+ created[0] = true; -+ -+ return newTask; -+ } -+ -+ taskRunning.inProgressWrite = data; -+ -+ return taskRunning; -+ }); -+ -+ if (created[0]) { -+ task.prioritisedTask.queue(); -+ } else { -+ task.prioritisedTask.raisePriority(priority); -+ } -+ } -+ -+ /** -+ * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call -+ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)} -+ * for single load. -+ *

-+ * Impl notes: -+ *

    -+ *
  • -+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may -+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of -+ * data is undefined behaviour, and can cause deadlock. -+ *
  • -+ *
-+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param onComplete Consumer to execute once this task has completed -+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost -+ * of this call. -+ * -+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. -+ * -+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) -+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) -+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) -+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) -+ */ -+ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ, -+ final Consumer onComplete, final boolean intendingToBlock) { -+ return RegionFileIOThread.loadAllChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, PrioritisedExecutor.Priority.NORMAL); -+ } -+ -+ /** -+ * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call -+ * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)} -+ * for single load. -+ *

-+ * Impl notes: -+ *

    -+ *
  • -+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may -+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of -+ * data is undefined behaviour, and can cause deadlock. -+ *
  • -+ *
-+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param onComplete Consumer to execute once this task has completed -+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost -+ * of this call. -+ * @param priority The minimum priority to load the data at. -+ * -+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. -+ * -+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) -+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) -+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) -+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) -+ */ -+ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ, -+ final Consumer onComplete, final boolean intendingToBlock, -+ final PrioritisedExecutor.Priority priority) { -+ return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, priority, CACHED_REGIONFILE_TYPES); -+ } -+ -+ /** -+ * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and -+ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)} -+ * for single load. -+ *

-+ * Impl notes: -+ *

    -+ *
  • -+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may -+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of -+ * data is undefined behaviour, and can cause deadlock. -+ *
  • -+ *
-+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param onComplete Consumer to execute once this task has completed -+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost -+ * of this call. -+ * @param types The regionfile type(s) to load. -+ * -+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. -+ * -+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) -+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) -+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) -+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) -+ */ -+ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ, -+ final Consumer onComplete, final boolean intendingToBlock, -+ final RegionFileType... types) { -+ return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, PrioritisedExecutor.Priority.NORMAL, types); -+ } -+ -+ /** -+ * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and -+ * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)} -+ * for single load. -+ *

-+ * Impl notes: -+ *

    -+ *
  • -+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may -+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of -+ * data is undefined behaviour, and can cause deadlock. -+ *
  • -+ *
-+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param onComplete Consumer to execute once this task has completed -+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost -+ * of this call. -+ * @param types The regionfile type(s) to load. -+ * @param priority The minimum priority to load the data at. -+ * -+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. -+ * -+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean) -+ * @see #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority) -+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) -+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) -+ */ -+ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ, -+ final Consumer onComplete, final boolean intendingToBlock, -+ final PrioritisedExecutor.Priority priority, final RegionFileType... types) { -+ if (types == null) { -+ throw new NullPointerException("Types cannot be null"); -+ } -+ if (types.length == 0) { -+ throw new IllegalArgumentException("Types cannot be empty"); -+ } -+ -+ final RegionFileData ret = new RegionFileData(); -+ -+ final Cancellable[] reads = new CancellableRead[types.length]; -+ final AtomicInteger completions = new AtomicInteger(); -+ final int expectedCompletions = types.length; -+ -+ for (int i = 0; i < expectedCompletions; ++i) { -+ final RegionFileType type = types[i]; -+ reads[i] = RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, -+ (final CompoundTag data, final Throwable throwable) -> { -+ if (throwable != null) { -+ ret.setThrowable(type, throwable); -+ } else { -+ ret.setData(type, data); -+ } -+ -+ if (completions.incrementAndGet() == expectedCompletions) { -+ onComplete.accept(ret); -+ } -+ }, intendingToBlock, priority); -+ } -+ -+ return new CancellableReads(reads); -+ } -+ -+ /** -+ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call -+ * {@code onComplete}. -+ *

-+ * Impl notes: -+ *

    -+ *
  • -+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may -+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of -+ * data is undefined behaviour, and can cause deadlock. -+ *
  • -+ *
-+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param onComplete Consumer to execute once this task has completed -+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost -+ * of this call. -+ * -+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. -+ * -+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) -+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) -+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) -+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) -+ */ -+ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ, -+ final RegionFileType type, final BiConsumer onComplete, -+ final boolean intendingToBlock) { -+ return RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, onComplete, intendingToBlock, PrioritisedExecutor.Priority.NORMAL); -+ } -+ -+ /** -+ * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call -+ * {@code onComplete}. -+ *

-+ * Impl notes: -+ *

    -+ *
  • -+ * The {@code onComplete} parameter may be completed during the execution of this function synchronously or it may -+ * be completed asynchronously on this file io thread. Interacting with the file IO thread in the completion of -+ * data is undefined behaviour, and can cause deadlock. -+ *
  • -+ *
-+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param onComplete Consumer to execute once this task has completed -+ * @param intendingToBlock Whether the caller is intending to block on completion. This only affects the cost -+ * of this call. -+ * @param priority Minimum priority to load the data at. -+ * -+ * @return The {@link Cancellable} for this chunk load. Cancelling it will not affect other loads for the same chunk data. -+ * -+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, RegionFileType...) -+ * @see #loadChunkData(ServerLevel, int, int, Consumer, boolean, Priority, RegionFileType...) -+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean) -+ * @see #loadAllChunkData(ServerLevel, int, int, Consumer, boolean, Priority) -+ */ -+ public static Cancellable loadDataAsync(final ServerLevel world, final int chunkX, final int chunkZ, -+ final RegionFileType type, final BiConsumer onComplete, -+ final boolean intendingToBlock, final PrioritisedExecutor.Priority priority) { -+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type); -+ return thread.loadDataAsyncInternal(world, chunkX, chunkZ, type, onComplete, intendingToBlock, priority); -+ } -+ -+ private static Boolean doesRegionFileExist(final int chunkX, final int chunkZ, final boolean intendingToBlock, -+ final ChunkDataController taskController) { -+ final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); -+ if (intendingToBlock) { -+ return taskController.computeForRegionFile(chunkX, chunkZ, true, (final RegionFile file) -> { -+ if (file == null) { // null if no regionfile exists -+ return Boolean.FALSE; -+ } -+ -+ return file.hasChunk(chunkPos) ? Boolean.TRUE : Boolean.FALSE; -+ }); -+ } else { -+ // first check if the region file for sure does not exist -+ if (taskController.doesRegionFileNotExist(chunkX, chunkZ)) { -+ return Boolean.FALSE; -+ } // else: it either exists or is not known, fall back to checking the loaded region file -+ -+ return taskController.computeForRegionFileIfLoaded(chunkX, chunkZ, (final RegionFile file) -> { -+ if (file == null) { // null if not loaded -+ // not sure at this point, let the I/O thread figure it out -+ return Boolean.TRUE; -+ } -+ -+ return file.hasChunk(chunkPos) ? Boolean.TRUE : Boolean.FALSE; -+ }); -+ } -+ } -+ -+ Cancellable loadDataAsyncInternal(final ServerLevel world, final int chunkX, final int chunkZ, -+ final RegionFileType type, final BiConsumer onComplete, -+ final boolean intendingToBlock, final PrioritisedExecutor.Priority priority) { -+ final ChunkDataController taskController = this.getControllerFor(world, type); -+ -+ final ImmediateCallbackCompletion callbackInfo = new ImmediateCallbackCompletion(); -+ -+ final ChunkCoordinate key = new ChunkCoordinate(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ final BiFunction compute = (final ChunkCoordinate keyInMap, final ChunkDataTask running) -> { -+ if (running == null) { -+ // not scheduled -+ -+ if (callbackInfo.regionFileCalculation == null) { -+ // caller will compute this outside of compute(), to avoid holding the bin lock -+ callbackInfo.needsRegionFileTest = true; -+ return null; -+ } -+ -+ if (callbackInfo.regionFileCalculation == Boolean.FALSE) { -+ // not on disk -+ callbackInfo.data = null; -+ callbackInfo.throwable = null; -+ callbackInfo.completeNow = true; -+ return null; -+ } -+ -+ // set up task -+ final ChunkDataTask newTask = new ChunkDataTask( -+ world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority -+ ); -+ newTask.inProgressRead = new RegionFileIOThread.InProgressRead(); -+ newTask.inProgressRead.waiters.add(onComplete); -+ -+ callbackInfo.tasksNeedsScheduling = true; -+ return newTask; -+ } -+ -+ final CompoundTag pendingWrite = running.inProgressWrite; -+ -+ if (pendingWrite == ChunkDataTask.NOTHING_TO_WRITE) { -+ // need to add to waiters here, because the regionfile thread will use compute() to lock and check for cancellations -+ if (!running.inProgressRead.addToWaiters(onComplete)) { -+ callbackInfo.data = running.inProgressRead.value; -+ callbackInfo.throwable = running.inProgressRead.throwable; -+ callbackInfo.completeNow = true; -+ } -+ return running; -+ } -+ // using the result sync here - don't bump priority -+ -+ // at this stage we have to use the in progress write's data to avoid an order issue -+ callbackInfo.data = pendingWrite; -+ callbackInfo.throwable = null; -+ callbackInfo.completeNow = true; -+ return running; -+ }; -+ -+ ChunkDataTask curr = taskController.tasks.get(key); -+ if (curr == null) { -+ callbackInfo.regionFileCalculation = doesRegionFileExist(chunkX, chunkZ, intendingToBlock, taskController); -+ } -+ ChunkDataTask ret = taskController.tasks.compute(key, compute); -+ if (callbackInfo.needsRegionFileTest) { -+ // curr isn't null but when we went into compute() it was -+ callbackInfo.regionFileCalculation = doesRegionFileExist(chunkX, chunkZ, intendingToBlock, taskController); -+ // now it should be fine -+ ret = taskController.tasks.compute(key, compute); -+ } -+ -+ // needs to be scheduled -+ if (callbackInfo.tasksNeedsScheduling) { -+ ret.prioritisedTask.queue(); -+ } else if (callbackInfo.completeNow) { -+ try { -+ onComplete.accept(callbackInfo.data, callbackInfo.throwable); -+ } catch (final ThreadDeath thr) { -+ throw thr; -+ } catch (final Throwable thr) { -+ LOGGER.error("Callback " + ConcurrentUtil.genericToString(onComplete) + " synchronously failed to handle chunk data for task " + ret.toString(), thr); -+ } -+ } else { -+ // we're waiting on a task we didn't schedule, so raise its priority to what we want -+ ret.prioritisedTask.raisePriority(priority); -+ } -+ -+ return new CancellableRead(onComplete, ret); -+ } -+ -+ /** -+ * Schedules a load task to be executed asynchronously, and blocks on that task. -+ * -+ * @param world Chunk's world -+ * @param chunkX Chunk's x coordinate -+ * @param chunkZ Chunk's z coordinate -+ * @param type Regionfile type -+ * @param priority Minimum priority to load the data at. -+ * -+ * @return The chunk data for the chunk. Note that a {@code null} result means the chunk or regionfile does not exist on disk. -+ * -+ * @throws IOException If the load fails for any reason -+ */ -+ public static CompoundTag loadData(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type, -+ final PrioritisedExecutor.Priority priority) throws IOException { -+ final CompletableFuture ret = new CompletableFuture<>(); -+ -+ RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, (final CompoundTag compound, final Throwable thr) -> { -+ if (thr != null) { -+ ret.completeExceptionally(thr); -+ } else { -+ ret.complete(compound); -+ } -+ }, true, priority); -+ -+ try { -+ return ret.join(); -+ } catch (final CompletionException ex) { -+ throw new IOException(ex); -+ } -+ } -+ -+ private static final class ImmediateCallbackCompletion { -+ -+ public CompoundTag data; -+ public Throwable throwable; -+ public boolean completeNow; -+ public boolean tasksNeedsScheduling; -+ public boolean needsRegionFileTest; -+ public Boolean regionFileCalculation; -+ -+ } -+ -+ static final class CancellableRead implements Cancellable { -+ -+ private BiConsumer callback; -+ private RegionFileIOThread.ChunkDataTask task; -+ -+ CancellableRead(final BiConsumer callback, final RegionFileIOThread.ChunkDataTask task) { -+ this.callback = callback; -+ this.task = task; -+ } -+ -+ @Override -+ public boolean cancel() { -+ final BiConsumer callback = this.callback; -+ final RegionFileIOThread.ChunkDataTask task = this.task; -+ -+ if (callback == null || task == null) { -+ return false; -+ } -+ -+ this.callback = null; -+ this.task = null; -+ -+ final RegionFileIOThread.InProgressRead read = task.inProgressRead; -+ -+ // read can be null if no read was scheduled (i.e no regionfile existed or chunk in regionfile didn't) -+ return (read != null && read.waiters.remove(callback)); -+ } -+ } -+ -+ static final class CancellableReads implements Cancellable { -+ -+ private Cancellable[] reads; -+ -+ protected static final VarHandle READS_HANDLE = ConcurrentUtil.getVarHandle(CancellableReads.class, "reads", Cancellable[].class); -+ -+ CancellableReads(final Cancellable[] reads) { -+ this.reads = reads; -+ } -+ -+ @Override -+ public boolean cancel() { -+ final Cancellable[] reads = (Cancellable[])READS_HANDLE.getAndSet((CancellableReads)this, (Cancellable[])null); -+ -+ if (reads == null) { -+ return false; -+ } -+ -+ boolean ret = false; -+ -+ for (final Cancellable read : reads) { -+ ret |= read.cancel(); -+ } -+ -+ return ret; -+ } -+ } -+ -+ static final class InProgressRead { -+ -+ private static final Logger LOGGER = LogUtils.getClassLogger(); -+ -+ CompoundTag value; -+ Throwable throwable; -+ final MultiThreadedQueue> waiters = new MultiThreadedQueue<>(); -+ -+ // rets false if already completed (callback not invoked), true if callback was added -+ boolean addToWaiters(final BiConsumer callback) { -+ return this.waiters.add(callback); -+ } -+ -+ void complete(final RegionFileIOThread.ChunkDataTask task, final CompoundTag value, final Throwable throwable) { -+ this.value = value; -+ this.throwable = throwable; -+ -+ BiConsumer consumer; -+ while ((consumer = this.waiters.pollOrBlockAdds()) != null) { -+ try { -+ consumer.accept(value, throwable); -+ } catch (final ThreadDeath thr) { -+ throw thr; -+ } catch (final Throwable thr) { -+ LOGGER.error("Callback " + ConcurrentUtil.genericToString(consumer) + " failed to handle chunk data for task " + task.toString(), thr); -+ } -+ } -+ } -+ } -+ -+ /** -+ * Class exists to replace {@link Long} usages as keys inside non-fastutil hashtables. The hash for some Long {@code x} -+ * is defined as {@code (x >>> 32) ^ x}. Chunk keys as long values are defined as {@code ((chunkX & 0xFFFFFFFFL) | (chunkZ << 32))}, -+ * which means the hashcode as a Long value will be {@code chunkX ^ chunkZ}. Given that most chunks are created within a radius arounds players, -+ * this will lead to many hash collisions. So, this class uses a better hashing algorithm so that usage of -+ * non-fastutil collections is not degraded. -+ */ -+ public static final class ChunkCoordinate implements Comparable { -+ -+ public final long key; -+ -+ public ChunkCoordinate(final long key) { -+ this.key = key; -+ } -+ -+ @Override -+ public int hashCode() { -+ return (int)HashCommon.mix(this.key); -+ } -+ -+ @Override -+ public boolean equals(final Object obj) { -+ if (this == obj) { -+ return true; -+ } -+ -+ if (!(obj instanceof ChunkCoordinate)) { -+ return false; -+ } -+ -+ final ChunkCoordinate other = (ChunkCoordinate)obj; -+ -+ return this.key == other.key; -+ } -+ -+ // This class is intended for HashMap/ConcurrentHashMap usage, which do treeify bin nodes if the chain -+ // is too large. So we should implement compareTo to help. -+ @Override -+ public int compareTo(final RegionFileIOThread.ChunkCoordinate other) { -+ return Long.compare(this.key, other.key); -+ } -+ -+ @Override -+ public String toString() { -+ return new ChunkPos(this.key).toString(); -+ } -+ } -+ -+ public static abstract class ChunkDataController { -+ -+ // ConcurrentHashMap synchronizes per chain, so reduce the chance of task's hashes colliding. -+ protected final ConcurrentHashMap tasks = new ConcurrentHashMap<>(8192, 0.10f); -+ -+ public final RegionFileType type; -+ -+ public ChunkDataController(final RegionFileType type) { -+ this.type = type; -+ } -+ -+ public abstract RegionFileStorage getCache(); -+ -+ public abstract void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException; -+ -+ public abstract CompoundTag readData(final int chunkX, final int chunkZ) throws IOException; -+ -+ public boolean hasTasks() { -+ return !this.tasks.isEmpty(); -+ } -+ -+ public boolean doesRegionFileNotExist(final int chunkX, final int chunkZ) { -+ return this.getCache().doesRegionFileNotExistNoIO(new ChunkPos(chunkX, chunkZ)); -+ } -+ -+ public T computeForRegionFile(final int chunkX, final int chunkZ, final boolean existingOnly, final Function function) { -+ final RegionFileStorage cache = this.getCache(); -+ final RegionFile regionFile; -+ synchronized (cache) { -+ try { -+ regionFile = cache.getRegionFile(new ChunkPos(chunkX, chunkZ), existingOnly, true); -+ } catch (final IOException ex) { -+ throw new RuntimeException(ex); -+ } -+ } -+ -+ try { -+ return function.apply(regionFile); -+ } finally { -+ if (regionFile != null) { -+ regionFile.fileLock.unlock(); -+ } -+ } -+ } -+ -+ public T computeForRegionFileIfLoaded(final int chunkX, final int chunkZ, final Function function) { -+ final RegionFileStorage cache = this.getCache(); -+ final RegionFile regionFile; -+ -+ synchronized (cache) { -+ regionFile = cache.getRegionFileIfLoaded(new ChunkPos(chunkX, chunkZ)); -+ if (regionFile != null) { -+ regionFile.fileLock.lock(); -+ } -+ } -+ -+ try { -+ return function.apply(regionFile); -+ } finally { -+ if (regionFile != null) { -+ regionFile.fileLock.unlock(); -+ } -+ } -+ } -+ } -+ -+ static final class ChunkDataTask implements Runnable { -+ -+ protected static final CompoundTag NOTHING_TO_WRITE = new CompoundTag(); -+ -+ private static final Logger LOGGER = LogUtils.getClassLogger(); -+ -+ RegionFileIOThread.InProgressRead inProgressRead; -+ volatile CompoundTag inProgressWrite = NOTHING_TO_WRITE; // only needs to be acquire/release -+ -+ boolean failedWrite; -+ -+ final ServerLevel world; -+ final int chunkX; -+ final int chunkZ; -+ final RegionFileIOThread.ChunkDataController taskController; -+ -+ final PrioritisedExecutor.PrioritisedTask prioritisedTask; -+ -+ /* -+ * IO thread will perform reads before writes for a given chunk x and z -+ * -+ * How reads/writes are scheduled: -+ * -+ * If read is scheduled while scheduling write, take no special action and just schedule write -+ * If read is scheduled while scheduling read and no write is scheduled, chain the read task -+ * -+ * -+ * If write is scheduled while scheduling read, use the pending write data and ret immediately (so no read is scheduled) -+ * If write is scheduled while scheduling write (ignore read in progress), overwrite the write in progress data -+ * -+ * This allows the reads and writes to act as if they occur synchronously to the thread scheduling them, however -+ * it fails to properly propagate write failures thanks to writes overwriting each other -+ */ -+ -+ public ChunkDataTask(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileIOThread.ChunkDataController taskController, -+ final PrioritisedExecutor executor, final PrioritisedExecutor.Priority priority) { -+ this.world = world; -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ this.taskController = taskController; -+ this.prioritisedTask = executor.createTask(this, priority); -+ } -+ -+ @Override -+ public String toString() { -+ return "Task for world: '" + this.world.getWorld().getName() + "' at (" + this.chunkX + "," + this.chunkZ + -+ ") type: " + this.taskController.type.name() + ", hash: " + this.hashCode(); -+ } -+ -+ @Override -+ public void run() { -+ final RegionFileIOThread.InProgressRead read = this.inProgressRead; -+ final ChunkCoordinate chunkKey = new ChunkCoordinate(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ)); -+ -+ if (read != null) { -+ final boolean[] canRead = new boolean[] { true }; -+ -+ if (read.waiters.isEmpty()) { -+ // cancelled read? go to task controller to confirm -+ final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final ChunkCoordinate keyInMap, final ChunkDataTask valueInMap) -> { -+ if (valueInMap == null) { -+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!"); -+ } -+ if (valueInMap != ChunkDataTask.this) { -+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); -+ } -+ -+ if (!read.waiters.isEmpty()) { // as per usual IntelliJ is unable to figure out that there are concurrent accesses. -+ return valueInMap; -+ } else { -+ canRead[0] = false; -+ } -+ -+ return valueInMap.inProgressWrite == NOTHING_TO_WRITE ? null : valueInMap; -+ }); -+ -+ if (inMap == null) { -+ // read is cancelled - and no write pending, so we're done -+ return; -+ } -+ // if there is a write in progress, we don't actually have to worry about waiters gaining new entries - -+ // the readers will just use the in progress write, so the value in canRead is good to use without -+ // further synchronisation. -+ } -+ -+ if (canRead[0]) { -+ CompoundTag compound = null; -+ Throwable throwable = null; -+ -+ try { -+ compound = this.taskController.readData(this.chunkX, this.chunkZ); -+ } catch (final ThreadDeath thr) { -+ throw thr; -+ } catch (final Throwable thr) { -+ throwable = thr; -+ LOGGER.error("Failed to read chunk data for task: " + this.toString(), thr); -+ } -+ read.complete(this, compound, throwable); -+ } -+ } -+ -+ CompoundTag write = this.inProgressWrite; -+ -+ if (write == NOTHING_TO_WRITE) { -+ final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final ChunkCoordinate keyInMap, final ChunkDataTask valueInMap) -> { -+ if (valueInMap == null) { -+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!"); -+ } -+ if (valueInMap != ChunkDataTask.this) { -+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); -+ } -+ return valueInMap.inProgressWrite == NOTHING_TO_WRITE ? null : valueInMap; -+ }); -+ -+ if (inMap == null) { -+ return; // set the task value to null, indicating we're done -+ } // else: inProgressWrite changed, so now we have something to write -+ } -+ -+ for (;;) { -+ write = this.inProgressWrite; -+ final CompoundTag dataWritten = write; -+ -+ boolean failedWrite = false; -+ -+ try { -+ this.taskController.writeData(this.chunkX, this.chunkZ, write); -+ } catch (final ThreadDeath thr) { -+ throw thr; -+ } catch (final Throwable thr) { -+ if (thr instanceof RegionFileStorage.RegionFileSizeException) { -+ final int maxSize = RegionFile.MAX_CHUNK_SIZE / (1024 * 1024); -+ LOGGER.error("Chunk at (" + this.chunkX + "," + this.chunkZ + ") in '" + this.world.getWorld().getName() + "' exceeds max size of " + maxSize + "MiB, it has been deleted from disk."); -+ } else { -+ failedWrite = thr instanceof IOException; -+ LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr); -+ } -+ } -+ -+ final boolean finalFailWrite = failedWrite; -+ final boolean[] done = new boolean[] { false }; -+ -+ this.taskController.tasks.compute(chunkKey, (final ChunkCoordinate keyInMap, final ChunkDataTask valueInMap) -> { -+ if (valueInMap == null) { -+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!"); -+ } -+ if (valueInMap != ChunkDataTask.this) { -+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!"); -+ } -+ if (valueInMap.inProgressWrite == dataWritten) { -+ valueInMap.failedWrite = finalFailWrite; -+ done[0] = true; -+ // keep the data in map if we failed the write so we can try to prevent data loss -+ return finalFailWrite ? valueInMap : null; -+ } -+ // different data than expected, means we need to retry write -+ return valueInMap; -+ }); -+ -+ if (done[0]) { -+ return; -+ } -+ -+ // fetch & write new data -+ continue; -+ } -+ } -+ } -+} -diff --git a/src/main/java/io/papermc/paper/chunk/system/light/LightQueue.java b/src/main/java/io/papermc/paper/chunk/system/light/LightQueue.java -new file mode 100644 -index 0000000000000000000000000000000000000000..69e9944358951bd69ff5e8b3482da1a5e4476209 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/system/light/LightQueue.java -@@ -0,0 +1,283 @@ -+package io.papermc.paper.chunk.system.light; -+ -+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; -+import ca.spottedleaf.starlight.common.light.BlockStarLightEngine; -+import ca.spottedleaf.starlight.common.light.SkyStarLightEngine; -+import ca.spottedleaf.starlight.common.light.StarLightInterface; -+import io.papermc.paper.util.CoordinateUtils; -+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -+import it.unimi.dsi.fastutil.shorts.ShortCollection; -+import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.SectionPos; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.chunk.status.ChunkStatus; -+import java.util.ArrayList; -+import java.util.HashSet; -+import java.util.List; -+import java.util.Set; -+import java.util.concurrent.CompletableFuture; -+import java.util.function.BooleanSupplier; -+ -+public final class LightQueue { -+ -+ protected final Long2ObjectOpenHashMap chunkTasks = new Long2ObjectOpenHashMap<>(); -+ protected final StarLightInterface manager; -+ protected final ServerLevel world; -+ -+ public LightQueue(final StarLightInterface manager) { -+ this.manager = manager; -+ this.world = ((ServerLevel)manager.getWorld()); -+ } -+ -+ public void lowerPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) { -+ final ChunkTasks task; -+ synchronized (this) { -+ task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ } -+ if (task != null) { -+ task.lowerPriority(priority); -+ } -+ } -+ -+ public void setPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) { -+ final ChunkTasks task; -+ synchronized (this) { -+ task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ } -+ if (task != null) { -+ task.setPriority(priority); -+ } -+ } -+ -+ public void raisePriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) { -+ final ChunkTasks task; -+ synchronized (this) { -+ task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ } -+ if (task != null) { -+ task.raisePriority(priority); -+ } -+ } -+ -+ public PrioritisedExecutor.Priority getPriority(final int chunkX, final int chunkZ) { -+ final ChunkTasks task; -+ synchronized (this) { -+ task = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ } -+ if (task != null) { -+ return task.getPriority(); -+ } -+ -+ return PrioritisedExecutor.Priority.COMPLETING; -+ } -+ -+ public boolean isEmpty() { -+ synchronized (this) { -+ return this.chunkTasks.isEmpty(); -+ } -+ } -+ -+ public ChunkTasks queueBlockChange(final BlockPos pos) { -+ final ChunkTasks tasks; -+ synchronized (this) { -+ tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { -+ return new ChunkTasks(keyInMap, LightQueue.this.manager, LightQueue.this); -+ }); -+ tasks.changedPositions.add(pos.immutable()); -+ } -+ -+ tasks.schedule(); -+ -+ return tasks; -+ } -+ -+ public ChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) { -+ final ChunkTasks tasks; -+ synchronized (this) { -+ tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { -+ return new ChunkTasks(keyInMap, LightQueue.this.manager, LightQueue.this); -+ }); -+ -+ if (tasks.changedSectionSet == null) { -+ tasks.changedSectionSet = new Boolean[this.manager.maxSection - this.manager.minSection + 1]; -+ } -+ tasks.changedSectionSet[pos.getY() - this.manager.minSection] = Boolean.valueOf(newEmptyValue); -+ } -+ -+ tasks.schedule(); -+ -+ return tasks; -+ } -+ -+ public ChunkTasks queueChunkLightTask(final ChunkPos pos, final BooleanSupplier lightTask, final PrioritisedExecutor.Priority priority) { -+ final ChunkTasks tasks; -+ synchronized (this) { -+ tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { -+ return new ChunkTasks(keyInMap, LightQueue.this.manager, LightQueue.this, priority); -+ }); -+ if (tasks.lightTasks == null) { -+ tasks.lightTasks = new ArrayList<>(); -+ } -+ tasks.lightTasks.add(lightTask); -+ } -+ -+ tasks.schedule(); -+ -+ return tasks; -+ } -+ -+ public ChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) { -+ final ChunkTasks tasks; -+ synchronized (this) { -+ tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { -+ return new ChunkTasks(keyInMap, LightQueue.this.manager, LightQueue.this); -+ }); -+ -+ ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksSky; -+ if (queuedEdges == null) { -+ queuedEdges = tasks.queuedEdgeChecksSky = new ShortOpenHashSet(); -+ } -+ queuedEdges.addAll(sections); -+ } -+ -+ tasks.schedule(); -+ -+ return tasks; -+ } -+ -+ public ChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) { -+ final ChunkTasks tasks; -+ -+ synchronized (this) { -+ tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { -+ return new ChunkTasks(keyInMap, LightQueue.this.manager, LightQueue.this); -+ }); -+ -+ ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksBlock; -+ if (queuedEdges == null) { -+ queuedEdges = tasks.queuedEdgeChecksBlock = new ShortOpenHashSet(); -+ } -+ queuedEdges.addAll(sections); -+ } -+ -+ tasks.schedule(); -+ -+ return tasks; -+ } -+ -+ public void removeChunk(final ChunkPos pos) { -+ final ChunkTasks tasks; -+ synchronized (this) { -+ tasks = this.chunkTasks.remove(CoordinateUtils.getChunkKey(pos)); -+ } -+ if (tasks != null && tasks.cancel()) { -+ tasks.onComplete.complete(null); -+ } -+ } -+ -+ public static final class ChunkTasks implements Runnable { -+ -+ public final CompletableFuture onComplete = new CompletableFuture<>(); -+ public boolean isTicketAdded; -+ public final long chunkCoordinate; -+ -+ private final StarLightInterface lightEngine; -+ private final LightQueue queue; -+ private final PrioritisedExecutor.PrioritisedTask task; -+ private final Set changedPositions = new HashSet<>(); -+ private Boolean[] changedSectionSet; -+ private ShortOpenHashSet queuedEdgeChecksSky; -+ private ShortOpenHashSet queuedEdgeChecksBlock; -+ private List lightTasks; -+ -+ public ChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final LightQueue queue) { -+ this(chunkCoordinate, lightEngine, queue, PrioritisedExecutor.Priority.NORMAL); -+ } -+ -+ public ChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine, final LightQueue queue, -+ final PrioritisedExecutor.Priority priority) { -+ this.chunkCoordinate = chunkCoordinate; -+ this.lightEngine = lightEngine; -+ this.queue = queue; -+ this.task = queue.world.chunkTaskScheduler.radiusAwareScheduler.createTask( -+ CoordinateUtils.getChunkX(chunkCoordinate), CoordinateUtils.getChunkZ(chunkCoordinate), -+ ChunkStatus.LIGHT.writeRadius, this, priority -+ ); -+ } -+ -+ public void schedule() { -+ this.task.queue(); -+ } -+ -+ public boolean cancel() { -+ return this.task.cancel(); -+ } -+ -+ public PrioritisedExecutor.Priority getPriority() { -+ return this.task.getPriority(); -+ } -+ -+ public void lowerPriority(final PrioritisedExecutor.Priority priority) { -+ this.task.lowerPriority(priority); -+ } -+ -+ public void setPriority(final PrioritisedExecutor.Priority priority) { -+ this.task.setPriority(priority); -+ } -+ -+ public void raisePriority(final PrioritisedExecutor.Priority priority) { -+ this.task.raisePriority(priority); -+ } -+ -+ @Override -+ public void run() { -+ synchronized (this.queue) { -+ this.queue.chunkTasks.remove(this.chunkCoordinate); -+ } -+ -+ boolean litChunk = false; -+ if (this.lightTasks != null) { -+ for (final BooleanSupplier run : this.lightTasks) { -+ if (run.getAsBoolean()) { -+ litChunk = true; -+ break; -+ } -+ } -+ } -+ -+ final SkyStarLightEngine skyEngine = this.lightEngine.getSkyLightEngine(); -+ final BlockStarLightEngine blockEngine = this.lightEngine.getBlockLightEngine(); -+ try { -+ final long coordinate = this.chunkCoordinate; -+ final int chunkX = CoordinateUtils.getChunkX(coordinate); -+ final int chunkZ = CoordinateUtils.getChunkZ(coordinate); -+ -+ final Set positions = this.changedPositions; -+ final Boolean[] sectionChanges = this.changedSectionSet; -+ -+ if (!litChunk) { -+ if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) { -+ skyEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges); -+ } -+ if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) { -+ blockEngine.blocksChangedInChunk(this.lightEngine.getLightAccess(), chunkX, chunkZ, positions, sectionChanges); -+ } -+ -+ if (skyEngine != null && this.queuedEdgeChecksSky != null) { -+ skyEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksSky); -+ } -+ if (blockEngine != null && this.queuedEdgeChecksBlock != null) { -+ blockEngine.checkChunkEdges(this.lightEngine.getLightAccess(), chunkX, chunkZ, this.queuedEdgeChecksBlock); -+ } -+ } -+ -+ this.onComplete.complete(null); -+ } finally { -+ this.lightEngine.releaseSkyLightEngine(skyEngine); -+ this.lightEngine.releaseBlockLightEngine(blockEngine); -+ } -+ } -+ } -+} -diff --git a/src/main/java/io/papermc/paper/chunk/system/poi/PoiChunk.java b/src/main/java/io/papermc/paper/chunk/system/poi/PoiChunk.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d72041aa814ff179e6e29a45dcd359a91d426d47 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/system/poi/PoiChunk.java -@@ -0,0 +1,213 @@ -+package io.papermc.paper.chunk.system.poi; -+ -+import com.mojang.logging.LogUtils; -+import com.mojang.serialization.Codec; -+import com.mojang.serialization.DataResult; -+import io.papermc.paper.util.CoordinateUtils; -+import io.papermc.paper.util.TickThread; -+import io.papermc.paper.util.WorldUtil; -+import net.minecraft.SharedConstants; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.nbt.NbtOps; -+import net.minecraft.nbt.Tag; -+import net.minecraft.resources.RegistryOps; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.entity.ai.village.poi.PoiManager; -+import net.minecraft.world.entity.ai.village.poi.PoiSection; -+import org.slf4j.Logger; -+ -+import java.util.Optional; -+ -+public final class PoiChunk { -+ -+ private static final Logger LOGGER = LogUtils.getClassLogger(); -+ -+ public final ServerLevel world; -+ public final int chunkX; -+ public final int chunkZ; -+ public final int minSection; -+ public final int maxSection; -+ -+ protected final PoiSection[] sections; -+ -+ private boolean isDirty; -+ private boolean loaded; -+ -+ public PoiChunk(final ServerLevel world, final int chunkX, final int chunkZ, final int minSection, final int maxSection) { -+ this(world, chunkX, chunkZ, minSection, maxSection, new PoiSection[maxSection - minSection + 1]); -+ } -+ -+ public PoiChunk(final ServerLevel world, final int chunkX, final int chunkZ, final int minSection, final int maxSection, final PoiSection[] sections) { -+ this.world = world; -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ this.minSection = minSection; -+ this.maxSection = maxSection; -+ this.sections = sections; -+ if (this.sections.length != (maxSection - minSection + 1)) { -+ throw new IllegalStateException("Incorrect length used, expected " + (maxSection - minSection + 1) + ", got " + this.sections.length); -+ } -+ } -+ -+ public void load() { -+ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Loading in poi chunk off-main"); -+ if (this.loaded) { -+ return; -+ } -+ this.loaded = true; -+ this.world.chunkSource.getPoiManager().loadInPoiChunk(this); -+ } -+ -+ public boolean isLoaded() { -+ return this.loaded; -+ } -+ -+ public boolean isEmpty() { -+ for (final PoiSection section : this.sections) { -+ if (section != null && !section.isEmpty()) { -+ return false; -+ } -+ } -+ -+ return true; -+ } -+ -+ public PoiSection getOrCreateSection(final int chunkY) { -+ if (chunkY >= this.minSection && chunkY <= this.maxSection) { -+ final int idx = chunkY - this.minSection; -+ final PoiSection ret = this.sections[idx]; -+ if (ret != null) { -+ return ret; -+ } -+ -+ final PoiManager poiManager = this.world.getPoiManager(); -+ final long key = CoordinateUtils.getChunkSectionKey(this.chunkX, chunkY, this.chunkZ); -+ -+ return this.sections[idx] = new PoiSection(() -> { -+ poiManager.setDirty(key); -+ }); -+ } -+ throw new IllegalArgumentException("chunkY is out of bounds, chunkY: " + chunkY + " outside [" + this.minSection + "," + this.maxSection + "]"); -+ } -+ -+ public PoiSection getSection(final int chunkY) { -+ if (chunkY >= this.minSection && chunkY <= this.maxSection) { -+ return this.sections[chunkY - this.minSection]; -+ } -+ return null; -+ } -+ -+ public Optional getSectionForVanilla(final int chunkY) { -+ if (chunkY >= this.minSection && chunkY <= this.maxSection) { -+ final PoiSection ret = this.sections[chunkY - this.minSection]; -+ return ret == null ? Optional.empty() : ret.noAllocateOptional; -+ } -+ return Optional.empty(); -+ } -+ -+ public boolean isDirty() { -+ return this.isDirty; -+ } -+ -+ public void setDirty(final boolean dirty) { -+ this.isDirty = dirty; -+ } -+ -+ // returns null if empty -+ public CompoundTag save() { -+ final RegistryOps registryOps = RegistryOps.create(NbtOps.INSTANCE, world.getPoiManager().registryAccess); -+ -+ final CompoundTag ret = new CompoundTag(); -+ final CompoundTag sections = new CompoundTag(); -+ ret.put("Sections", sections); -+ -+ ret.putInt("DataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion()); -+ -+ final ServerLevel world = this.world; -+ final PoiManager poiManager = world.getPoiManager(); -+ final int chunkX = this.chunkX; -+ final int chunkZ = this.chunkZ; -+ -+ for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) { -+ final PoiSection chunk = this.sections[sectionY - this.minSection]; -+ if (chunk == null || chunk.isEmpty()) { -+ continue; -+ } -+ -+ final long key = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ); -+ // codecs are honestly such a fucking disaster. What the fuck is this trash? -+ final Codec codec = PoiSection.codec(() -> { -+ poiManager.setDirty(key); -+ }); -+ -+ final DataResult serializedResult = codec.encodeStart(registryOps, chunk); -+ final int finalSectionY = sectionY; -+ final Tag serialized = serializedResult.resultOrPartial((final String description) -> { -+ LOGGER.error("Failed to serialize poi chunk for world: " + world.getWorld().getName() + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description); -+ }).orElse(null); -+ if (serialized == null) { -+ // failed, should be logged from the resultOrPartial -+ continue; -+ } -+ -+ sections.put(Integer.toString(sectionY), serialized); -+ } -+ -+ return sections.isEmpty() ? null : ret; -+ } -+ -+ public static PoiChunk empty(final ServerLevel world, final int chunkX, final int chunkZ) { -+ final PoiChunk ret = new PoiChunk(world, chunkX, chunkZ, WorldUtil.getMinSection(world), WorldUtil.getMaxSection(world)); -+ ret.loaded = true; -+ return ret; -+ } -+ -+ public static PoiChunk parse(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data) { -+ final PoiChunk ret = empty(world, chunkX, chunkZ); -+ -+ final RegistryOps registryOps = RegistryOps.create(NbtOps.INSTANCE, world.getPoiManager().registryAccess); -+ -+ final CompoundTag sections = data.getCompound("Sections"); -+ -+ if (sections.isEmpty()) { -+ // nothing to parse -+ return ret; -+ } -+ -+ final PoiManager poiManager = world.getPoiManager(); -+ -+ boolean readAnything = false; -+ -+ for (int sectionY = ret.minSection; sectionY <= ret.maxSection; ++sectionY) { -+ final String key = Integer.toString(sectionY); -+ if (!sections.contains(key)) { -+ continue; -+ } -+ -+ final long coordinateKey = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ); -+ // codecs are honestly such a fucking disaster. What the fuck is this trash? -+ final Codec codec = PoiSection.codec(() -> { -+ poiManager.setDirty(coordinateKey); -+ }); -+ -+ final CompoundTag section = sections.getCompound(key); -+ final DataResult deserializeResult = codec.parse(registryOps, section); -+ final int finalSectionY = sectionY; -+ final PoiSection deserialized = deserializeResult.resultOrPartial((final String description) -> { -+ LOGGER.error("Failed to deserialize poi chunk for world: " + world.getWorld().getName() + ", chunk: (" + chunkX + "," + finalSectionY + "," + chunkZ + "); description: " + description); -+ }).orElse(null); -+ -+ if (deserialized == null || deserialized.isEmpty()) { -+ // completely empty, no point in storing this -+ continue; -+ } -+ -+ readAnything = true; -+ ret.sections[sectionY - ret.minSection] = deserialized; -+ } -+ -+ ret.loaded = !readAnything; // Set loaded to false if we read anything to ensure proper callbacks to PoiManager are made on #load -+ -+ return ret; -+ } -+} -diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkFullTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkFullTask.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c307b084f59f7bb94dc02f25bbcd3e01e01d2306 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkFullTask.java -@@ -0,0 +1,131 @@ -+package io.papermc.paper.chunk.system.scheduling; -+ -+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; -+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; -+import com.mojang.logging.LogUtils; -+import io.papermc.paper.chunk.system.poi.PoiChunk; -+import net.minecraft.server.level.ChunkMap; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.level.chunk.ChunkAccess; -+import net.minecraft.world.level.chunk.status.ChunkStatus; -+import net.minecraft.world.level.chunk.ImposterProtoChunk; -+import net.minecraft.world.level.chunk.LevelChunk; -+import net.minecraft.world.level.chunk.ProtoChunk; -+import org.slf4j.Logger; -+import java.lang.invoke.VarHandle; -+ -+public final class ChunkFullTask extends ChunkProgressionTask implements Runnable { -+ -+ private static final Logger LOGGER = LogUtils.getClassLogger(); -+ -+ protected final NewChunkHolder chunkHolder; -+ protected final ChunkAccess fromChunk; -+ protected final PrioritisedExecutor.PrioritisedTask convertToFullTask; -+ -+ public ChunkFullTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, -+ final NewChunkHolder chunkHolder, final ChunkAccess fromChunk, final PrioritisedExecutor.Priority priority) { -+ super(scheduler, world, chunkX, chunkZ); -+ this.chunkHolder = chunkHolder; -+ this.fromChunk = fromChunk; -+ this.convertToFullTask = scheduler.createChunkTask(chunkX, chunkZ, this, priority); -+ } -+ -+ @Override -+ public ChunkStatus getTargetStatus() { -+ return ChunkStatus.FULL; -+ } -+ -+ @Override -+ public void run() { -+ // See Vanilla protoChunkToFullChunk for what this function should be doing -+ final LevelChunk chunk; -+ try { -+ // moved from the load from nbt stage into here -+ final PoiChunk poiChunk = this.chunkHolder.getPoiChunk(); -+ if (poiChunk == null) { -+ LOGGER.error("Expected poi chunk to be loaded with chunk for task " + this.toString()); -+ } else { -+ poiChunk.load(); -+ this.world.getPoiManager().checkConsistency(this.fromChunk); -+ } -+ -+ if (this.fromChunk instanceof ImposterProtoChunk wrappedFull) { -+ chunk = wrappedFull.getWrapped(); -+ } else { -+ final ServerLevel world = this.world; -+ final ProtoChunk protoChunk = (ProtoChunk)this.fromChunk; -+ chunk = new LevelChunk(this.world, protoChunk, (final LevelChunk unused) -> { -+ ChunkMap.postLoadProtoChunk(world, protoChunk.getEntities(), protoChunk.getPos()); // Paper - rewrite chunk system -+ }); -+ } -+ -+ chunk.setChunkHolder(this.scheduler.chunkHolderManager.getChunkHolder(this.chunkX, this.chunkZ)); // replaces setFullStatus -+ chunk.runPostLoad(); -+ // Unlike Vanilla, we load the entity chunk here, as we load the NBT in empty status (unlike Vanilla) -+ // This brings entity addition back in line with older versions of the game -+ // Since we load the NBT in the empty status, this will never block for I/O -+ this.world.chunkTaskScheduler.chunkHolderManager.getOrCreateEntityChunk(this.chunkX, this.chunkZ, false); -+ -+ // we don't need the entitiesInLevel trash, this system doesn't double run callbacks -+ chunk.setLoaded(true); -+ chunk.registerAllBlockEntitiesAfterLevelLoad(); -+ chunk.registerTickContainerInLevel(this.world); -+ } catch (final Throwable throwable) { -+ this.complete(null, throwable); -+ return; -+ } -+ this.complete(chunk, null); -+ } -+ -+ protected volatile boolean scheduled; -+ protected static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkFullTask.class, "scheduled", boolean.class); -+ -+ @Override -+ public boolean isScheduled() { -+ return this.scheduled; -+ } -+ -+ @Override -+ public void schedule() { -+ if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkFullTask)this, true)) { -+ throw new IllegalStateException("Cannot double call schedule()"); -+ } -+ this.convertToFullTask.queue(); -+ } -+ -+ @Override -+ public void cancel() { -+ if (this.convertToFullTask.cancel()) { -+ this.complete(null, null); -+ } -+ } -+ -+ @Override -+ public PrioritisedExecutor.Priority getPriority() { -+ return this.convertToFullTask.getPriority(); -+ } -+ -+ @Override -+ public void lowerPriority(final PrioritisedExecutor.Priority priority) { -+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ this.convertToFullTask.lowerPriority(priority); -+ } -+ -+ @Override -+ public void setPriority(final PrioritisedExecutor.Priority priority) { -+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ this.convertToFullTask.setPriority(priority); -+ } -+ -+ @Override -+ public void raisePriority(final PrioritisedExecutor.Priority priority) { -+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ this.convertToFullTask.raisePriority(priority); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..5b446e6ac151f99f64f0c442d0b40b5e251bc4c4 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java -@@ -0,0 +1,1500 @@ -+package io.papermc.paper.chunk.system.scheduling; -+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; -+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; -+import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable; -+import com.google.common.collect.ImmutableList; -+import com.google.gson.JsonArray; -+import com.google.gson.JsonObject; -+import com.mojang.logging.LogUtils; -+import io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader; -+import io.papermc.paper.chunk.system.io.RegionFileIOThread; -+import io.papermc.paper.chunk.system.poi.PoiChunk; -+import io.papermc.paper.threadedregions.TickRegions; -+import io.papermc.paper.util.CoordinateUtils; -+import io.papermc.paper.util.TickThread; -+import io.papermc.paper.world.ChunkEntitySlices; -+import it.unimi.dsi.fastutil.longs.Long2ByteLinkedOpenHashMap; -+import it.unimi.dsi.fastutil.longs.Long2ByteMap; -+import it.unimi.dsi.fastutil.longs.Long2IntLinkedOpenHashMap; -+import it.unimi.dsi.fastutil.longs.Long2IntMap; -+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; -+import it.unimi.dsi.fastutil.longs.Long2ObjectMap; -+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -+import it.unimi.dsi.fastutil.longs.LongArrayList; -+import it.unimi.dsi.fastutil.longs.LongIterator; -+import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet; -+import net.minecraft.nbt.CompoundTag; -+import io.papermc.paper.chunk.system.ChunkSystem; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ChunkHolder; -+import net.minecraft.server.level.ChunkLevel; -+import net.minecraft.server.level.FullChunkStatus; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.Ticket; -+import net.minecraft.server.level.TicketType; -+import net.minecraft.util.SortedArraySet; -+import net.minecraft.util.Unit; -+import net.minecraft.world.level.ChunkPos; -+import org.bukkit.plugin.Plugin; -+import org.slf4j.Logger; -+import java.io.IOException; -+import java.text.DecimalFormat; -+import java.util.ArrayDeque; -+import java.util.ArrayList; -+import java.util.Collection; -+import java.util.Collections; -+import java.util.Iterator; -+import java.util.List; -+import java.util.concurrent.ConcurrentHashMap; -+import java.util.concurrent.TimeUnit; -+import java.util.concurrent.atomic.AtomicBoolean; -+import java.util.concurrent.atomic.AtomicLong; -+import java.util.concurrent.atomic.AtomicReference; -+import java.util.concurrent.locks.LockSupport; -+import java.util.function.Predicate; -+ -+public final class ChunkHolderManager { -+ -+ private static final Logger LOGGER = LogUtils.getClassLogger(); -+ -+ public static final int FULL_LOADED_TICKET_LEVEL = 33; -+ public static final int BLOCK_TICKING_TICKET_LEVEL = 32; -+ public static final int ENTITY_TICKING_TICKET_LEVEL = 31; -+ public static final int MAX_TICKET_LEVEL = ChunkLevel.MAX_LEVEL; // inclusive -+ -+ private static final long NO_TIMEOUT_MARKER = Long.MIN_VALUE; -+ private static final long PROBE_MARKER = Long.MIN_VALUE + 1; -+ public final ReentrantAreaLock ticketLockArea; -+ -+ private final ConcurrentHashMap>> tickets = new java.util.concurrent.ConcurrentHashMap<>(); -+ private final ConcurrentHashMap sectionToChunkToExpireCount = new java.util.concurrent.ConcurrentHashMap<>(); -+ final ChunkQueue unloadQueue; -+ -+ public boolean processTicketUpdates(final int posX, final int posZ) { -+ final int ticketShift = ThreadedTicketLevelPropagator.SECTION_SHIFT; -+ final int ticketMask = (1 << ticketShift) - 1; -+ final List scheduledTasks = new ArrayList<>(); -+ final List changedFullStatus = new ArrayList<>(); -+ final boolean ret; -+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( -+ ((posX >> ticketShift) - 1) << ticketShift, -+ ((posZ >> ticketShift) - 1) << ticketShift, -+ (((posX >> ticketShift) + 1) << ticketShift) | ticketMask, -+ (((posZ >> ticketShift) + 1) << ticketShift) | ticketMask -+ ); -+ try { -+ ret = this.processTicketUpdatesNoLock(posX >> ticketShift, posZ >> ticketShift, scheduledTasks, changedFullStatus); -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ -+ this.addChangedStatuses(changedFullStatus); -+ -+ for (int i = 0, len = scheduledTasks.size(); i < len; ++i) { -+ scheduledTasks.get(i).schedule(); -+ } -+ -+ return ret; -+ } -+ -+ private boolean processTicketUpdatesNoLock(final int sectionX, final int sectionZ, final List scheduledTasks, -+ final List changedFullStatus) { -+ return this.ticketLevelPropagator.performUpdate( -+ sectionX, sectionZ, this.taskScheduler.schedulingLockArea, scheduledTasks, changedFullStatus -+ ); -+ } -+ -+ private final SWMRLong2ObjectHashTable chunkHolders = new SWMRLong2ObjectHashTable<>(16384, 0.25f); -+ // what a disaster of a name -+ // this is a map of removal tick to a map of chunks and the number of tickets a chunk has that are to expire that tick -+ private final Long2ObjectOpenHashMap removeTickToChunkExpireTicketCount = new Long2ObjectOpenHashMap<>(); -+ private final ServerLevel world; -+ private final ChunkTaskScheduler taskScheduler; -+ private long currentTick; -+ -+ private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>(); -+ private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { -+ if (c1 == c2) { -+ return 0; -+ } -+ -+ final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); -+ -+ if (saveTickCompare != 0) { -+ return saveTickCompare; -+ } -+ -+ final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); -+ final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); -+ -+ if (coord1 == coord2) { -+ throw new IllegalStateException("Duplicate chunkholder in auto save queue"); -+ } -+ -+ return Long.compare(coord1, coord2); -+ }); -+ -+ public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) { -+ this.world = world; -+ this.taskScheduler = taskScheduler; -+ this.ticketLockArea = new ReentrantAreaLock(taskScheduler.getChunkSystemLockShift()); -+ this.unloadQueue = new ChunkQueue(world.getRegionChunkShift()); -+ } -+ -+ private final AtomicLong statusUpgradeId = new AtomicLong(); -+ -+ long getNextStatusUpgradeId() { -+ return this.statusUpgradeId.incrementAndGet(); -+ } -+ -+ public List getOldChunkHolders() { -+ final List holders = this.getChunkHolders(); -+ final List ret = new ArrayList<>(holders.size()); -+ for (final NewChunkHolder holder : holders) { -+ ret.add(holder.vanillaChunkHolder); -+ } -+ return ret; -+ } -+ -+ public List getChunkHolders() { -+ final List ret = new ArrayList<>(this.chunkHolders.size()); -+ this.chunkHolders.forEachValue(ret::add); -+ return ret; -+ } -+ -+ public int size() { -+ return this.chunkHolders.size(); -+ } -+ -+ public void close(final boolean save, final boolean halt) { -+ TickThread.ensureTickThread("Closing world off-main"); -+ if (halt) { -+ LOGGER.info("Waiting 60s for chunk system to halt for world '" + this.world.getWorld().getName() + "'"); -+ if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) { -+ LOGGER.warn("Failed to halt world generation/loading tasks for world '" + this.world.getWorld().getName() + "'"); -+ } else { -+ LOGGER.info("Halted chunk system for world '" + this.world.getWorld().getName() + "'"); -+ } -+ } -+ -+ if (save) { -+ this.saveAllChunks(true, true, true); -+ } -+ -+ if (this.world.chunkDataControllerNew.hasTasks() || this.world.entityDataControllerNew.hasTasks() || this.world.poiDataControllerNew.hasTasks()) { -+ RegionFileIOThread.flush(); -+ } -+ -+ // kill regionfile cache -+ try { -+ this.world.chunkDataControllerNew.getCache().close(); -+ } catch (final IOException ex) { -+ LOGGER.error("Failed to close chunk regionfile cache for world '" + this.world.getWorld().getName() + "'", ex); -+ } -+ try { -+ this.world.entityDataControllerNew.getCache().close(); -+ } catch (final IOException ex) { -+ LOGGER.error("Failed to close entity regionfile cache for world '" + this.world.getWorld().getName() + "'", ex); -+ } -+ try { -+ this.world.poiDataControllerNew.getCache().close(); -+ } catch (final IOException ex) { -+ LOGGER.error("Failed to close poi regionfile cache for world '" + this.world.getWorld().getName() + "'", ex); -+ } -+ } -+ -+ void ensureInAutosave(final NewChunkHolder holder) { -+ if (!this.autoSaveQueue.contains(holder)) { -+ holder.lastAutoSave = MinecraftServer.currentTick; -+ this.autoSaveQueue.add(holder); -+ } -+ } -+ -+ public void autoSave() { -+ final List reschedule = new ArrayList<>(); -+ final long currentTick = MinecraftServer.currentTickLong; -+ final long maxSaveTime = currentTick - this.world.paperConfig().chunks.autoSaveInterval.value(); -+ for (int autoSaved = 0; autoSaved < this.world.paperConfig().chunks.maxAutoSaveChunksPerTick && !this.autoSaveQueue.isEmpty();) { -+ final NewChunkHolder holder = this.autoSaveQueue.first(); -+ -+ if (holder.lastAutoSave > maxSaveTime) { -+ break; -+ } -+ -+ this.autoSaveQueue.remove(holder); -+ -+ holder.lastAutoSave = currentTick; -+ if (holder.save(false, false) != null) { -+ ++autoSaved; -+ } -+ -+ if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) { -+ reschedule.add(holder); -+ } -+ } -+ -+ for (final NewChunkHolder holder : reschedule) { -+ if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) { -+ this.autoSaveQueue.add(holder); -+ } -+ } -+ } -+ -+ public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) { -+ final List holders = this.getChunkHolders(); -+ -+ if (logProgress) { -+ LOGGER.info("Saving all chunkholders for world '" + this.world.getWorld().getName() + "'"); -+ } -+ -+ final DecimalFormat format = new DecimalFormat("#0.00"); -+ -+ int saved = 0; -+ -+ long start = System.nanoTime(); -+ long lastLog = start; -+ boolean needsFlush = false; -+ final int flushInterval = 50; -+ -+ int savedChunk = 0; -+ int savedEntity = 0; -+ int savedPoi = 0; -+ -+ for (int i = 0, len = holders.size(); i < len; ++i) { -+ final NewChunkHolder holder = holders.get(i); -+ try { -+ final NewChunkHolder.SaveStat saveStat = holder.save(shutdown, false); -+ if (saveStat != null) { -+ ++saved; -+ needsFlush = flush; -+ if (saveStat.savedChunk()) { -+ ++savedChunk; -+ } -+ if (saveStat.savedEntityChunk()) { -+ ++savedEntity; -+ } -+ if (saveStat.savedPoiChunk()) { -+ ++savedPoi; -+ } -+ } -+ } catch (final ThreadDeath thr) { -+ throw thr; -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to save chunk (" + holder.chunkX + "," + holder.chunkZ + ") in world '" + this.world.getWorld().getName() + "'", thr); -+ } -+ if (needsFlush && (saved % flushInterval) == 0) { -+ needsFlush = false; -+ RegionFileIOThread.partialFlush(flushInterval / 2); -+ } -+ if (logProgress) { -+ final long currTime = System.nanoTime(); -+ if ((currTime - lastLog) > TimeUnit.SECONDS.toNanos(10L)) { -+ lastLog = currTime; -+ LOGGER.info("Saved " + saved + " chunks (" + format.format((double)(i+1)/(double)len * 100.0) + "%) in world '" + this.world.getWorld().getName() + "'"); -+ } -+ } -+ } -+ if (flush) { -+ RegionFileIOThread.flush(); -+ if (this.world.paperConfig().chunks.flushRegionsOnSave) { -+ try { -+ this.world.chunkSource.chunkMap.regionFileCache.flush(); -+ } catch (IOException ex) { -+ LOGGER.error("Exception when flushing regions in world {}", this.world.getWorld().getName(), ex); -+ } -+ } -+ } -+ if (logProgress) { -+ LOGGER.info("Saved " + savedChunk + " block chunks, " + savedEntity + " entity chunks, " + savedPoi + " poi chunks in world '" + this.world.getWorld().getName() + "' in " + format.format(1.0E-9 * (System.nanoTime() - start)) + "s"); -+ } -+ } -+ -+ protected final ThreadedTicketLevelPropagator ticketLevelPropagator = new ThreadedTicketLevelPropagator() { -+ @Override -+ protected void processLevelUpdates(final Long2ByteLinkedOpenHashMap updates) { -+ // first the necessary chunkholders must be created, so just update the ticket levels -+ for (final Iterator iterator = updates.long2ByteEntrySet().fastIterator(); iterator.hasNext();) { -+ final Long2ByteMap.Entry entry = iterator.next(); -+ final long key = entry.getLongKey(); -+ final int newLevel = convertBetweenTicketLevels((int)entry.getByteValue()); -+ -+ NewChunkHolder current = ChunkHolderManager.this.chunkHolders.get(key); -+ if (current == null && newLevel > MAX_TICKET_LEVEL) { -+ // not loaded and it shouldn't be loaded! -+ iterator.remove(); -+ continue; -+ } -+ -+ final int currentLevel = current == null ? MAX_TICKET_LEVEL + 1 : current.getCurrentTicketLevel(); -+ if (currentLevel == newLevel) { -+ // nothing to do -+ iterator.remove(); -+ continue; -+ } -+ -+ if (current == null) { -+ // must create -+ current = ChunkHolderManager.this.createChunkHolder(key); -+ synchronized (ChunkHolderManager.this.chunkHolders) { -+ ChunkHolderManager.this.chunkHolders.put(key, current); -+ } -+ current.updateTicketLevel(newLevel); -+ } else { -+ current.updateTicketLevel(newLevel); -+ } -+ } -+ } -+ -+ @Override -+ protected void processSchedulingUpdates(final Long2ByteLinkedOpenHashMap updates, final List scheduledTasks, -+ final List changedFullStatus) { -+ final List prev = CURRENT_TICKET_UPDATE_SCHEDULING.get(); -+ CURRENT_TICKET_UPDATE_SCHEDULING.set(scheduledTasks); -+ try { -+ for (final LongIterator iterator = updates.keySet().iterator(); iterator.hasNext();) { -+ final long key = iterator.nextLong(); -+ final NewChunkHolder current = ChunkHolderManager.this.chunkHolders.get(key); -+ -+ if (current == null) { -+ throw new IllegalStateException("Expected chunk holder to be created"); -+ } -+ -+ current.processTicketLevelUpdate(scheduledTasks, changedFullStatus); -+ } -+ } finally { -+ CURRENT_TICKET_UPDATE_SCHEDULING.set(prev); -+ } -+ } -+ }; -+ // function for converting between ticket levels and propagator levels and vice versa -+ // the problem is the ticket level propagator will propagate from a set source down to zero, whereas mojang expects -+ // levels to propagate from a set value up to a maximum value. so we need to convert the levels we put into the propagator -+ // and the levels we get out of the propagator -+ -+ public static int convertBetweenTicketLevels(final int level) { -+ return ChunkLevel.MAX_LEVEL - level + 1; -+ } -+ -+ public String getTicketDebugString(final long coordinate) { -+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate)); -+ try { -+ final SortedArraySet> tickets = this.tickets.get(new RegionFileIOThread.ChunkCoordinate(coordinate)); -+ -+ return tickets != null ? tickets.first().toString() : "no_ticket"; -+ } finally { -+ if (ticketLock != null) { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ } -+ } -+ -+ public Long2ObjectOpenHashMap>> getTicketsCopy() { -+ final Long2ObjectOpenHashMap>> ret = new Long2ObjectOpenHashMap<>(); -+ final Long2ObjectOpenHashMap> sections = new Long2ObjectOpenHashMap(); -+ final int sectionShift = this.taskScheduler.getChunkSystemLockShift(); -+ for (final RegionFileIOThread.ChunkCoordinate coord : this.tickets.keySet()) { -+ sections.computeIfAbsent( -+ CoordinateUtils.getChunkKey( -+ CoordinateUtils.getChunkX(coord.key) >> sectionShift, -+ CoordinateUtils.getChunkZ(coord.key) >> sectionShift -+ ), -+ (final long keyInMap) -> { -+ return new ArrayList<>(); -+ } -+ ).add(coord); -+ } -+ -+ for (final Iterator>> iterator = sections.long2ObjectEntrySet().fastIterator(); -+ iterator.hasNext();) { -+ final Long2ObjectMap.Entry> entry = iterator.next(); -+ final long sectionKey = entry.getLongKey(); -+ final List coordinates = entry.getValue(); -+ -+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( -+ CoordinateUtils.getChunkX(sectionKey) << sectionShift, -+ CoordinateUtils.getChunkZ(sectionKey) << sectionShift -+ ); -+ try { -+ for (final RegionFileIOThread.ChunkCoordinate coord : coordinates) { -+ final SortedArraySet> tickets = this.tickets.get(coord); -+ if (tickets == null) { -+ // removed before we acquired lock -+ continue; -+ } -+ ret.put(coord.key, new SortedArraySet<>(tickets)); -+ } -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ } -+ -+ return ret; -+ } -+ -+ public Collection getPluginChunkTickets(int x, int z) { -+ ImmutableList.Builder ret; -+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(x, z); -+ try { -+ final long coordinate = CoordinateUtils.getChunkKey(x, z); -+ final SortedArraySet> tickets = this.tickets.get(new RegionFileIOThread.ChunkCoordinate(coordinate)); -+ -+ if (tickets == null) { -+ return Collections.emptyList(); -+ } -+ -+ ret = ImmutableList.builder(); -+ for (Ticket ticket : tickets) { -+ if (ticket.getType() == TicketType.PLUGIN_TICKET) { -+ ret.add((Plugin)ticket.key); -+ } -+ } -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ -+ return ret.build(); -+ } -+ -+ protected final void updateTicketLevel(final long coordinate, final int ticketLevel) { -+ if (ticketLevel > ChunkLevel.MAX_LEVEL) { -+ this.ticketLevelPropagator.removeSource(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate)); -+ } else { -+ this.ticketLevelPropagator.setSource(CoordinateUtils.getChunkX(coordinate), CoordinateUtils.getChunkZ(coordinate), convertBetweenTicketLevels(ticketLevel)); -+ } -+ } -+ -+ private static int getTicketLevelAt(SortedArraySet> tickets) { -+ return !tickets.isEmpty() ? tickets.first().getTicketLevel() : MAX_TICKET_LEVEL + 1; -+ } -+ -+ public boolean addTicketAtLevel(final TicketType type, final ChunkPos chunkPos, final int level, -+ final T identifier) { -+ return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier); -+ } -+ -+ public boolean addTicketAtLevel(final TicketType type, final int chunkX, final int chunkZ, final int level, -+ final T identifier) { -+ return this.addTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier); -+ } -+ -+ private void addExpireCount(final int chunkX, final int chunkZ) { -+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ -+ final int sectionShift = this.world.getRegionChunkShift(); -+ final RegionFileIOThread.ChunkCoordinate sectionKey = new RegionFileIOThread.ChunkCoordinate(CoordinateUtils.getChunkKey( -+ chunkX >> sectionShift, -+ chunkZ >> sectionShift -+ )); -+ -+ this.sectionToChunkToExpireCount.computeIfAbsent(sectionKey, (final RegionFileIOThread.ChunkCoordinate keyInMap) -> { -+ return new Long2IntOpenHashMap(); -+ }).addTo(chunkKey, 1); -+ } -+ -+ private void removeExpireCount(final int chunkX, final int chunkZ) { -+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ -+ final int sectionShift = this.world.getRegionChunkShift(); -+ final RegionFileIOThread.ChunkCoordinate sectionKey = new RegionFileIOThread.ChunkCoordinate(CoordinateUtils.getChunkKey( -+ chunkX >> sectionShift, -+ chunkZ >> sectionShift -+ )); -+ -+ final Long2IntOpenHashMap removeCounts = this.sectionToChunkToExpireCount.get(sectionKey); -+ final int prevCount = removeCounts.addTo(chunkKey, -1); -+ -+ if (prevCount == 1) { -+ removeCounts.remove(chunkKey); -+ if (removeCounts.isEmpty()) { -+ this.sectionToChunkToExpireCount.remove(sectionKey); -+ } -+ } -+ } -+ -+ // supposed to return true if the ticket was added and did not replace another -+ // but, we always return false if the ticket cannot be added -+ public boolean addTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier) { -+ return this.addTicketAtLevel(type, chunk, level, identifier, true); -+ } -+ -+ boolean addTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier, final boolean lock) { -+ final long removeDelay = type.timeout <= 0 ? NO_TIMEOUT_MARKER : type.timeout; -+ if (level > MAX_TICKET_LEVEL) { -+ return false; -+ } -+ -+ final int chunkX = CoordinateUtils.getChunkX(chunk); -+ final int chunkZ = CoordinateUtils.getChunkZ(chunk); -+ final RegionFileIOThread.ChunkCoordinate chunkCoord = new RegionFileIOThread.ChunkCoordinate(chunk); -+ final Ticket ticket = new Ticket<>(type, level, identifier, removeDelay); -+ -+ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null; -+ try { -+ final SortedArraySet> ticketsAtChunk = this.tickets.computeIfAbsent(chunkCoord, (final RegionFileIOThread.ChunkCoordinate keyInMap) -> { -+ return SortedArraySet.create(4); -+ }); -+ -+ final int levelBefore = getTicketLevelAt(ticketsAtChunk); -+ final Ticket current = (Ticket)ticketsAtChunk.replace(ticket); -+ final int levelAfter = getTicketLevelAt(ticketsAtChunk); -+ -+ if (current != ticket) { -+ final long oldRemoveDelay = current.removeDelay; -+ if (removeDelay != oldRemoveDelay) { -+ if (oldRemoveDelay != NO_TIMEOUT_MARKER && removeDelay == NO_TIMEOUT_MARKER) { -+ this.removeExpireCount(chunkX, chunkZ); -+ } else if (oldRemoveDelay == NO_TIMEOUT_MARKER) { -+ // since old != new, we have that NO_TIMEOUT_MARKER != new -+ this.addExpireCount(chunkX, chunkZ); -+ } -+ } -+ } else { -+ if (removeDelay != NO_TIMEOUT_MARKER) { -+ this.addExpireCount(chunkX, chunkZ); -+ } -+ } -+ -+ if (levelBefore != levelAfter) { -+ this.updateTicketLevel(chunk, levelAfter); -+ } -+ -+ return current == ticket; -+ } finally { -+ if (ticketLock != null) { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ } -+ } -+ -+ public boolean removeTicketAtLevel(final TicketType type, final ChunkPos chunkPos, final int level, final T identifier) { -+ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier); -+ } -+ -+ public boolean removeTicketAtLevel(final TicketType type, final int chunkX, final int chunkZ, final int level, final T identifier) { -+ return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkX, chunkZ), level, identifier); -+ } -+ -+ public boolean removeTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier) { -+ return this.removeTicketAtLevel(type, chunk, level, identifier, true); -+ } -+ -+ boolean removeTicketAtLevel(final TicketType type, final long chunk, final int level, final T identifier, final boolean lock) { -+ if (level > MAX_TICKET_LEVEL) { -+ return false; -+ } -+ -+ final int chunkX = CoordinateUtils.getChunkX(chunk); -+ final int chunkZ = CoordinateUtils.getChunkZ(chunk); -+ final RegionFileIOThread.ChunkCoordinate chunkCoord = new RegionFileIOThread.ChunkCoordinate(chunk); -+ final Ticket probe = new Ticket<>(type, level, identifier, PROBE_MARKER); -+ -+ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null; -+ try { -+ final SortedArraySet> ticketsAtChunk = this.tickets.get(chunkCoord); -+ if (ticketsAtChunk == null) { -+ return false; -+ } -+ -+ final int oldLevel = getTicketLevelAt(ticketsAtChunk); -+ final Ticket ticket = (Ticket)ticketsAtChunk.removeAndGet(probe); -+ -+ if (ticket == null) { -+ return false; -+ } -+ -+ final int newLevel = getTicketLevelAt(ticketsAtChunk); -+ // we should not change the ticket levels while the target region may be ticking -+ if (oldLevel != newLevel) { -+ // Delay unload chunk patch originally by Aikar, updated to 1.20 by jpenilla -+ // these days, the patch is mostly useful to keep chunks ticking when players teleport -+ // so that their pets can teleport with them as well. -+ final long delayTimeout = this.world.paperConfig().chunks.delayChunkUnloadsBy.ticks(); -+ final TicketType toAdd; -+ final long timeout; -+ if (type == RegionizedPlayerChunkLoader.REGION_PLAYER_TICKET && delayTimeout > 0) { -+ toAdd = TicketType.DELAY_UNLOAD; -+ timeout = delayTimeout; -+ } else { -+ toAdd = TicketType.UNKNOWN; -+ // always expect UNKNOWN to be > 1, but just in case -+ timeout = Math.max(1, toAdd.timeout); -+ } -+ final Ticket unknownTicket = new Ticket<>(toAdd, level, new ChunkPos(chunk), timeout); -+ if (ticketsAtChunk.add(unknownTicket)) { -+ this.addExpireCount(chunkX, chunkZ); -+ } else { -+ throw new IllegalStateException("Should have been able to add " + unknownTicket + " to " + ticketsAtChunk); -+ } -+ } -+ -+ final long removeDelay = ticket.removeDelay; -+ if (removeDelay != NO_TIMEOUT_MARKER) { -+ this.removeExpireCount(chunkX, chunkZ); -+ } -+ -+ return true; -+ } finally { -+ if (ticketLock != null) { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ } -+ } -+ -+ // atomic with respect to all add/remove/addandremove ticket calls for the given chunk -+ public void addAndRemoveTickets(final long chunk, final TicketType addType, final int addLevel, final T addIdentifier, -+ final TicketType removeType, final int removeLevel, final V removeIdentifier) { -+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)); -+ try { -+ this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier, false); -+ this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier, false); -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ } -+ -+ // atomic with respect to all add/remove/addandremove ticket calls for the given chunk -+ public boolean addIfRemovedTicket(final long chunk, final TicketType addType, final int addLevel, final T addIdentifier, -+ final TicketType removeType, final int removeLevel, final V removeIdentifier) { -+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(CoordinateUtils.getChunkX(chunk), CoordinateUtils.getChunkZ(chunk)); -+ try { -+ if (this.removeTicketAtLevel(removeType, chunk, removeLevel, removeIdentifier, false)) { -+ this.addTicketAtLevel(addType, chunk, addLevel, addIdentifier, false); -+ return true; -+ } -+ return false; -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ } -+ -+ public void removeAllTicketsFor(final TicketType ticketType, final int ticketLevel, final T ticketIdentifier) { -+ if (ticketLevel > MAX_TICKET_LEVEL) { -+ return; -+ } -+ -+ final Long2ObjectOpenHashMap> sections = new Long2ObjectOpenHashMap(); -+ final int sectionShift = this.taskScheduler.getChunkSystemLockShift(); -+ for (final RegionFileIOThread.ChunkCoordinate coord : this.tickets.keySet()) { -+ sections.computeIfAbsent( -+ CoordinateUtils.getChunkKey( -+ CoordinateUtils.getChunkX(coord.key) >> sectionShift, -+ CoordinateUtils.getChunkZ(coord.key) >> sectionShift -+ ), -+ (final long keyInMap) -> { -+ return new ArrayList<>(); -+ } -+ ).add(coord); -+ } -+ -+ for (final Iterator>> iterator = sections.long2ObjectEntrySet().fastIterator(); -+ iterator.hasNext();) { -+ final Long2ObjectMap.Entry> entry = iterator.next(); -+ final long sectionKey = entry.getLongKey(); -+ final List coordinates = entry.getValue(); -+ -+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( -+ CoordinateUtils.getChunkX(sectionKey) << sectionShift, -+ CoordinateUtils.getChunkZ(sectionKey) << sectionShift -+ ); -+ try { -+ for (final RegionFileIOThread.ChunkCoordinate coord : coordinates) { -+ this.removeTicketAtLevel(ticketType, coord.key, ticketLevel, ticketIdentifier, false); -+ } -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ } -+ } -+ -+ public void tick() { -+ final int sectionShift = this.world.getRegionChunkShift(); -+ -+ final Predicate> expireNow = (final Ticket ticket) -> { -+ if (ticket.removeDelay == NO_TIMEOUT_MARKER) { -+ return false; -+ } -+ return --ticket.removeDelay <= 0L; -+ }; -+ -+ for (final Iterator iterator = this.sectionToChunkToExpireCount.keySet().iterator(); iterator.hasNext();) { -+ final RegionFileIOThread.ChunkCoordinate section = iterator.next(); -+ final long sectionKey = section.key; -+ -+ if (!this.sectionToChunkToExpireCount.containsKey(section)) { -+ // removed concurrently -+ continue; -+ } -+ -+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( -+ CoordinateUtils.getChunkX(sectionKey) << sectionShift, -+ CoordinateUtils.getChunkZ(sectionKey) << sectionShift -+ ); -+ -+ try { -+ final Long2IntOpenHashMap chunkToExpireCount = this.sectionToChunkToExpireCount.get(section); -+ if (chunkToExpireCount == null) { -+ // lost to some race -+ continue; -+ } -+ -+ for (final Iterator iterator1 = chunkToExpireCount.long2IntEntrySet().fastIterator(); iterator1.hasNext();) { -+ final Long2IntMap.Entry entry = iterator1.next(); -+ -+ final long chunkKey = entry.getLongKey(); -+ final int expireCount = entry.getIntValue(); -+ -+ final RegionFileIOThread.ChunkCoordinate chunk = new RegionFileIOThread.ChunkCoordinate(chunkKey); -+ -+ final SortedArraySet> tickets = this.tickets.get(chunk); -+ final int levelBefore = getTicketLevelAt(tickets); -+ -+ final int sizeBefore = tickets.size(); -+ tickets.removeIf(expireNow); -+ final int sizeAfter = tickets.size(); -+ final int levelAfter = getTicketLevelAt(tickets); -+ -+ if (tickets.isEmpty()) { -+ this.tickets.remove(chunk); -+ } -+ if (levelBefore != levelAfter) { -+ this.updateTicketLevel(chunkKey, levelAfter); -+ } -+ -+ final int newExpireCount = expireCount - (sizeBefore - sizeAfter); -+ -+ if (newExpireCount == expireCount) { -+ continue; -+ } -+ -+ if (newExpireCount != 0) { -+ entry.setValue(newExpireCount); -+ } else { -+ iterator1.remove(); -+ } -+ } -+ -+ if (chunkToExpireCount.isEmpty()) { -+ this.sectionToChunkToExpireCount.remove(section); -+ } -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ } -+ -+ this.processTicketUpdates(); -+ } -+ -+ public NewChunkHolder getChunkHolder(final int chunkX, final int chunkZ) { -+ return this.chunkHolders.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ } -+ -+ public NewChunkHolder getChunkHolder(final long position) { -+ return this.chunkHolders.get(position); -+ } -+ -+ public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { -+ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); -+ if (chunkHolder != null) { -+ chunkHolder.raisePriority(priority); -+ } -+ } -+ -+ public void setPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { -+ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); -+ if (chunkHolder != null) { -+ chunkHolder.setPriority(priority); -+ } -+ } -+ -+ public void lowerPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { -+ final NewChunkHolder chunkHolder = this.getChunkHolder(x, z); -+ if (chunkHolder != null) { -+ chunkHolder.lowerPriority(priority); -+ } -+ } -+ -+ private NewChunkHolder createChunkHolder(final long position) { -+ final NewChunkHolder ret = new NewChunkHolder(this.world, CoordinateUtils.getChunkX(position), CoordinateUtils.getChunkZ(position), this.taskScheduler); -+ -+ ChunkSystem.onChunkHolderCreate(this.world, ret.vanillaChunkHolder); -+ ret.vanillaChunkHolder.onChunkAdd(); -+ -+ return ret; -+ } -+ -+ // because this function creates the chunk holder without a ticket, it is the caller's responsibility to ensure -+ // the chunk holder eventually unloads. this should only be used to avoid using processTicketUpdates to create chunkholders, -+ // as processTicketUpdates may call plugin logic; in every other case a ticket is appropriate -+ private NewChunkHolder getOrCreateChunkHolder(final int chunkX, final int chunkZ) { -+ return this.getOrCreateChunkHolder(CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ } -+ -+ private NewChunkHolder getOrCreateChunkHolder(final long position) { -+ final int chunkX = CoordinateUtils.getChunkX(position); -+ final int chunkZ = CoordinateUtils.getChunkZ(position); -+ -+ if (!this.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ)) { -+ throw new IllegalStateException("Must hold ticket level update lock!"); -+ } -+ if (!this.taskScheduler.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ)) { -+ throw new IllegalStateException("Must hold scheduler lock!!"); -+ } -+ -+ // we could just acquire these locks, but... -+ // must own the locks because the caller needs to ensure that no unload can occur AFTER this function returns -+ -+ NewChunkHolder current = this.chunkHolders.get(position); -+ if (current != null) { -+ return current; -+ } -+ -+ current = this.createChunkHolder(position); -+ synchronized (this.chunkHolders) { -+ this.chunkHolders.put(position, current); -+ } -+ -+ return current; -+ } -+ -+ private final AtomicLong entityLoadCounter = new AtomicLong(); -+ -+ public ChunkEntitySlices getOrCreateEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) { -+ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create entity chunk off-main"); -+ ChunkEntitySlices ret; -+ -+ NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ); -+ if (current != null && (ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) { -+ return ret; -+ } -+ -+ final AtomicBoolean isCompleted = new AtomicBoolean(); -+ final Thread waiter = Thread.currentThread(); -+ final Long entityLoadId = Long.valueOf(this.entityLoadCounter.getAndIncrement()); -+ NewChunkHolder.GenericDataLoadTaskCallback loadTask = null; -+ final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ); -+ try { -+ this.addTicketAtLevel(TicketType.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId); -+ final ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ); -+ try { -+ current = this.getOrCreateChunkHolder(chunkX, chunkZ); -+ if ((ret = current.getEntityChunk()) != null && (transientChunk || !ret.isTransient())) { -+ this.removeTicketAtLevel(TicketType.ENTITY_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, entityLoadId); -+ return ret; -+ } -+ -+ if (current.isEntityChunkNBTLoaded()) { -+ isCompleted.setPlain(true); -+ } else { -+ loadTask = current.getOrLoadEntityData((final GenericDataLoadTask.TaskResult result) -> { -+ if (!transientChunk) { -+ isCompleted.set(true); -+ LockSupport.unpark(waiter); -+ } -+ }); -+ final ChunkLoadTask.EntityDataLoadTask entityLoad = current.getEntityDataLoadTask(); -+ -+ if (entityLoad != null && !transientChunk) { -+ entityLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING); -+ } -+ } -+ } finally { -+ this.taskScheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ -+ if (loadTask != null) { -+ loadTask.schedule(); -+ } -+ -+ if (!transientChunk) { -+ // Note: no need to busy wait on the chunk queue, entity load will complete off-main -+ boolean interrupted = false; -+ while (!isCompleted.get()) { -+ interrupted |= Thread.interrupted(); -+ LockSupport.park(); -+ } -+ -+ if (interrupted) { -+ Thread.currentThread().interrupt(); -+ } -+ } -+ -+ // now that the entity data is loaded, we can load it into the world -+ -+ ret = current.loadInEntityChunk(transientChunk); -+ -+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ this.addAndRemoveTickets(chunkKey, -+ TicketType.UNKNOWN, MAX_TICKET_LEVEL, new ChunkPos(chunkX, chunkZ), -+ TicketType.ENTITY_LOAD, MAX_TICKET_LEVEL, entityLoadId -+ ); -+ -+ return ret; -+ } -+ -+ public PoiChunk getPoiChunkIfLoaded(final int chunkX, final int chunkZ, final boolean checkLoadInCallback) { -+ final NewChunkHolder holder = this.getChunkHolder(chunkX, chunkZ); -+ if (holder != null) { -+ final PoiChunk ret = holder.getPoiChunk(); -+ return ret == null || (checkLoadInCallback && !ret.isLoaded()) ? null : ret; -+ } -+ return null; -+ } -+ -+ private final AtomicLong poiLoadCounter = new AtomicLong(); -+ -+ public PoiChunk loadPoiChunk(final int chunkX, final int chunkZ) { -+ TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Cannot create poi chunk off-main"); -+ PoiChunk ret; -+ -+ NewChunkHolder current = this.getChunkHolder(chunkX, chunkZ); -+ if (current != null && (ret = current.getPoiChunk()) != null) { -+ if (!ret.isLoaded()) { -+ ret.load(); -+ } -+ return ret; -+ } -+ -+ final AtomicReference completed = new AtomicReference<>(); -+ final AtomicBoolean isCompleted = new AtomicBoolean(); -+ final Thread waiter = Thread.currentThread(); -+ final Long poiLoadId = Long.valueOf(this.poiLoadCounter.getAndIncrement()); -+ NewChunkHolder.GenericDataLoadTaskCallback loadTask = null; -+ final ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(chunkX, chunkZ); // Folia - use area based lock to reduce contention -+ try { -+ // Folia - use area based lock to reduce contention -+ this.addTicketAtLevel(TicketType.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId); -+ final ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock.Node schedulingLock = this.taskScheduler.schedulingLockArea.lock(chunkX, chunkZ); // Folia - use area based lock to reduce contention -+ try { -+ current = this.getOrCreateChunkHolder(chunkX, chunkZ); -+ if (current.isPoiChunkLoaded()) { -+ this.removeTicketAtLevel(TicketType.POI_LOAD, chunkX, chunkZ, MAX_TICKET_LEVEL, poiLoadId); -+ return current.getPoiChunk(); -+ } -+ -+ loadTask = current.getOrLoadPoiData((final GenericDataLoadTask.TaskResult result) -> { -+ completed.setPlain(result.left()); -+ isCompleted.set(true); -+ LockSupport.unpark(waiter); -+ }); -+ final ChunkLoadTask.PoiDataLoadTask poiLoad = current.getPoiDataLoadTask(); -+ -+ if (poiLoad != null) { -+ poiLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING); -+ } -+ } finally { -+ this.taskScheduler.schedulingLockArea.unlock(schedulingLock); // Folia - use area based lock to reduce contention -+ } -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); // Folia - use area based lock to reduce contention -+ } -+ -+ if (loadTask != null) { -+ loadTask.schedule(); -+ } -+ -+ // Note: no need to busy wait on the chunk queue, poi load will complete off-main -+ -+ boolean interrupted = false; -+ while (!isCompleted.get()) { -+ interrupted |= Thread.interrupted(); -+ LockSupport.park(); -+ } -+ -+ if (interrupted) { -+ Thread.currentThread().interrupt(); -+ } -+ -+ ret = completed.getPlain(); -+ -+ ret.load(); -+ -+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ this.addAndRemoveTickets(chunkKey, -+ TicketType.UNKNOWN, MAX_TICKET_LEVEL, new ChunkPos(chunkX, chunkZ), -+ TicketType.POI_LOAD, MAX_TICKET_LEVEL, poiLoadId -+ ); -+ -+ return ret; -+ } -+ -+ void addChangedStatuses(final List changedFullStatus) { -+ if (changedFullStatus.isEmpty()) { -+ return; -+ } -+ if (!TickThread.isTickThread()) { -+ this.taskScheduler.scheduleChunkTask(() -> { -+ final ArrayDeque pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate; -+ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { -+ pendingFullLoadUpdate.add(changedFullStatus.get(i)); -+ } -+ -+ ChunkHolderManager.this.processPendingFullUpdate(); -+ }, PrioritisedExecutor.Priority.HIGHEST); -+ } else { -+ final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; -+ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { -+ pendingFullLoadUpdate.add(changedFullStatus.get(i)); -+ } -+ } -+ } -+ -+ private void removeChunkHolder(final NewChunkHolder holder) { -+ holder.killed = true; -+ holder.vanillaChunkHolder.onChunkRemove(); -+ this.autoSaveQueue.remove(holder); -+ ChunkSystem.onChunkHolderDelete(this.world, holder.vanillaChunkHolder); -+ synchronized (this.chunkHolders) { -+ this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ)); -+ } -+ } -+ -+ // note: never call while inside the chunk system, this will absolutely break everything -+ public void processUnloads() { -+ TickThread.ensureTickThread("Cannot unload chunks off-main"); -+ -+ if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) { -+ throw new IllegalStateException("Cannot unload chunks recursively"); -+ } -+ final int sectionShift = this.unloadQueue.coordinateShift; // sectionShift <= lock shift -+ final List unloadSectionsForRegion = this.unloadQueue.retrieveForAllRegions(); -+ int unloadCountTentative = 0; -+ for (final ChunkQueue.SectionToUnload sectionRef : unloadSectionsForRegion) { -+ final ChunkQueue.UnloadSection section -+ = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ()); -+ -+ if (section == null) { -+ // removed concurrently -+ continue; -+ } -+ -+ // technically reading the size field is unsafe, and it may be incorrect. -+ // We assume that the error here cumulatively goes away over many ticks. If it did not, then it is possible -+ // for chunks to never unload or not unload fast enough. -+ unloadCountTentative += section.chunks.size(); -+ } -+ -+ if (unloadCountTentative <= 0) { -+ // no work to do -+ return; -+ } -+ -+ // Note: The behaviour that we process ticket updates while holding the lock has been dropped here, as it is racey behavior. -+ // But, we do need to process updates here so that any add ticket that is synchronised before this call does not go missed. -+ this.processTicketUpdates(); -+ -+ final int toUnloadCount = Math.max(50, (int)(unloadCountTentative * 0.05)); -+ int processedCount = 0; -+ -+ for (final ChunkQueue.SectionToUnload sectionRef : unloadSectionsForRegion) { -+ final List stage1 = new ArrayList<>(); -+ final List stage2 = new ArrayList<>(); -+ -+ final int sectionLowerX = sectionRef.sectionX() << sectionShift; -+ final int sectionLowerZ = sectionRef.sectionZ() << sectionShift; -+ -+ // stage 1: set up for stage 2 while holding critical locks -+ ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(sectionLowerX, sectionLowerZ); -+ try { -+ final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ); -+ try { -+ final ChunkQueue.UnloadSection section -+ = this.unloadQueue.getSectionUnsynchronized(sectionRef.sectionX(), sectionRef.sectionZ()); -+ -+ if (section == null) { -+ // removed concurrently -+ continue; -+ } -+ -+ // collect the holders to run stage 1 on -+ final int sectionCount = section.chunks.size(); -+ -+ if ((sectionCount + processedCount) <= toUnloadCount) { -+ // we can just drain the entire section -+ -+ for (final LongIterator iterator = section.chunks.iterator(); iterator.hasNext();) { -+ final NewChunkHolder holder = this.chunkHolders.get(iterator.nextLong()); -+ if (holder == null) { -+ throw new IllegalStateException(); -+ } -+ stage1.add(holder); -+ } -+ -+ // remove section -+ this.unloadQueue.removeSection(sectionRef.sectionX(), sectionRef.sectionZ()); -+ } else { -+ // processedCount + len = toUnloadCount -+ // we cannot drain the entire section -+ for (int i = 0, len = toUnloadCount - processedCount; i < len; ++i) { -+ final NewChunkHolder holder = this.chunkHolders.get(section.chunks.removeFirstLong()); -+ if (holder == null) { -+ throw new IllegalStateException(); -+ } -+ stage1.add(holder); -+ } -+ } -+ -+ // run stage 1 -+ for (int i = 0, len = stage1.size(); i < len; ++i) { -+ final NewChunkHolder chunkHolder = stage1.get(i); -+ if (chunkHolder.isSafeToUnload() != null) { -+ LOGGER.error("Chunkholder " + chunkHolder + " is not safe to unload but is inside the unload queue?"); -+ continue; -+ } -+ final NewChunkHolder.UnloadState state = chunkHolder.unloadStage1(); -+ if (state == null) { -+ // can unload immediately -+ this.removeChunkHolder(chunkHolder); -+ continue; -+ } -+ stage2.add(state); -+ } -+ } finally { -+ this.taskScheduler.schedulingLockArea.unlock(scheduleLock); -+ } -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ -+ // stage 2: invoke expensive unload logic, designed to run without locks thanks to stage 1 -+ final List stage3 = new ArrayList<>(stage2.size()); -+ -+ final Boolean before = this.blockTicketUpdates(); -+ try { -+ for (int i = 0, len = stage2.size(); i < len; ++i) { -+ final NewChunkHolder.UnloadState state = stage2.get(i); -+ final NewChunkHolder holder = state.holder(); -+ -+ holder.unloadStage2(state); -+ stage3.add(holder); -+ } -+ } finally { -+ this.unblockTicketUpdates(before); -+ } -+ -+ // stage 3: actually attempt to remove the chunk holders -+ ticketLock = this.ticketLockArea.lock(sectionLowerX, sectionLowerZ); -+ try { -+ final ReentrantAreaLock.Node scheduleLock = this.taskScheduler.schedulingLockArea.lock(sectionLowerX, sectionLowerZ); -+ try { -+ for (int i = 0, len = stage3.size(); i < len; ++i) { -+ final NewChunkHolder holder = stage3.get(i); -+ -+ if (holder.unloadStage3()) { -+ this.removeChunkHolder(holder); -+ } else { -+ // add cooldown so the next unload check is not immediately next tick -+ this.addTicketAtLevel(TicketType.UNLOAD_COOLDOWN, CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ), MAX_TICKET_LEVEL, Unit.INSTANCE, false); -+ } -+ } -+ } finally { -+ this.taskScheduler.schedulingLockArea.unlock(scheduleLock); -+ } -+ } finally { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ -+ processedCount += stage1.size(); -+ -+ if (processedCount >= toUnloadCount) { -+ break; -+ } -+ } -+ } -+ -+ public enum TicketOperationType { -+ ADD, REMOVE, ADD_IF_REMOVED, ADD_AND_REMOVE -+ } -+ -+ public static record TicketOperation ( -+ TicketOperationType op, long chunkCoord, -+ TicketType ticketType, int ticketLevel, T identifier, -+ TicketType ticketType2, int ticketLevel2, V identifier2 -+ ) { -+ -+ private TicketOperation(TicketOperationType op, long chunkCoord, -+ TicketType ticketType, int ticketLevel, T identifier) { -+ this(op, chunkCoord, ticketType, ticketLevel, identifier, null, 0, null); -+ } -+ -+ public static TicketOperation addOp(final ChunkPos chunk, final TicketType type, final int ticketLevel, final T identifier) { -+ return addOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier); -+ } -+ -+ public static TicketOperation addOp(final int chunkX, final int chunkZ, final TicketType type, final int ticketLevel, final T identifier) { -+ return addOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier); -+ } -+ -+ public static TicketOperation addOp(final long chunk, final TicketType type, final int ticketLevel, final T identifier) { -+ return new TicketOperation<>(TicketOperationType.ADD, chunk, type, ticketLevel, identifier); -+ } -+ -+ public static TicketOperation removeOp(final ChunkPos chunk, final TicketType type, final int ticketLevel, final T identifier) { -+ return removeOp(CoordinateUtils.getChunkKey(chunk), type, ticketLevel, identifier); -+ } -+ -+ public static TicketOperation removeOp(final int chunkX, final int chunkZ, final TicketType type, final int ticketLevel, final T identifier) { -+ return removeOp(CoordinateUtils.getChunkKey(chunkX, chunkZ), type, ticketLevel, identifier); -+ } -+ -+ public static TicketOperation removeOp(final long chunk, final TicketType type, final int ticketLevel, final T identifier) { -+ return new TicketOperation<>(TicketOperationType.REMOVE, chunk, type, ticketLevel, identifier); -+ } -+ -+ public static TicketOperation addIfRemovedOp(final long chunk, -+ final TicketType addType, final int addLevel, final T addIdentifier, -+ final TicketType removeType, final int removeLevel, final V removeIdentifier) { -+ return new TicketOperation<>( -+ TicketOperationType.ADD_IF_REMOVED, chunk, addType, addLevel, addIdentifier, -+ removeType, removeLevel, removeIdentifier -+ ); -+ } -+ -+ public static TicketOperation addAndRemove(final long chunk, -+ final TicketType addType, final int addLevel, final T addIdentifier, -+ final TicketType removeType, final int removeLevel, final V removeIdentifier) { -+ return new TicketOperation<>( -+ TicketOperationType.ADD_AND_REMOVE, chunk, addType, addLevel, addIdentifier, -+ removeType, removeLevel, removeIdentifier -+ ); -+ } -+ } -+ -+ private boolean processTicketOp(TicketOperation operation) { -+ boolean ret = false; -+ switch (operation.op) { -+ case ADD: { -+ ret |= this.addTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier); -+ break; -+ } -+ case REMOVE: { -+ ret |= this.removeTicketAtLevel(operation.ticketType, operation.chunkCoord, operation.ticketLevel, operation.identifier); -+ break; -+ } -+ case ADD_IF_REMOVED: { -+ ret |= this.addIfRemovedTicket( -+ operation.chunkCoord, -+ operation.ticketType, operation.ticketLevel, operation.identifier, -+ operation.ticketType2, operation.ticketLevel2, operation.identifier2 -+ ); -+ break; -+ } -+ case ADD_AND_REMOVE: { -+ ret = true; -+ this.addAndRemoveTickets( -+ operation.chunkCoord, -+ operation.ticketType, operation.ticketLevel, operation.identifier, -+ operation.ticketType2, operation.ticketLevel2, operation.identifier2 -+ ); -+ break; -+ } -+ } -+ -+ return ret; -+ } -+ -+ public void performTicketUpdates(final Collection> operations) { -+ for (final TicketOperation operation : operations) { -+ this.processTicketOp(operation); -+ } -+ } -+ -+ private final ThreadLocal BLOCK_TICKET_UPDATES = ThreadLocal.withInitial(() -> { -+ return Boolean.FALSE; -+ }); -+ -+ public Boolean blockTicketUpdates() { -+ final Boolean ret = BLOCK_TICKET_UPDATES.get(); -+ BLOCK_TICKET_UPDATES.set(Boolean.TRUE); -+ return ret; -+ } -+ -+ public void unblockTicketUpdates(final Boolean before) { -+ BLOCK_TICKET_UPDATES.set(before); -+ } -+ -+ public boolean processTicketUpdates() { -+ return this.processTicketUpdates(true, true, null); -+ } -+ -+ private static final ThreadLocal> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>(); -+ -+ static List getCurrentTicketUpdateScheduling() { -+ return CURRENT_TICKET_UPDATE_SCHEDULING.get(); -+ } -+ -+ private boolean processTicketUpdates(final boolean checkLocks, final boolean processFullUpdates, List scheduledTasks) { -+ TickThread.ensureTickThread("Cannot process ticket levels off-main"); -+ if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) { -+ throw new IllegalStateException("Cannot update ticket level while unloading chunks or updating entity manager"); -+ } -+ -+ List changedFullStatus = null; -+ -+ final boolean isTickThread = TickThread.isTickThread(); -+ -+ boolean ret = false; -+ final boolean canProcessFullUpdates = processFullUpdates & isTickThread; -+ final boolean canProcessScheduling = scheduledTasks == null; -+ -+ if (this.ticketLevelPropagator.hasPendingUpdates()) { -+ if (scheduledTasks == null) { -+ scheduledTasks = new ArrayList<>(); -+ } -+ changedFullStatus = new ArrayList<>(); -+ -+ ret |= this.ticketLevelPropagator.performUpdates( -+ this.ticketLockArea, this.taskScheduler.schedulingLockArea, -+ scheduledTasks, changedFullStatus -+ ); -+ } -+ -+ if (changedFullStatus != null) { -+ this.addChangedStatuses(changedFullStatus); -+ } -+ -+ if (canProcessScheduling && scheduledTasks != null) { -+ for (int i = 0, len = scheduledTasks.size(); i < len; ++i) { -+ scheduledTasks.get(i).schedule(); -+ } -+ } -+ -+ if (canProcessFullUpdates) { -+ ret |= this.processPendingFullUpdate(); -+ } -+ -+ return ret; -+ } -+ -+ // only call on tick thread -+ protected final boolean processPendingFullUpdate() { -+ final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; -+ -+ boolean ret = false; -+ -+ List changedFullStatus = new ArrayList<>(); -+ -+ NewChunkHolder holder; -+ while ((holder = pendingFullLoadUpdate.poll()) != null) { -+ ret |= holder.handleFullStatusChange(changedFullStatus); -+ -+ if (!changedFullStatus.isEmpty()) { -+ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { -+ pendingFullLoadUpdate.add(changedFullStatus.get(i)); -+ } -+ changedFullStatus.clear(); -+ } -+ } -+ -+ return ret; -+ } -+ -+ public JsonObject getDebugJsonForWatchdog() { -+ return this.getDebugJsonNoLock(); -+ } -+ -+ private JsonObject getDebugJsonNoLock() { -+ final JsonObject ret = new JsonObject(); -+ ret.addProperty("current_tick", Long.valueOf(this.currentTick)); -+ -+ final JsonArray unloadQueue = new JsonArray(); -+ ret.add("unload_queue", unloadQueue); -+ ret.addProperty("lock_shift", Integer.valueOf(this.taskScheduler.getChunkSystemLockShift())); -+ ret.addProperty("ticket_shift", Integer.valueOf(ThreadedTicketLevelPropagator.SECTION_SHIFT)); -+ ret.addProperty("region_shift", Integer.valueOf(this.world.getRegionChunkShift())); -+ for (final ChunkQueue.SectionToUnload section : this.unloadQueue.retrieveForAllRegions()) { -+ final JsonObject sectionJson = new JsonObject(); -+ unloadQueue.add(sectionJson); -+ sectionJson.addProperty("sectionX", section.sectionX()); -+ sectionJson.addProperty("sectionZ", section.sectionX()); -+ sectionJson.addProperty("order", section.order()); -+ -+ final JsonArray coordinates = new JsonArray(); -+ sectionJson.add("coordinates", coordinates); -+ -+ final ChunkQueue.UnloadSection actualSection = this.unloadQueue.getSectionUnsynchronized(section.sectionX(), section.sectionZ()); -+ for (final LongIterator iterator = actualSection.chunks.iterator(); iterator.hasNext();) { -+ final long coordinate = iterator.nextLong(); -+ -+ final JsonObject coordinateJson = new JsonObject(); -+ coordinates.add(coordinateJson); -+ -+ coordinateJson.addProperty("chunkX", Integer.valueOf(CoordinateUtils.getChunkX(coordinate))); -+ coordinateJson.addProperty("chunkZ", Integer.valueOf(CoordinateUtils.getChunkZ(coordinate))); -+ } -+ } -+ -+ final JsonArray holders = new JsonArray(); -+ ret.add("chunkholders", holders); -+ -+ for (final NewChunkHolder holder : this.getChunkHolders()) { -+ holders.add(holder.getDebugJson()); -+ } -+ -+ // TODO -+ /* -+ final JsonArray removeTickToChunkExpireTicketCount = new JsonArray(); -+ ret.add("remove_tick_to_chunk_expire_ticket_count", removeTickToChunkExpireTicketCount); -+ -+ for (final Long2ObjectMap.Entry tickEntry : this.removeTickToChunkExpireTicketCount.long2ObjectEntrySet()) { -+ final long tick = tickEntry.getLongKey(); -+ final Long2IntOpenHashMap coordinateToCount = tickEntry.getValue(); -+ -+ final JsonObject tickJson = new JsonObject(); -+ removeTickToChunkExpireTicketCount.add(tickJson); -+ -+ tickJson.addProperty("tick", Long.valueOf(tick)); -+ -+ final JsonArray tickEntries = new JsonArray(); -+ tickJson.add("entries", tickEntries); -+ -+ for (final Long2IntMap.Entry entry : coordinateToCount.long2IntEntrySet()) { -+ final long coordinate = entry.getLongKey(); -+ final int count = entry.getIntValue(); -+ -+ final JsonObject entryJson = new JsonObject(); -+ tickEntries.add(entryJson); -+ -+ entryJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate))); -+ entryJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate))); -+ entryJson.addProperty("count", Integer.valueOf(count)); -+ } -+ } -+ -+ final JsonArray allTicketsJson = new JsonArray(); -+ ret.add("tickets", allTicketsJson); -+ -+ for (final Long2ObjectMap.Entry>> coordinateTickets : this.tickets.long2ObjectEntrySet()) { -+ final long coordinate = coordinateTickets.getLongKey(); -+ final SortedArraySet> tickets = coordinateTickets.getValue(); -+ -+ final JsonObject coordinateJson = new JsonObject(); -+ allTicketsJson.add(coordinateJson); -+ -+ coordinateJson.addProperty("chunkX", Long.valueOf(CoordinateUtils.getChunkX(coordinate))); -+ coordinateJson.addProperty("chunkZ", Long.valueOf(CoordinateUtils.getChunkZ(coordinate))); -+ -+ final JsonArray ticketsSerialized = new JsonArray(); -+ coordinateJson.add("tickets", ticketsSerialized); -+ -+ for (final Ticket ticket : tickets) { -+ final JsonObject ticketSerialized = new JsonObject(); -+ ticketsSerialized.add(ticketSerialized); -+ -+ ticketSerialized.addProperty("type", ticket.getType().toString()); -+ ticketSerialized.addProperty("level", Integer.valueOf(ticket.getTicketLevel())); -+ ticketSerialized.addProperty("identifier", Objects.toString(ticket.key)); -+ ticketSerialized.addProperty("remove_tick", Long.valueOf(ticket.removalTick)); -+ } -+ } -+ */ -+ -+ return ret; -+ } -+ -+ public JsonObject getDebugJson() { -+ return this.getDebugJsonNoLock(); // Folia - use area based lock to reduce contention -+ } -+} -diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLightTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLightTask.java -new file mode 100644 -index 0000000000000000000000000000000000000000..86e618586d2ad9d843ad761b7336bb3073ed4c23 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLightTask.java -@@ -0,0 +1,181 @@ -+package io.papermc.paper.chunk.system.scheduling; -+ -+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; -+import ca.spottedleaf.starlight.common.light.StarLightEngine; -+import ca.spottedleaf.starlight.common.light.StarLightInterface; -+import io.papermc.paper.chunk.system.light.LightQueue; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.chunk.ChunkAccess; -+import net.minecraft.world.level.chunk.status.ChunkStatus; -+import net.minecraft.world.level.chunk.ProtoChunk; -+import org.apache.logging.log4j.LogManager; -+import org.apache.logging.log4j.Logger; -+import java.util.function.BooleanSupplier; -+ -+public final class ChunkLightTask extends ChunkProgressionTask { -+ -+ private static final Logger LOGGER = LogManager.getLogger(); -+ -+ protected final ChunkAccess fromChunk; -+ -+ private final LightTaskPriorityHolder priorityHolder; -+ -+ public ChunkLightTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, -+ final ChunkAccess chunk, final PrioritisedExecutor.Priority priority) { -+ super(scheduler, world, chunkX, chunkZ); -+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ this.priorityHolder = new LightTaskPriorityHolder(priority, this); -+ this.fromChunk = chunk; -+ } -+ -+ @Override -+ public boolean isScheduled() { -+ return this.priorityHolder.isScheduled(); -+ } -+ -+ @Override -+ public ChunkStatus getTargetStatus() { -+ return ChunkStatus.LIGHT; -+ } -+ -+ @Override -+ public void schedule() { -+ this.priorityHolder.schedule(); -+ } -+ -+ @Override -+ public void cancel() { -+ this.priorityHolder.cancel(); -+ } -+ -+ @Override -+ public PrioritisedExecutor.Priority getPriority() { -+ return this.priorityHolder.getPriority(); -+ } -+ -+ @Override -+ public void lowerPriority(final PrioritisedExecutor.Priority priority) { -+ this.priorityHolder.raisePriority(priority); -+ } -+ -+ @Override -+ public void setPriority(final PrioritisedExecutor.Priority priority) { -+ this.priorityHolder.setPriority(priority); -+ } -+ -+ @Override -+ public void raisePriority(final PrioritisedExecutor.Priority priority) { -+ this.priorityHolder.raisePriority(priority); -+ } -+ -+ private static final class LightTaskPriorityHolder extends PriorityHolder { -+ -+ protected final ChunkLightTask task; -+ -+ protected LightTaskPriorityHolder(final PrioritisedExecutor.Priority priority, final ChunkLightTask task) { -+ super(priority); -+ this.task = task; -+ } -+ -+ @Override -+ protected void cancelScheduled() { -+ final ChunkLightTask task = this.task; -+ task.complete(null, null); -+ } -+ -+ @Override -+ protected PrioritisedExecutor.Priority getScheduledPriority() { -+ final ChunkLightTask task = this.task; -+ return task.world.getChunkSource().getLightEngine().theLightEngine.lightQueue.getPriority(task.chunkX, task.chunkZ); -+ } -+ -+ @Override -+ protected void scheduleTask(final PrioritisedExecutor.Priority priority) { -+ final ChunkLightTask task = this.task; -+ final StarLightInterface starLightInterface = task.world.getChunkSource().getLightEngine().theLightEngine; -+ final LightQueue lightQueue = starLightInterface.lightQueue; -+ lightQueue.queueChunkLightTask(new ChunkPos(task.chunkX, task.chunkZ), new LightTask(starLightInterface, task), priority); -+ lightQueue.setPriority(task.chunkX, task.chunkZ, priority); -+ } -+ -+ @Override -+ protected void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority) { -+ final ChunkLightTask task = this.task; -+ final StarLightInterface starLightInterface = task.world.getChunkSource().getLightEngine().theLightEngine; -+ final LightQueue lightQueue = starLightInterface.lightQueue; -+ lightQueue.lowerPriority(task.chunkX, task.chunkZ, priority); -+ } -+ -+ @Override -+ protected void setPriorityScheduled(final PrioritisedExecutor.Priority priority) { -+ final ChunkLightTask task = this.task; -+ final StarLightInterface starLightInterface = task.world.getChunkSource().getLightEngine().theLightEngine; -+ final LightQueue lightQueue = starLightInterface.lightQueue; -+ lightQueue.setPriority(task.chunkX, task.chunkZ, priority); -+ } -+ -+ @Override -+ protected void raisePriorityScheduled(final PrioritisedExecutor.Priority priority) { -+ final ChunkLightTask task = this.task; -+ final StarLightInterface starLightInterface = task.world.getChunkSource().getLightEngine().theLightEngine; -+ final LightQueue lightQueue = starLightInterface.lightQueue; -+ lightQueue.raisePriority(task.chunkX, task.chunkZ, priority); -+ } -+ } -+ -+ private static final class LightTask implements BooleanSupplier { -+ -+ protected final StarLightInterface lightEngine; -+ protected final ChunkLightTask task; -+ -+ public LightTask(final StarLightInterface lightEngine, final ChunkLightTask task) { -+ this.lightEngine = lightEngine; -+ this.task = task; -+ } -+ -+ @Override -+ public boolean getAsBoolean() { -+ final ChunkLightTask task = this.task; -+ // executed on light thread -+ if (!task.priorityHolder.markExecuting()) { -+ // cancelled -+ return false; -+ } -+ -+ try { -+ final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(task.fromChunk); -+ -+ if (task.fromChunk.isLightCorrect() && task.fromChunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { -+ this.lightEngine.forceLoadInChunk(task.fromChunk, emptySections); -+ this.lightEngine.checkChunkEdges(task.chunkX, task.chunkZ); -+ } else { -+ task.fromChunk.setLightCorrect(false); -+ this.lightEngine.lightChunk(task.fromChunk, emptySections); -+ task.fromChunk.setLightCorrect(true); -+ } -+ // we need to advance status -+ if (task.fromChunk instanceof ProtoChunk chunk && chunk.getStatus() == ChunkStatus.LIGHT.getParent()) { -+ chunk.setStatus(ChunkStatus.LIGHT); -+ } -+ } catch (final Throwable thr) { -+ if (!(thr instanceof ThreadDeath)) { -+ LOGGER.fatal("Failed to light chunk " + task.fromChunk.getPos().toString() + " in world '" + this.lightEngine.getWorld().getWorld().getName() + "'", thr); -+ } -+ -+ task.complete(null, thr); -+ -+ if (thr instanceof ThreadDeath) { -+ throw (ThreadDeath)thr; -+ } -+ -+ return true; -+ } -+ -+ task.complete(task.fromChunk, null); -+ return true; -+ } -+ } -+} -diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java -new file mode 100644 -index 0000000000000000000000000000000000000000..5ff994d5af24b0bdd7b3a16e245b2c4100bef3f0 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkLoadTask.java -@@ -0,0 +1,484 @@ -+package io.papermc.paper.chunk.system.scheduling; -+ -+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; -+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; -+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; -+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; -+import ca.spottedleaf.dataconverter.minecraft.MCDataConverter; -+import ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry; -+import com.mojang.logging.LogUtils; -+import io.papermc.paper.chunk.system.io.RegionFileIOThread; -+import io.papermc.paper.chunk.system.poi.PoiChunk; -+import net.minecraft.SharedConstants; -+import net.minecraft.core.registries.Registries; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.server.level.ChunkMap; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.chunk.ChunkAccess; -+import net.minecraft.world.level.chunk.status.ChunkStatus; -+import net.minecraft.world.level.chunk.ProtoChunk; -+import net.minecraft.world.level.chunk.UpgradeData; -+import net.minecraft.world.level.chunk.storage.ChunkSerializer; -+import net.minecraft.world.level.chunk.storage.EntityStorage; -+import net.minecraft.world.level.levelgen.blending.BlendingData; -+import org.slf4j.Logger; -+import java.lang.invoke.VarHandle; -+import java.util.Map; -+import java.util.concurrent.atomic.AtomicInteger; -+import java.util.function.Consumer; -+ -+public final class ChunkLoadTask extends ChunkProgressionTask { -+ -+ private static final Logger LOGGER = LogUtils.getClassLogger(); -+ -+ private final NewChunkHolder chunkHolder; -+ private final ChunkDataLoadTask loadTask; -+ -+ private volatile boolean cancelled; -+ private NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask; -+ private NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask; -+ private GenericDataLoadTask.TaskResult loadResult; -+ private final AtomicInteger taskCountToComplete = new AtomicInteger(3); // one for poi, one for entity, and one for chunk data -+ -+ protected ChunkLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ, -+ final NewChunkHolder chunkHolder, final PrioritisedExecutor.Priority priority) { -+ super(scheduler, world, chunkX, chunkZ); -+ this.chunkHolder = chunkHolder; -+ this.loadTask = new ChunkDataLoadTask(scheduler, world, chunkX, chunkZ, priority); -+ this.loadTask.addCallback((final GenericDataLoadTask.TaskResult result) -> { -+ ChunkLoadTask.this.loadResult = result; // must be before getAndDecrement -+ ChunkLoadTask.this.tryCompleteLoad(); -+ }); -+ } -+ -+ private void tryCompleteLoad() { -+ if (this.taskCountToComplete.decrementAndGet() == 0) { -+ final GenericDataLoadTask.TaskResult result = this.cancelled ? null : this.loadResult; // only after the getAndDecrement -+ ChunkLoadTask.this.complete(result == null ? null : result.left(), result == null ? null : result.right()); -+ } -+ } -+ -+ @Override -+ public ChunkStatus getTargetStatus() { -+ return ChunkStatus.EMPTY; -+ } -+ -+ private boolean scheduled; -+ -+ @Override -+ public boolean isScheduled() { -+ return this.scheduled; -+ } -+ -+ @Override -+ public void schedule() { -+ final NewChunkHolder.GenericDataLoadTaskCallback entityLoadTask; -+ final NewChunkHolder.GenericDataLoadTaskCallback poiLoadTask; -+ -+ final Consumer> scheduleLoadTask = (final GenericDataLoadTask.TaskResult result) -> { -+ ChunkLoadTask.this.tryCompleteLoad(); -+ }; -+ -+ // NOTE: it is IMPOSSIBLE for getOrLoadEntityData/getOrLoadPoiData to complete synchronously, because -+ // they must schedule a task to off main or to on main to complete -+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ if (this.scheduled) { -+ throw new IllegalStateException("schedule() called twice"); -+ } -+ this.scheduled = true; -+ if (this.cancelled) { -+ return; -+ } -+ if (!this.chunkHolder.isEntityChunkNBTLoaded()) { -+ entityLoadTask = this.chunkHolder.getOrLoadEntityData((Consumer)scheduleLoadTask); -+ } else { -+ entityLoadTask = null; -+ this.taskCountToComplete.getAndDecrement(); // we know the chunk load is not done here, as it is not scheduled -+ } -+ -+ if (!this.chunkHolder.isPoiChunkLoaded()) { -+ poiLoadTask = this.chunkHolder.getOrLoadPoiData((Consumer)scheduleLoadTask); -+ } else { -+ poiLoadTask = null; -+ this.taskCountToComplete.getAndDecrement(); // we know the chunk load is not done here, as it is not scheduled -+ } -+ -+ this.entityLoadTask = entityLoadTask; -+ this.poiLoadTask = poiLoadTask; -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ -+ if (entityLoadTask != null) { -+ entityLoadTask.schedule(); -+ } -+ -+ if (poiLoadTask != null) { -+ poiLoadTask.schedule(); -+ } -+ -+ this.loadTask.schedule(false); -+ } -+ -+ @Override -+ public void cancel() { -+ // must be before load task access, so we can synchronise with the writes to the fields -+ final boolean scheduled; -+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ // must read field here, as it may be written later conucrrently - -+ // we need to know if we scheduled _before_ cancellation -+ scheduled = this.scheduled; -+ this.cancelled = true; -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ -+ /* -+ Note: The entityLoadTask/poiLoadTask do not complete when cancelled, -+ so we need to manually try to complete in those cases -+ It is also important to note that we set the cancelled field first, just in case -+ the chunk load task attempts to complete with a non-null value -+ */ -+ -+ if (scheduled) { -+ // since we scheduled, we need to cancel the tasks -+ if (this.entityLoadTask != null) { -+ if (this.entityLoadTask.cancel()) { -+ this.tryCompleteLoad(); -+ } -+ } -+ if (this.poiLoadTask != null) { -+ if (this.poiLoadTask.cancel()) { -+ this.tryCompleteLoad(); -+ } -+ } -+ } else { -+ // since nothing was scheduled, we need to decrement the task count here ourselves -+ -+ // for entity load task -+ this.tryCompleteLoad(); -+ -+ // for poi load task -+ this.tryCompleteLoad(); -+ } -+ this.loadTask.cancel(); -+ } -+ -+ @Override -+ public PrioritisedExecutor.Priority getPriority() { -+ return this.loadTask.getPriority(); -+ } -+ -+ @Override -+ public void lowerPriority(final PrioritisedExecutor.Priority priority) { -+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); -+ if (entityLoad != null) { -+ entityLoad.lowerPriority(priority); -+ } -+ -+ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); -+ -+ if (poiLoad != null) { -+ poiLoad.lowerPriority(priority); -+ } -+ -+ this.loadTask.lowerPriority(priority); -+ } -+ -+ @Override -+ public void setPriority(final PrioritisedExecutor.Priority priority) { -+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); -+ if (entityLoad != null) { -+ entityLoad.setPriority(priority); -+ } -+ -+ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); -+ -+ if (poiLoad != null) { -+ poiLoad.setPriority(priority); -+ } -+ -+ this.loadTask.setPriority(priority); -+ } -+ -+ @Override -+ public void raisePriority(final PrioritisedExecutor.Priority priority) { -+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask(); -+ if (entityLoad != null) { -+ entityLoad.raisePriority(priority); -+ } -+ -+ final PoiDataLoadTask poiLoad = this.chunkHolder.getPoiDataLoadTask(); -+ -+ if (poiLoad != null) { -+ poiLoad.raisePriority(priority); -+ } -+ -+ this.loadTask.raisePriority(priority); -+ } -+ -+ protected static abstract class CallbackDataLoadTask extends GenericDataLoadTask { -+ -+ private TaskResult result; -+ private final MultiThreadedQueue>> waiters = new MultiThreadedQueue<>(); -+ -+ protected volatile boolean completed; -+ protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(CallbackDataLoadTask.class, "completed", boolean.class); -+ -+ protected CallbackDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, -+ final int chunkZ, final RegionFileIOThread.RegionFileType type, -+ final PrioritisedExecutor.Priority priority) { -+ super(scheduler, world, chunkX, chunkZ, type, priority); -+ } -+ -+ public void addCallback(final Consumer> consumer) { -+ if (!this.waiters.add(consumer)) { -+ try { -+ consumer.accept(this.result); -+ } catch (final Throwable throwable) { -+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( -+ "Consumer", ChunkTaskScheduler.stringIfNull(consumer), -+ "Completed throwable", ChunkTaskScheduler.stringIfNull(this.result.right()) -+ ), throwable); -+ if (throwable instanceof ThreadDeath) { -+ throw (ThreadDeath)throwable; -+ } -+ } -+ } -+ } -+ -+ @Override -+ protected void onComplete(final TaskResult result) { -+ if ((boolean)COMPLETED_HANDLE.getAndSet((CallbackDataLoadTask)this, (boolean)true)) { -+ throw new IllegalStateException("Already completed"); -+ } -+ this.result = result; -+ Consumer> consumer; -+ while ((consumer = this.waiters.pollOrBlockAdds()) != null) { -+ try { -+ consumer.accept(result); -+ } catch (final Throwable throwable) { -+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( -+ "Consumer", ChunkTaskScheduler.stringIfNull(consumer), -+ "Completed throwable", ChunkTaskScheduler.stringIfNull(result.right()) -+ ), throwable); -+ if (throwable instanceof ThreadDeath) { -+ throw (ThreadDeath)throwable; -+ } -+ return; -+ } -+ } -+ } -+ } -+ -+ public static final class ChunkDataLoadTask extends CallbackDataLoadTask { -+ protected ChunkDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, -+ final int chunkZ, final PrioritisedExecutor.Priority priority) { -+ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.CHUNK_DATA, priority); -+ } -+ -+ @Override -+ protected boolean hasOffMain() { -+ return true; -+ } -+ -+ @Override -+ protected boolean hasOnMain() { -+ return false; -+ } -+ -+ @Override -+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) { -+ return this.scheduler.loadExecutor.createTask(run, priority); -+ } -+ -+ @Override -+ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ protected TaskResult completeOnMainOffMain(final ChunkAccess data, final Throwable throwable) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ private ProtoChunk getEmptyChunk() { -+ return new ProtoChunk( -+ new ChunkPos(this.chunkX, this.chunkZ), UpgradeData.EMPTY, this.world, -+ this.world.registryAccess().registryOrThrow(Registries.BIOME), (BlendingData)null -+ ); -+ } -+ -+ @Override -+ protected TaskResult runOffMain(final CompoundTag data, final Throwable throwable) { -+ if (throwable != null) { -+ LOGGER.error("Failed to load chunk data for task: " + this.toString() + ", chunk data will be lost", throwable); -+ return new TaskResult<>(this.getEmptyChunk(), null); -+ } -+ -+ if (data == null) { -+ return new TaskResult<>(this.getEmptyChunk(), null); -+ } -+ -+ // need to convert data, and then deserialize it -+ -+ try { -+ final ChunkPos chunkPos = new ChunkPos(this.chunkX, this.chunkZ); -+ final ChunkMap chunkMap = this.world.getChunkSource().chunkMap; -+ // run converters -+ // note: upgradeChunkTag copies the data already -+ final CompoundTag converted = chunkMap.upgradeChunkTag( -+ this.world.getTypeKey(), chunkMap.overworldDataStorage, data, chunkMap.generator.getTypeNameForDataFixer(), -+ chunkPos, this.world -+ ); -+ // deserialize -+ final ChunkSerializer.InProgressChunkHolder chunkHolder = ChunkSerializer.readInProgressChunkHolder( -+ this.world, chunkMap.getPoiManager(), chunkPos, converted -+ ); -+ -+ return new TaskResult<>(chunkHolder.protoChunk, null); -+ } catch (final ThreadDeath death) { -+ throw death; -+ } catch (final Throwable thr2) { -+ LOGGER.error("Failed to parse chunk data for task: " + this.toString() + ", chunk data will be lost", thr2); -+ return new TaskResult<>(this.getEmptyChunk(), null); -+ } -+ } -+ -+ @Override -+ protected TaskResult runOnMain(final ChunkAccess data, final Throwable throwable) { -+ throw new UnsupportedOperationException(); -+ } -+ } -+ -+ public static final class PoiDataLoadTask extends CallbackDataLoadTask { -+ public PoiDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, -+ final int chunkZ, final PrioritisedExecutor.Priority priority) { -+ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.POI_DATA, priority); -+ } -+ -+ @Override -+ protected boolean hasOffMain() { -+ return true; -+ } -+ -+ @Override -+ protected boolean hasOnMain() { -+ return false; -+ } -+ -+ @Override -+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) { -+ return this.scheduler.loadExecutor.createTask(run, priority); -+ } -+ -+ @Override -+ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ protected TaskResult completeOnMainOffMain(final PoiChunk data, final Throwable throwable) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ protected TaskResult runOffMain(CompoundTag data, final Throwable throwable) { -+ if (throwable != null) { -+ LOGGER.error("Failed to load poi data for task: " + this.toString() + ", poi data will be lost", throwable); -+ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); -+ } -+ -+ if (data == null || data.isEmpty()) { -+ // nothing to do -+ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); -+ } -+ -+ try { -+ data = data.copy(); // coming from the I/O thread, so we need to copy -+ // run converters -+ final int dataVersion = !data.contains(SharedConstants.DATA_VERSION_TAG, net.minecraft.nbt.Tag.TAG_ANY_NUMERIC) ? 1945 : data.getInt(SharedConstants.DATA_VERSION_TAG); -+ final CompoundTag converted = MCDataConverter.convertTag( -+ MCTypeRegistry.POI_CHUNK, data, dataVersion, SharedConstants.getCurrentVersion().getDataVersion().getVersion() -+ ); -+ -+ // now we need to parse it -+ return new TaskResult<>(PoiChunk.parse(this.world, this.chunkX, this.chunkZ, converted), null); -+ } catch (final ThreadDeath death) { -+ throw death; -+ } catch (final Throwable thr2) { -+ LOGGER.error("Failed to run parse poi data for task: " + this.toString() + ", poi data will be lost", thr2); -+ return new TaskResult<>(PoiChunk.empty(this.world, this.chunkX, this.chunkZ), null); -+ } -+ } -+ -+ @Override -+ protected TaskResult runOnMain(final PoiChunk data, final Throwable throwable) { -+ throw new UnsupportedOperationException(); -+ } -+ } -+ -+ public static final class EntityDataLoadTask extends CallbackDataLoadTask { -+ -+ public EntityDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, -+ final int chunkZ, final PrioritisedExecutor.Priority priority) { -+ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, priority); -+ } -+ -+ @Override -+ protected boolean hasOffMain() { -+ return true; -+ } -+ -+ @Override -+ protected boolean hasOnMain() { -+ return false; -+ } -+ -+ @Override -+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) { -+ return this.scheduler.loadExecutor.createTask(run, priority); -+ } -+ -+ @Override -+ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ protected TaskResult completeOnMainOffMain(final CompoundTag data, final Throwable throwable) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ protected TaskResult runOffMain(final CompoundTag data, final Throwable throwable) { -+ if (throwable != null) { -+ LOGGER.error("Failed to load entity data for task: " + this.toString() + ", entity data will be lost", throwable); -+ return new TaskResult<>(null, null); -+ } -+ -+ if (data == null || data.isEmpty()) { -+ // nothing to do -+ return new TaskResult<>(null, null); -+ } -+ -+ try { -+ // note: data comes from the I/O thread, so we need to copy it -+ return new TaskResult<>(EntityStorage.upgradeChunkTag(data.copy()), null); -+ } catch (final ThreadDeath death) { -+ throw death; -+ } catch (final Throwable thr2) { -+ LOGGER.error("Failed to run converters for entity data for task: " + this.toString() + ", entity data will be lost", thr2); -+ return new TaskResult<>(null, thr2); -+ } -+ } -+ -+ @Override -+ protected TaskResult runOnMain(final CompoundTag data, final Throwable throwable) { -+ throw new UnsupportedOperationException(); -+ } -+ } -+} -diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkProgressionTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkProgressionTask.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b2341328bb22f08836ef18785dc27393a36ce8d6 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkProgressionTask.java -@@ -0,0 +1,105 @@ -+package io.papermc.paper.chunk.system.scheduling; -+ -+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; -+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; -+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.level.chunk.ChunkAccess; -+import net.minecraft.world.level.chunk.status.ChunkStatus; -+import java.lang.invoke.VarHandle; -+import java.util.Map; -+import java.util.function.BiConsumer; -+ -+public abstract class ChunkProgressionTask { -+ -+ private final MultiThreadedQueue> waiters = new MultiThreadedQueue<>(); -+ private ChunkAccess completedChunk; -+ private Throwable completedThrowable; -+ -+ protected final ChunkTaskScheduler scheduler; -+ protected final ServerLevel world; -+ protected final int chunkX; -+ protected final int chunkZ; -+ -+ protected volatile boolean completed; -+ protected static final VarHandle COMPLETED_HANDLE = ConcurrentUtil.getVarHandle(ChunkProgressionTask.class, "completed", boolean.class); -+ -+ protected ChunkProgressionTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ) { -+ this.scheduler = scheduler; -+ this.world = world; -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ } -+ -+ // Used only for debug json -+ public abstract boolean isScheduled(); -+ -+ // Note: It is the responsibility of the task to set the chunk's status once it has completed -+ public abstract ChunkStatus getTargetStatus(); -+ -+ /* Only executed once */ -+ /* Implementations must be prepared to handle cases where cancel() is called before schedule() */ -+ public abstract void schedule(); -+ -+ /* May be called multiple times */ -+ public abstract void cancel(); -+ -+ public abstract PrioritisedExecutor.Priority getPriority(); -+ -+ /* Schedule lock is always held for the priority update calls */ -+ -+ public abstract void lowerPriority(final PrioritisedExecutor.Priority priority); -+ -+ public abstract void setPriority(final PrioritisedExecutor.Priority priority); -+ -+ public abstract void raisePriority(final PrioritisedExecutor.Priority priority); -+ -+ public final void onComplete(final BiConsumer onComplete) { -+ if (!this.waiters.add(onComplete)) { -+ try { -+ onComplete.accept(this.completedChunk, this.completedThrowable); -+ } catch (final Throwable throwable) { -+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( -+ "Consumer", ChunkTaskScheduler.stringIfNull(onComplete), -+ "Completed throwable", ChunkTaskScheduler.stringIfNull(this.completedThrowable) -+ ), throwable); -+ if (throwable instanceof ThreadDeath) { -+ throw (ThreadDeath)throwable; -+ } -+ } -+ } -+ } -+ -+ protected final void complete(final ChunkAccess chunk, final Throwable throwable) { -+ try { -+ this.complete0(chunk, throwable); -+ } catch (final Throwable thr2) { -+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( -+ "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable) -+ ), thr2); -+ if (thr2 instanceof ThreadDeath) { -+ throw (ThreadDeath)thr2; -+ } -+ } -+ } -+ -+ private void complete0(final ChunkAccess chunk, final Throwable throwable) { -+ if ((boolean)COMPLETED_HANDLE.getAndSet((ChunkProgressionTask)this, (boolean)true)) { -+ throw new IllegalStateException("Already completed"); -+ } -+ this.completedChunk = chunk; -+ this.completedThrowable = throwable; -+ -+ BiConsumer consumer; -+ while ((consumer = this.waiters.pollOrBlockAdds()) != null) { -+ consumer.accept(chunk, throwable); -+ } -+ } -+ -+ @Override -+ public String toString() { -+ return "ChunkProgressionTask{class: " + this.getClass().getName() + ", for world: " + this.world.getWorld().getName() + -+ ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() + -+ ", status: " + this.getTargetStatus().toString() + ", scheduled: " + this.isScheduled() + "}"; -+ } -+} -diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkQueue.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkQueue.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4cc1b3ba6d093a9683dbd8b7fe76106ae391e019 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkQueue.java -@@ -0,0 +1,160 @@ -+package io.papermc.paper.chunk.system.scheduling; -+ -+import it.unimi.dsi.fastutil.HashCommon; -+import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; -+import java.util.ArrayList; -+import java.util.List; -+import java.util.Map; -+import java.util.concurrent.ConcurrentHashMap; -+import java.util.concurrent.atomic.AtomicLong; -+ -+public final class ChunkQueue { -+ -+ public final int coordinateShift; -+ private final AtomicLong orderGenerator = new AtomicLong(); -+ private final ConcurrentHashMap unloadSections = new ConcurrentHashMap<>(); -+ -+ /* -+ * Note: write operations do not occur in parallel for any given section. -+ * Note: coordinateShift <= region shift in order for retrieveForCurrentRegion() to function correctly -+ */ -+ -+ public ChunkQueue(final int coordinateShift) { -+ this.coordinateShift = coordinateShift; -+ } -+ -+ public static record SectionToUnload(int sectionX, int sectionZ, Coordinate coord, long order, int count) {} -+ -+ public List retrieveForAllRegions() { -+ final List ret = new ArrayList<>(); -+ -+ for (final Map.Entry entry : this.unloadSections.entrySet()) { -+ final Coordinate coord = entry.getKey(); -+ final long key = coord.key; -+ final UnloadSection section = entry.getValue(); -+ final int sectionX = Coordinate.x(key); -+ final int sectionZ = Coordinate.z(key); -+ -+ ret.add(new SectionToUnload(sectionX, sectionZ, coord, section.order, section.chunks.size())); -+ } -+ -+ ret.sort((final SectionToUnload s1, final SectionToUnload s2) -> { -+ return Long.compare(s1.order, s2.order); -+ }); -+ -+ return ret; -+ } -+ -+ public UnloadSection getSectionUnsynchronized(final int sectionX, final int sectionZ) { -+ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ)); -+ return this.unloadSections.get(coordinate); -+ } -+ -+ public UnloadSection removeSection(final int sectionX, final int sectionZ) { -+ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ)); -+ return this.unloadSections.remove(coordinate); -+ } -+ -+ // write operation -+ public boolean addChunk(final int chunkX, final int chunkZ) { -+ final int shift = this.coordinateShift; -+ final int sectionX = chunkX >> shift; -+ final int sectionZ = chunkZ >> shift; -+ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ)); -+ final long chunkKey = Coordinate.key(chunkX, chunkZ); -+ -+ UnloadSection section = this.unloadSections.get(coordinate); -+ if (section == null) { -+ section = new UnloadSection(this.orderGenerator.getAndIncrement()); -+ // write operations do not occur in parallel for a given section -+ this.unloadSections.put(coordinate, section); -+ } -+ -+ return section.chunks.add(chunkKey); -+ } -+ -+ // write operation -+ public boolean removeChunk(final int chunkX, final int chunkZ) { -+ final int shift = this.coordinateShift; -+ final int sectionX = chunkX >> shift; -+ final int sectionZ = chunkZ >> shift; -+ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ)); -+ final long chunkKey = Coordinate.key(chunkX, chunkZ); -+ -+ final UnloadSection section = this.unloadSections.get(coordinate); -+ -+ if (section == null) { -+ return false; -+ } -+ -+ if (!section.chunks.remove(chunkKey)) { -+ return false; -+ } -+ -+ if (section.chunks.isEmpty()) { -+ this.unloadSections.remove(coordinate); -+ } -+ -+ return true; -+ } -+ -+ public static final class UnloadSection { -+ -+ public final long order; -+ public final LongLinkedOpenHashSet chunks = new LongLinkedOpenHashSet(); -+ -+ public UnloadSection(final long order) { -+ this.order = order; -+ } -+ } -+ -+ private static final class Coordinate implements Comparable { -+ -+ public final long key; -+ -+ public Coordinate(final long key) { -+ this.key = key; -+ } -+ -+ public Coordinate(final int x, final int z) { -+ this.key = key(x, z); -+ } -+ -+ public static long key(final int x, final int z) { -+ return ((long)z << 32) | (x & 0xFFFFFFFFL); -+ } -+ -+ public static int x(final long key) { -+ return (int)key; -+ } -+ -+ public static int z(final long key) { -+ return (int)(key >>> 32); -+ } -+ -+ @Override -+ public int hashCode() { -+ return (int)HashCommon.mix(this.key); -+ } -+ -+ @Override -+ public boolean equals(final Object obj) { -+ if (this == obj) { -+ return true; -+ } -+ -+ if (!(obj instanceof Coordinate other)) { -+ return false; -+ } -+ -+ return this.key == other.key; -+ } -+ -+ // This class is intended for HashMap/ConcurrentHashMap usage, which do treeify bin nodes if the chain -+ // is too large. So we should implement compareTo to help. -+ @Override -+ public int compareTo(final Coordinate other) { -+ return Long.compare(this.key, other.key); -+ } -+ } -+} -diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java -new file mode 100644 -index 0000000000000000000000000000000000000000..049e20407033073b06fcdeb46c38485f4926d778 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkTaskScheduler.java -@@ -0,0 +1,883 @@ -+package io.papermc.paper.chunk.system.scheduling; -+ -+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; -+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool; -+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue; -+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; -+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; -+import com.mojang.logging.LogUtils; -+import io.papermc.paper.chunk.system.io.RegionFileIOThread; -+import io.papermc.paper.chunk.system.scheduling.queue.RadiusAwarePrioritisedExecutor; -+import io.papermc.paper.configuration.GlobalConfiguration; -+import io.papermc.paper.util.CoordinateUtils; -+import io.papermc.paper.util.TickThread; -+import java.util.function.BooleanSupplier; -+import net.minecraft.CrashReport; -+import net.minecraft.CrashReportCategory; -+import net.minecraft.ReportedException; -+import io.papermc.paper.util.MCUtil; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ChunkMap; -+import net.minecraft.server.level.FullChunkStatus; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.TicketType; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.chunk.ChunkAccess; -+import net.minecraft.world.level.chunk.status.ChunkStatus; -+import net.minecraft.world.level.chunk.LevelChunk; -+import org.bukkit.Bukkit; -+import org.slf4j.Logger; -+import java.io.File; -+import java.util.ArrayDeque; -+import java.util.ArrayList; -+import java.util.Arrays; -+import java.util.Collections; -+import java.util.List; -+import java.util.Map; -+import java.util.Objects; -+import java.util.concurrent.atomic.AtomicBoolean; -+import java.util.concurrent.atomic.AtomicLong; -+import java.util.function.Consumer; -+ -+public final class ChunkTaskScheduler { -+ -+ private static final Logger LOGGER = LogUtils.getClassLogger(); -+ -+ static int newChunkSystemIOThreads; -+ static int newChunkSystemWorkerThreads; -+ static int newChunkSystemGenParallelism; -+ static int newChunkSystemLoadParallelism; -+ -+ public static ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool workerThreads; -+ -+ private static boolean initialised = false; -+ -+ public static void init(final GlobalConfiguration.ChunkSystem config) { -+ if (initialised) { -+ return; -+ } -+ initialised = true; -+ newChunkSystemIOThreads = config.ioThreads; -+ newChunkSystemWorkerThreads = config.workerThreads; -+ if (newChunkSystemIOThreads < 0) { -+ newChunkSystemIOThreads = 1; -+ } else { -+ newChunkSystemIOThreads = Math.max(1, newChunkSystemIOThreads); -+ } -+ int defaultWorkerThreads = Runtime.getRuntime().availableProcessors() / 2; -+ if (defaultWorkerThreads <= 4) { -+ defaultWorkerThreads = defaultWorkerThreads <= 3 ? 1 : 2; -+ } else { -+ defaultWorkerThreads = defaultWorkerThreads / 2; -+ } -+ defaultWorkerThreads = Integer.getInteger("Paper.WorkerThreadCount", Integer.valueOf(defaultWorkerThreads)); -+ -+ if (newChunkSystemWorkerThreads < 0) { -+ newChunkSystemWorkerThreads = defaultWorkerThreads; -+ } else { -+ newChunkSystemWorkerThreads = Math.max(1, newChunkSystemWorkerThreads); -+ } -+ -+ String newChunkSystemGenParallelism = config.genParallelism; -+ if (newChunkSystemGenParallelism.equalsIgnoreCase("default")) { -+ newChunkSystemGenParallelism = "true"; -+ } -+ boolean useParallelGen; -+ if (newChunkSystemGenParallelism.equalsIgnoreCase("on") || newChunkSystemGenParallelism.equalsIgnoreCase("enabled") -+ || newChunkSystemGenParallelism.equalsIgnoreCase("true")) { -+ useParallelGen = true; -+ } else if (newChunkSystemGenParallelism.equalsIgnoreCase("off") || newChunkSystemGenParallelism.equalsIgnoreCase("disabled") -+ || newChunkSystemGenParallelism.equalsIgnoreCase("false")) { -+ useParallelGen = false; -+ } else { -+ throw new IllegalStateException("Invalid option for gen-parallelism: must be one of [on, off, enabled, disabled, true, false, default]"); -+ } -+ -+ ChunkTaskScheduler.newChunkSystemGenParallelism = useParallelGen ? newChunkSystemWorkerThreads : 1; -+ ChunkTaskScheduler.newChunkSystemLoadParallelism = newChunkSystemWorkerThreads; -+ -+ RegionFileIOThread.init(newChunkSystemIOThreads); -+ workerThreads = new ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool( -+ "Paper Chunk System Worker Pool", newChunkSystemWorkerThreads, -+ (final Thread thread, final Integer id) -> { -+ thread.setPriority(Thread.NORM_PRIORITY - 2); -+ thread.setName("Tuinity Chunk System Worker #" + id.intValue()); -+ thread.setUncaughtExceptionHandler(io.papermc.paper.chunk.system.scheduling.NewChunkHolder.CHUNKSYSTEM_UNCAUGHT_EXCEPTION_HANDLER); -+ }, (long)(20.0e6)); // 20ms -+ -+ LOGGER.info("Chunk system is using " + newChunkSystemIOThreads + " I/O threads, " + newChunkSystemWorkerThreads + " worker threads, and gen parallelism of " + ChunkTaskScheduler.newChunkSystemGenParallelism + " threads"); -+ } -+ -+ public final ServerLevel world; -+ public final PrioritisedThreadPool workers; -+ public final RadiusAwarePrioritisedExecutor radiusAwareScheduler; -+ public final PrioritisedThreadPool.PrioritisedPoolExecutor parallelGenExecutor; -+ private final PrioritisedThreadPool.PrioritisedPoolExecutor radiusAwareGenExecutor; -+ public final PrioritisedThreadPool.PrioritisedPoolExecutor loadExecutor; -+ -+ private final PrioritisedThreadedTaskQueue mainThreadExecutor = new PrioritisedThreadedTaskQueue(); -+ -+ public final ChunkHolderManager chunkHolderManager; -+ -+ static { -+ ChunkStatus.EMPTY.writeRadius = 0; -+ ChunkStatus.STRUCTURE_STARTS.writeRadius = 0; -+ ChunkStatus.STRUCTURE_REFERENCES.writeRadius = 0; -+ ChunkStatus.BIOMES.writeRadius = 0; -+ ChunkStatus.NOISE.writeRadius = 0; -+ ChunkStatus.SURFACE.writeRadius = 0; -+ ChunkStatus.CARVERS.writeRadius = 0; -+ ChunkStatus.FEATURES.writeRadius = 1; -+ ChunkStatus.INITIALIZE_LIGHT.writeRadius = 0; -+ ChunkStatus.LIGHT.writeRadius = 2; -+ ChunkStatus.SPAWN.writeRadius = 0; -+ ChunkStatus.FULL.writeRadius = 0; -+ -+ /* -+ It's important that the neighbour read radius is taken into account. If _any_ later status is using some chunk as -+ a neighbour, it must be also safe if that neighbour is being generated. i.e for any status later than FEATURES, -+ for a status to be parallel safe it must not read the block data from its neighbours. -+ */ -+ final List parallelCapableStatus = Arrays.asList( -+ // No-op executor. -+ ChunkStatus.EMPTY, -+ -+ // This is parallel capable, as CB has fixed the concurrency issue with stronghold generations. -+ // Does not touch neighbour chunks. -+ ChunkStatus.STRUCTURE_STARTS, -+ -+ // Surprisingly this is parallel capable. It is simply reading the already-created structure starts -+ // into the structure references for the chunk. So while it reads from it neighbours, its neighbours -+ // will not change, even if executed in parallel. -+ ChunkStatus.STRUCTURE_REFERENCES, -+ -+ // Safe. Mojang runs it in parallel as well. -+ ChunkStatus.BIOMES, -+ -+ // Safe. Mojang runs it in parallel as well. -+ ChunkStatus.NOISE, -+ -+ // Parallel safe. Only touches the target chunk. Biome retrieval is now noise based, which is -+ // completely thread-safe. -+ ChunkStatus.SURFACE, -+ -+ // No global state is modified in the carvers. It only touches the specified chunk. So it is parallel safe. -+ ChunkStatus.CARVERS, -+ -+ // FEATURES is not parallel safe. It writes to neighbours. -+ -+ // no-op executor -+ ChunkStatus.INITIALIZE_LIGHT -+ -+ // LIGHT is not parallel safe. It also doesn't run on the generation executor, so no point. -+ -+ // Only writes to the specified chunk. State is not read by later statuses. Parallel safe. -+ // Note: it may look unsafe because it writes to a worldgenregion, but the region size is always 0 - -+ // see the task margin. -+ // However, if the neighbouring FEATURES chunk is unloaded, but then fails to load in again (for whatever -+ // reason), then it would write to this chunk - and since this status reads blocks from itself, it's not -+ // safe to execute this in parallel. -+ // SPAWN -+ -+ // FULL is executed on main. -+ ); -+ -+ for (final ChunkStatus status : parallelCapableStatus) { -+ status.isParallelCapable = true; -+ } -+ } -+ -+ private static final int[] ACCESS_RADIUS_TABLE = new int[ChunkStatus.getStatusList().size()]; -+ private static final int[] MAX_ACCESS_RADIUS_TABLE = new int[ACCESS_RADIUS_TABLE.length]; -+ static { -+ Arrays.fill(ACCESS_RADIUS_TABLE, -1); -+ } -+ -+ private static int getAccessRadius0(final ChunkStatus genStatus) { -+ if (genStatus == ChunkStatus.EMPTY) { -+ return 0; -+ } -+ -+ final int radius = Math.max(genStatus.loadRange, genStatus.getRange()); -+ int maxRange = radius; -+ -+ for (int dist = 1; dist <= radius; ++dist) { -+ final ChunkStatus requiredNeighbourStatus = ChunkMap.getDependencyStatus(genStatus, radius); -+ final int rad = ACCESS_RADIUS_TABLE[requiredNeighbourStatus.getIndex()]; -+ if (rad == -1) { -+ throw new IllegalStateException(); -+ } -+ -+ maxRange = Math.max(maxRange, dist + rad); -+ } -+ -+ return maxRange; -+ } -+ -+ private static int maxAccessRadius; -+ -+ static { -+ final List statuses = ChunkStatus.getStatusList(); -+ for (int i = 0, len = statuses.size(); i < len; ++i) { -+ ACCESS_RADIUS_TABLE[i] = getAccessRadius0(statuses.get(i)); -+ } -+ int max = 0; -+ for (int i = 0, len = statuses.size(); i < len; ++i) { -+ MAX_ACCESS_RADIUS_TABLE[i] = max = Math.max(ACCESS_RADIUS_TABLE[i], max); -+ } -+ maxAccessRadius = max; -+ } -+ -+ public static int getMaxAccessRadius() { -+ return maxAccessRadius; -+ } -+ -+ public static int getAccessRadius(final ChunkStatus genStatus) { -+ return ACCESS_RADIUS_TABLE[genStatus.getIndex()]; -+ } -+ -+ public static int getAccessRadius(final FullChunkStatus status) { -+ return (status.ordinal() - 1) + getAccessRadius(ChunkStatus.FULL); -+ } -+ -+ final ReentrantAreaLock schedulingLockArea; -+ private final int lockShift; -+ -+ public final int getChunkSystemLockShift() { -+ return this.lockShift; -+ } -+ // Folia end - use area based lock to reduce contention -+ -+ public ChunkTaskScheduler(final ServerLevel world, final PrioritisedThreadPool workers) { -+ this.world = world; -+ this.workers = workers; -+ // must be >= region shift (in paper, doesn't exist) and must be >= ticket propagator section shift -+ // it must be >= region shift since the regioniser assumes ticket updates do not occur in parallel for the region sections -+ // it must be >= ticket propagator section shift so that the ticket propagator can assume that owning a position implies owning -+ // the entire section -+ // we just take the max, as we want the smallest shift that satisfies these properties -+ this.lockShift = Math.max(world.getRegionChunkShift(), ThreadedTicketLevelPropagator.SECTION_SHIFT); -+ this.schedulingLockArea = new ReentrantAreaLock(this.getChunkSystemLockShift()); -+ -+ final String worldName = world.getWorld().getName(); -+ this.parallelGenExecutor = workers.createExecutor("Chunk parallel generation executor for world '" + worldName + "'", Math.max(1, newChunkSystemGenParallelism)); -+ this.radiusAwareGenExecutor = -+ newChunkSystemGenParallelism <= 1 ? this.parallelGenExecutor : workers.createExecutor("Chunk radius aware generator for world '" + worldName + "'", newChunkSystemGenParallelism); -+ this.loadExecutor = workers.createExecutor("Chunk load executor for world '" + worldName + "'", newChunkSystemLoadParallelism); -+ this.radiusAwareScheduler = new RadiusAwarePrioritisedExecutor(this.radiusAwareGenExecutor, Math.max(1, newChunkSystemGenParallelism)); -+ this.chunkHolderManager = new ChunkHolderManager(world, this); -+ } -+ -+ private final AtomicBoolean failedChunkSystem = new AtomicBoolean(); -+ -+ public static Object stringIfNull(final Object obj) { -+ return obj == null ? "null" : obj; -+ } -+ -+ public void unrecoverableChunkSystemFailure(final int chunkX, final int chunkZ, final Map objectsOfInterest, final Throwable thr) { -+ final NewChunkHolder holder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ); -+ LOGGER.error("Chunk system error at chunk (" + chunkX + "," + chunkZ + "), holder: " + holder + ", exception:", new Throwable(thr)); -+ -+ if (this.failedChunkSystem.getAndSet(true)) { -+ return; -+ } -+ -+ final ReportedException reportedException = thr instanceof ReportedException ? (ReportedException)thr : new ReportedException(new CrashReport("Chunk system error", thr)); -+ -+ CrashReportCategory crashReportCategory = reportedException.getReport().addCategory("Chunk system details"); -+ crashReportCategory.setDetail("Chunk coordinate", new ChunkPos(chunkX, chunkZ).toString()); -+ crashReportCategory.setDetail("ChunkHolder", Objects.toString(holder)); -+ crashReportCategory.setDetail("unrecoverableChunkSystemFailure caller thread", Thread.currentThread().getName()); -+ -+ crashReportCategory = reportedException.getReport().addCategory("Chunk System Objects of Interest"); -+ for (final Map.Entry entry : objectsOfInterest.entrySet()) { -+ if (entry.getValue() instanceof Throwable thrObject) { -+ crashReportCategory.setDetailError(Objects.toString(entry.getKey()), thrObject); -+ } else { -+ crashReportCategory.setDetail(Objects.toString(entry.getKey()), Objects.toString(entry.getValue())); -+ } -+ } -+ -+ final Runnable crash = () -> { -+ throw new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException); -+ }; -+ -+ // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions -+ this.scheduleChunkTask(chunkX, chunkZ, crash, PrioritisedExecutor.Priority.BLOCKING); -+ // so, make the main thread pick it up -+ MinecraftServer.chunkSystemCrash = new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException); -+ } -+ -+ public boolean executeMainThreadTask() { -+ TickThread.ensureTickThread("Cannot execute main thread task off-main"); -+ return this.mainThreadExecutor.executeTask(); -+ } -+ -+ public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { -+ this.chunkHolderManager.raisePriority(x, z, priority); -+ } -+ -+ public void setPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { -+ this.chunkHolderManager.setPriority(x, z, priority); -+ } -+ -+ public void lowerPriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { -+ this.chunkHolderManager.lowerPriority(x, z, priority); -+ } -+ -+ private final AtomicLong chunkLoadCounter = new AtomicLong(); -+ -+ public void scheduleTickingState(final int chunkX, final int chunkZ, final FullChunkStatus toStatus, -+ final boolean addTicket, final PrioritisedExecutor.Priority priority, -+ final Consumer onComplete) { -+ if (!TickThread.isTickThread()) { -+ this.scheduleChunkTask(chunkX, chunkZ, () -> { -+ ChunkTaskScheduler.this.scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); -+ }, priority); -+ return; -+ } -+ final int accessRadius = getAccessRadius(toStatus); -+ if (this.chunkHolderManager.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { -+ throw new IllegalStateException("Cannot schedule chunk load during ticket level update"); -+ } -+ if (this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { -+ throw new IllegalStateException("Cannot schedule chunk loading recursively"); -+ } -+ -+ if (toStatus == FullChunkStatus.INACCESSIBLE) { -+ throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status"); -+ } -+ -+ final int minLevel = 33 - (toStatus.ordinal() - 1); -+ final Long chunkReference = addTicket ? Long.valueOf(this.chunkLoadCounter.getAndIncrement()) : null; -+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ -+ if (addTicket) { -+ this.chunkHolderManager.addTicketAtLevel(TicketType.CHUNK_LOAD, chunkKey, minLevel, chunkReference); -+ this.chunkHolderManager.processTicketUpdates(); -+ } -+ -+ final Consumer loadCallback = (final LevelChunk chunk) -> { -+ try { -+ if (onComplete != null) { -+ onComplete.accept(chunk); -+ } -+ } finally { -+ if (addTicket) { -+ ChunkTaskScheduler.this.chunkHolderManager.addAndRemoveTickets(chunkKey, -+ TicketType.UNKNOWN, minLevel, new ChunkPos(chunkKey), -+ TicketType.CHUNK_LOAD, minLevel, chunkReference -+ ); -+ } -+ } -+ }; -+ -+ final boolean scheduled; -+ final LevelChunk chunk; -+ final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); -+ try { -+ final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); -+ try { -+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); -+ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { -+ scheduled = false; -+ chunk = null; -+ } else { -+ final FullChunkStatus currStatus = chunkHolder.getChunkStatus(); -+ if (currStatus.isOrAfter(toStatus)) { -+ scheduled = false; -+ chunk = (LevelChunk)chunkHolder.getCurrentChunk(); -+ } else { -+ scheduled = true; -+ chunk = null; -+ -+ final int radius = toStatus.ordinal() - 1; // 0 -> BORDER, 1 -> TICKING, 2 -> ENTITY_TICKING -+ for (int dz = -radius; dz <= radius; ++dz) { -+ for (int dx = -radius; dx <= radius; ++dx) { -+ final NewChunkHolder neighbour = -+ (dx | dz) == 0 ? chunkHolder : this.chunkHolderManager.getChunkHolder(dx + chunkX, dz + chunkZ); -+ if (neighbour != null) { -+ neighbour.raisePriority(priority); -+ } -+ } -+ } -+ -+ // ticket level should schedule for us -+ chunkHolder.addFullStatusConsumer(toStatus, loadCallback); -+ } -+ } -+ } finally { -+ this.schedulingLockArea.unlock(schedulingLock); -+ } -+ } finally { -+ this.chunkHolderManager.ticketLockArea.unlock(ticketLock); -+ } -+ -+ if (!scheduled) { -+ // couldn't schedule -+ try { -+ loadCallback.accept(chunk); -+ } catch (final ThreadDeath thr) { -+ throw thr; -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to process chunk full status callback", thr); -+ } -+ } -+ } -+ -+ public void scheduleChunkLoad(final int chunkX, final int chunkZ, final boolean gen, final ChunkStatus toStatus, final boolean addTicket, -+ final PrioritisedExecutor.Priority priority, final Consumer onComplete) { -+ if (gen) { -+ this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); -+ return; -+ } -+ this.scheduleChunkLoad(chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> { -+ if (chunk == null) { -+ onComplete.accept(null); -+ } else { -+ if (chunk.getStatus().isOrAfter(toStatus)) { -+ this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); -+ } else { -+ onComplete.accept(null); -+ } -+ } -+ }); -+ } -+ -+ // only appropriate to use with ServerLevel#syncLoadNonFull -+ public boolean beginChunkLoadForNonFullSync(final int chunkX, final int chunkZ, final ChunkStatus toStatus, -+ final PrioritisedExecutor.Priority priority) { -+ final int accessRadius = getAccessRadius(toStatus); -+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ final int minLevel = 33 + ChunkStatus.getDistance(toStatus); -+ final List tasks = new ArrayList<>(); -+ final ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention -+ try { -+ final ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); // Folia - use area based lock to reduce contention -+ try { -+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); -+ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { -+ return false; -+ } else { -+ final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus(); -+ if (genStatus != null && genStatus.isOrAfter(toStatus)) { -+ return true; -+ } else { -+ chunkHolder.raisePriority(priority); -+ -+ if (!chunkHolder.upgradeGenTarget(toStatus)) { -+ this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks); -+ } -+ } -+ } -+ } finally { -+ this.schedulingLockArea.unlock(schedulingLock); -+ } -+ } finally { -+ this.chunkHolderManager.ticketLockArea.unlock(ticketLock); -+ } -+ -+ for (int i = 0, len = tasks.size(); i < len; ++i) { -+ tasks.get(i).schedule(); -+ } -+ -+ return true; -+ } -+ -+ public void scheduleChunkLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus, final boolean addTicket, -+ final PrioritisedExecutor.Priority priority, final Consumer onComplete) { -+ if (!TickThread.isTickThread()) { -+ this.scheduleChunkTask(chunkX, chunkZ, () -> { -+ ChunkTaskScheduler.this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete); -+ }, priority); -+ return; -+ } -+ final int accessRadius = getAccessRadius(toStatus); -+ if (this.chunkHolderManager.ticketLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { -+ throw new IllegalStateException("Cannot schedule chunk load during ticket level update"); -+ } -+ if (this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, accessRadius)) { -+ throw new IllegalStateException("Cannot schedule chunk loading recursively"); -+ } -+ -+ if (toStatus == ChunkStatus.FULL) { -+ this.scheduleTickingState(chunkX, chunkZ, FullChunkStatus.FULL, addTicket, priority, (Consumer)onComplete); -+ return; -+ } -+ -+ final int minLevel = 33 + ChunkStatus.getDistance(toStatus); -+ final Long chunkReference = addTicket ? Long.valueOf(this.chunkLoadCounter.getAndIncrement()) : null; -+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); -+ -+ if (addTicket) { -+ this.chunkHolderManager.addTicketAtLevel(TicketType.CHUNK_LOAD, chunkKey, minLevel, chunkReference); -+ this.chunkHolderManager.processTicketUpdates(); -+ } -+ -+ final Consumer loadCallback = (final ChunkAccess chunk) -> { -+ try { -+ if (onComplete != null) { -+ onComplete.accept(chunk); -+ } -+ } finally { -+ if (addTicket) { -+ ChunkTaskScheduler.this.chunkHolderManager.addAndRemoveTickets(chunkKey, -+ TicketType.UNKNOWN, minLevel, new ChunkPos(chunkKey), -+ TicketType.CHUNK_LOAD, minLevel, chunkReference -+ ); -+ } -+ } -+ }; -+ -+ final List tasks = new ArrayList<>(); -+ -+ final boolean scheduled; -+ final ChunkAccess chunk; -+ final ReentrantAreaLock.Node ticketLock = this.chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ, accessRadius); -+ try { -+ final ReentrantAreaLock.Node schedulingLock = this.schedulingLockArea.lock(chunkX, chunkZ, accessRadius); -+ try { -+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkKey); -+ if (chunkHolder == null || chunkHolder.getTicketLevel() > minLevel) { -+ scheduled = false; -+ chunk = null; -+ } else { -+ final ChunkStatus genStatus = chunkHolder.getCurrentGenStatus(); -+ if (genStatus != null && genStatus.isOrAfter(toStatus)) { -+ scheduled = false; -+ chunk = chunkHolder.getCurrentChunk(); -+ } else { -+ scheduled = true; -+ chunk = null; -+ chunkHolder.raisePriority(priority); -+ -+ if (!chunkHolder.upgradeGenTarget(toStatus)) { -+ this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks); -+ } -+ chunkHolder.addStatusConsumer(toStatus, loadCallback); -+ } -+ } -+ } finally { -+ this.schedulingLockArea.unlock(schedulingLock); -+ } -+ } finally { -+ this.chunkHolderManager.ticketLockArea.unlock(ticketLock); -+ } -+ -+ for (int i = 0, len = tasks.size(); i < len; ++i) { -+ tasks.get(i).schedule(); -+ } -+ -+ if (!scheduled) { -+ // couldn't schedule -+ try { -+ loadCallback.accept(chunk); -+ } catch (final ThreadDeath thr) { -+ throw thr; -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to process chunk status callback", thr); -+ } -+ } -+ } -+ -+ private ChunkProgressionTask createTask(final int chunkX, final int chunkZ, final ChunkAccess chunk, -+ final NewChunkHolder chunkHolder, final List neighbours, -+ final ChunkStatus toStatus, final PrioritisedExecutor.Priority initialPriority) { -+ if (toStatus == ChunkStatus.EMPTY) { -+ return new ChunkLoadTask(this, this.world, chunkX, chunkZ, chunkHolder, initialPriority); -+ } -+ if (toStatus == ChunkStatus.LIGHT) { -+ return new ChunkLightTask(this, this.world, chunkX, chunkZ, chunk, initialPriority); -+ } -+ if (toStatus == ChunkStatus.FULL) { -+ return new ChunkFullTask(this, this.world, chunkX, chunkZ, chunkHolder, chunk, initialPriority); -+ } -+ -+ return new ChunkUpgradeGenericStatusTask(this, this.world, chunkX, chunkZ, chunk, neighbours, toStatus, initialPriority); -+ } -+ -+ ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus, final NewChunkHolder chunkHolder, -+ final List allTasks) { -+ return this.schedule(chunkX, chunkZ, targetStatus, chunkHolder, allTasks, chunkHolder.getEffectivePriority()); -+ } -+ -+ // rets new task scheduled for the _specified_ chunk -+ // note: this must hold the scheduling lock -+ // minPriority is only used to pass the priority through to neighbours, as priority calculation has not yet been done -+ // schedule will ignore the generation target, so it should be checked by the caller to ensure the target is not regressed! -+ private ChunkProgressionTask schedule(final int chunkX, final int chunkZ, final ChunkStatus targetStatus, -+ final NewChunkHolder chunkHolder, final List allTasks, -+ final PrioritisedExecutor.Priority minPriority) { -+ if (!this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, getAccessRadius(targetStatus))) { -+ throw new IllegalStateException("Not holding scheduling lock"); -+ } -+ -+ if (chunkHolder.hasGenerationTask()) { -+ chunkHolder.upgradeGenTarget(targetStatus); -+ return null; -+ } -+ -+ final PrioritisedExecutor.Priority requestedPriority = PrioritisedExecutor.Priority.max(minPriority, chunkHolder.getEffectivePriority()); -+ final ChunkStatus currentGenStatus = chunkHolder.getCurrentGenStatus(); -+ final ChunkAccess chunk = chunkHolder.getCurrentChunk(); -+ -+ if (currentGenStatus == null) { -+ // not yet loaded -+ final ChunkProgressionTask task = this.createTask( -+ chunkX, chunkZ, chunk, chunkHolder, Collections.emptyList(), ChunkStatus.EMPTY, requestedPriority -+ ); -+ -+ allTasks.add(task); -+ -+ final List chunkHolderNeighbours = new ArrayList<>(1); -+ chunkHolderNeighbours.add(chunkHolder); -+ -+ chunkHolder.setGenerationTarget(targetStatus); -+ chunkHolder.setGenerationTask(task, ChunkStatus.EMPTY, chunkHolderNeighbours); -+ -+ return task; -+ } -+ -+ if (currentGenStatus.isOrAfter(targetStatus)) { -+ // nothing to do -+ return null; -+ } -+ -+ // we know for sure now that we want to schedule _something_, so set the target -+ chunkHolder.setGenerationTarget(targetStatus); -+ -+ final ChunkStatus chunkRealStatus = chunk.getStatus(); -+ final ChunkStatus toStatus = currentGenStatus.getNextStatus(); -+ -+ // if this chunk has already generated up to or past the specified status, then we don't -+ // need the neighbours AT ALL. -+ final int neighbourReadRadius = chunkRealStatus.isOrAfter(toStatus) ? toStatus.loadRange : toStatus.getRange(); -+ -+ boolean unGeneratedNeighbours = false; -+ -+ // copied from MCUtil.getSpiralOutChunks -+ for (int r = 1; r <= neighbourReadRadius; r++) { -+ int x = -r; -+ int z = r; -+ -+ // Iterates the edge of half of the box; then negates for other half. -+ while (x <= r && z > -r) { -+ final int radius = Math.max(Math.abs(x), Math.abs(z)); -+ final ChunkStatus requiredNeighbourStatus = ChunkMap.getDependencyStatus(toStatus, radius); -+ -+ unGeneratedNeighbours |= this.checkNeighbour( -+ chunkX + x, chunkZ + z, requiredNeighbourStatus, chunkHolder, allTasks, requestedPriority -+ ); -+ unGeneratedNeighbours |= this.checkNeighbour( -+ chunkX - x, chunkZ - z, requiredNeighbourStatus, chunkHolder, allTasks, requestedPriority -+ ); -+ -+ if (x < r) { -+ x++; -+ } else { -+ z--; -+ } -+ } -+ } -+ -+ if (unGeneratedNeighbours) { -+ // can't schedule, but neighbour completion will schedule for us when they're ALL done -+ -+ // propagate our priority to neighbours -+ chunkHolder.recalculateNeighbourPriorities(); -+ return null; -+ } -+ -+ // need to gather neighbours -+ -+ final List neighbours; -+ final List chunkHolderNeighbours; -+ if (neighbourReadRadius <= 0) { -+ neighbours = new ArrayList<>(1); -+ chunkHolderNeighbours = new ArrayList<>(1); -+ neighbours.add(chunk); -+ chunkHolderNeighbours.add(chunkHolder); -+ } else { -+ // the iteration order is _very_ important, as all generation statuses expect a certain order such that: -+ // chunkAtRelative = neighbours.get(relX + relZ * (2 * radius + 1)) -+ neighbours = new ArrayList<>((2 * neighbourReadRadius + 1) * (2 * neighbourReadRadius + 1)); -+ chunkHolderNeighbours = new ArrayList<>((2 * neighbourReadRadius + 1) * (2 * neighbourReadRadius + 1)); -+ for (int dz = -neighbourReadRadius; dz <= neighbourReadRadius; ++dz) { -+ for (int dx = -neighbourReadRadius; dx <= neighbourReadRadius; ++dx) { -+ final NewChunkHolder holder = (dx | dz) == 0 ? chunkHolder : this.chunkHolderManager.getChunkHolder(dx + chunkX, dz + chunkZ); -+ neighbours.add(holder.getChunkForNeighbourAccess()); -+ chunkHolderNeighbours.add(holder); -+ } -+ } -+ } -+ -+ final ChunkProgressionTask task = this.createTask(chunkX, chunkZ, chunk, chunkHolder, neighbours, toStatus, chunkHolder.getEffectivePriority()); -+ allTasks.add(task); -+ -+ chunkHolder.setGenerationTask(task, toStatus, chunkHolderNeighbours); -+ -+ return task; -+ } -+ -+ // rets true if the neighbour is not at the required status, false otherwise -+ private boolean checkNeighbour(final int chunkX, final int chunkZ, final ChunkStatus requiredStatus, final NewChunkHolder center, -+ final List tasks, final PrioritisedExecutor.Priority minPriority) { -+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ); -+ -+ if (chunkHolder == null) { -+ throw new IllegalStateException("Missing chunkholder when required"); -+ } -+ -+ final ChunkStatus holderStatus = chunkHolder.getCurrentGenStatus(); -+ if (holderStatus != null && holderStatus.isOrAfter(requiredStatus)) { -+ return false; -+ } -+ -+ if (chunkHolder.hasFailedGeneration()) { -+ return true; -+ } -+ -+ center.addGenerationBlockingNeighbour(chunkHolder); -+ chunkHolder.addWaitingNeighbour(center, requiredStatus); -+ -+ if (chunkHolder.upgradeGenTarget(requiredStatus)) { -+ return true; -+ } -+ -+ // not at status required, so we need to schedule its generation -+ this.schedule( -+ chunkX, chunkZ, requiredStatus, chunkHolder, tasks, minPriority -+ ); -+ -+ return true; -+ } -+ -+ /** -+ * @deprecated Chunk tasks must be tied to coordinates in the future -+ */ -+ @Deprecated -+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run) { -+ return this.scheduleChunkTask(run, PrioritisedExecutor.Priority.NORMAL); -+ } -+ -+ /** -+ * @deprecated Chunk tasks must be tied to coordinates in the future -+ */ -+ @Deprecated -+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final PrioritisedExecutor.Priority priority) { -+ return this.mainThreadExecutor.queueRunnable(run, priority); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) { -+ return this.createChunkTask(chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run, -+ final PrioritisedExecutor.Priority priority) { -+ return this.mainThreadExecutor.createTask(run, priority); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) { -+ return this.mainThreadExecutor.queueRunnable(run); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run, -+ final PrioritisedExecutor.Priority priority) { -+ return this.mainThreadExecutor.queueRunnable(run, priority); -+ } -+ -+ public void executeTasksUntil(final BooleanSupplier exit) { -+ if (Bukkit.isPrimaryThread()) { -+ this.mainThreadExecutor.executeConditionally(exit); -+ } else { -+ long counter = 1L; -+ while (!exit.getAsBoolean()) { -+ counter = ConcurrentUtil.linearLongBackoff(counter, 100_000L, 5_000_000L); // 100us, 5ms -+ } -+ } -+ } -+ -+ public boolean halt(final boolean sync, final long maxWaitNS) { -+ this.radiusAwareGenExecutor.halt(); -+ this.parallelGenExecutor.halt(); -+ this.loadExecutor.halt(); -+ final long time = System.nanoTime(); -+ if (sync) { -+ for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) { -+ if ( -+ !this.radiusAwareGenExecutor.isActive() && -+ !this.parallelGenExecutor.isActive() && -+ !this.loadExecutor.isActive() -+ ) { -+ return true; -+ } -+ if ((System.nanoTime() - time) >= maxWaitNS) { -+ return false; -+ } -+ } -+ } -+ -+ return true; -+ } -+ -+ public static final ArrayDeque WAITING_CHUNKS = new ArrayDeque<>(); // stack -+ -+ public static final class ChunkInfo { -+ -+ public final int chunkX; -+ public final int chunkZ; -+ public final ServerLevel world; -+ -+ public ChunkInfo(final int chunkX, final int chunkZ, final ServerLevel world) { -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ this.world = world; -+ } -+ -+ @Override -+ public String toString() { -+ return "[( " + this.chunkX + "," + this.chunkZ + ") in '" + this.world.getWorld().getName() + "']"; -+ } -+ } -+ -+ public static void pushChunkWait(final ServerLevel world, final int chunkX, final int chunkZ) { -+ synchronized (WAITING_CHUNKS) { -+ WAITING_CHUNKS.push(new ChunkInfo(chunkX, chunkZ, world)); -+ } -+ } -+ -+ public static void popChunkWait() { -+ synchronized (WAITING_CHUNKS) { -+ WAITING_CHUNKS.pop(); -+ } -+ } -+ -+ public static ChunkInfo[] getChunkInfos() { -+ synchronized (WAITING_CHUNKS) { -+ return WAITING_CHUNKS.toArray(new ChunkInfo[0]); -+ } -+ } -+ -+ public static void dumpAllChunkLoadInfo(final boolean longPrint) { -+ final ChunkInfo[] chunkInfos = getChunkInfos(); -+ if (chunkInfos.length > 0) { -+ LOGGER.error("Chunk wait task info below: "); -+ for (final ChunkInfo chunkInfo : chunkInfos) { -+ final NewChunkHolder holder = chunkInfo.world.chunkTaskScheduler.chunkHolderManager.getChunkHolder(chunkInfo.chunkX, chunkInfo.chunkZ); -+ LOGGER.error("Chunk wait: " + chunkInfo); -+ LOGGER.error("Chunk holder: " + holder); -+ } -+ -+ if (longPrint) { -+ final File file = new File(new File(new File("."), "debug"), "chunks-watchdog.txt"); -+ LOGGER.error("Writing chunk information dump to " + file); -+ try { -+ MCUtil.dumpChunks(file, true); -+ LOGGER.error("Successfully written chunk information!"); -+ } catch (final Throwable thr) { -+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr); -+ } -+ } -+ } -+ } -+} -diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkUpgradeGenericStatusTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkUpgradeGenericStatusTask.java -new file mode 100644 -index 0000000000000000000000000000000000000000..bd0d0c4436f357392e13d9efd4412886385a6924 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkUpgradeGenericStatusTask.java -@@ -0,0 +1,214 @@ -+package io.papermc.paper.chunk.system.scheduling; -+ -+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; -+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; -+import com.mojang.logging.LogUtils; -+import net.minecraft.server.level.ChunkMap; -+import net.minecraft.server.level.ChunkResult; -+import net.minecraft.server.level.ServerChunkCache; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.level.chunk.ChunkAccess; -+import net.minecraft.world.level.chunk.status.ChunkStatus; -+import net.minecraft.world.level.chunk.ProtoChunk; -+import net.minecraft.world.level.chunk.status.WorldGenContext; -+import org.slf4j.Logger; -+import java.lang.invoke.VarHandle; -+import java.util.List; -+import java.util.Map; -+import java.util.concurrent.CompletableFuture; -+ -+public final class ChunkUpgradeGenericStatusTask extends ChunkProgressionTask implements Runnable { -+ -+ private static final Logger LOGGER = LogUtils.getClassLogger(); -+ -+ protected final ChunkAccess fromChunk; -+ protected final ChunkStatus fromStatus; -+ protected final ChunkStatus toStatus; -+ protected final List neighbours; -+ -+ protected final PrioritisedExecutor.PrioritisedTask generateTask; -+ -+ public ChunkUpgradeGenericStatusTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, -+ final int chunkZ, final ChunkAccess chunk, final List neighbours, -+ final ChunkStatus toStatus, final PrioritisedExecutor.Priority priority) { -+ super(scheduler, world, chunkX, chunkZ); -+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ this.fromChunk = chunk; -+ this.fromStatus = chunk.getStatus(); -+ this.toStatus = toStatus; -+ this.neighbours = neighbours; -+ if (this.toStatus.isParallelCapable) { -+ this.generateTask = this.scheduler.parallelGenExecutor.createTask(this, priority); -+ } else { -+ this.generateTask = this.scheduler.radiusAwareScheduler.createTask(chunkX, chunkZ, this.toStatus.writeRadius, this, priority); -+ } -+ } -+ -+ @Override -+ public ChunkStatus getTargetStatus() { -+ return this.toStatus; -+ } -+ -+ private boolean isEmptyTask() { -+ // must use fromStatus here to avoid any race condition with run() overwriting the status -+ final boolean generation = !this.fromStatus.isOrAfter(this.toStatus); -+ return (generation && this.toStatus.isEmptyGenStatus()) || (!generation && this.toStatus.isEmptyLoadStatus()); -+ } -+ -+ @Override -+ public void run() { -+ final ChunkAccess chunk = this.fromChunk; -+ -+ final ServerChunkCache serverChunkCache = this.world.chunkSource; -+ final ChunkMap chunkMap = serverChunkCache.chunkMap; -+ -+ final CompletableFuture completeFuture; -+ -+ final boolean generation; -+ boolean completing = false; -+ -+ // note: should optimise the case where the chunk does not need to execute the status, because -+ // schedule() calls this synchronously if it will run through that path -+ -+ final WorldGenContext ctx = new WorldGenContext( -+ this.world, -+ chunkMap.generator, -+ chunkMap.getWorldGenContext().structureManager(), -+ serverChunkCache.getLightEngine() -+ ); -+ try { -+ generation = !chunk.getStatus().isOrAfter(this.toStatus); -+ if (generation) { -+ if (this.toStatus.isEmptyGenStatus()) { -+ if (chunk instanceof ProtoChunk) { -+ ((ProtoChunk)chunk).setStatus(this.toStatus); -+ } -+ completing = true; -+ this.complete(chunk, null); -+ return; -+ } -+ completeFuture = this.toStatus.generate(ctx, Runnable::run, null, this.neighbours) -+ .whenComplete((final ChunkAccess either, final Throwable throwable) -> { -+ if (either instanceof ProtoChunk proto) { -+ proto.setStatus(ChunkUpgradeGenericStatusTask.this.toStatus); -+ } -+ } -+ ); -+ } else { -+ if (this.toStatus.isEmptyLoadStatus()) { -+ completing = true; -+ this.complete(chunk, null); -+ return; -+ } -+ completeFuture = this.toStatus.load(ctx, null, chunk); -+ } -+ } catch (final Throwable throwable) { -+ if (!completing) { -+ this.complete(null, throwable); -+ -+ if (throwable instanceof ThreadDeath) { -+ throw (ThreadDeath)throwable; -+ } -+ return; -+ } -+ -+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( -+ "Target status", ChunkTaskScheduler.stringIfNull(this.toStatus), -+ "From status", ChunkTaskScheduler.stringIfNull(this.fromStatus), -+ "Generation task", this -+ ), throwable); -+ -+ if (!(throwable instanceof ThreadDeath)) { -+ LOGGER.error("Failed to complete status for chunk: status:" + this.toStatus + ", chunk: (" + this.chunkX + "," + this.chunkZ + "), world: " + this.world.getWorld().getName(), throwable); -+ } else { -+ // ensure the chunk system can respond, then die -+ throw (ThreadDeath)throwable; -+ } -+ return; -+ } -+ -+ if (!completeFuture.isDone() && !this.toStatus.warnedAboutNoImmediateComplete.getAndSet(true)) { -+ LOGGER.warn("Future status not complete after scheduling: " + this.toStatus.toString() + ", generate: " + generation); -+ } -+ -+ final ChunkAccess newChunk; -+ -+ try { -+ newChunk = completeFuture.join(); -+ } catch (final Throwable throwable) { -+ this.complete(null, throwable); -+ // ensure the chunk system can respond, then die -+ if (throwable instanceof ThreadDeath) { -+ throw (ThreadDeath)throwable; -+ } -+ return; -+ } -+ -+ if (newChunk == null) { -+ this.complete(null, new IllegalStateException("Chunk for status: " + ChunkUpgradeGenericStatusTask.this.toStatus.toString() + ", generation: " + generation + " should not be null! Future: " + completeFuture).fillInStackTrace()); -+ return; -+ } -+ -+ this.complete(newChunk, null); -+ } -+ -+ protected volatile boolean scheduled; -+ protected static final VarHandle SCHEDULED_HANDLE = ConcurrentUtil.getVarHandle(ChunkUpgradeGenericStatusTask.class, "scheduled", boolean.class); -+ -+ @Override -+ public boolean isScheduled() { -+ return this.scheduled; -+ } -+ -+ @Override -+ public void schedule() { -+ if ((boolean)SCHEDULED_HANDLE.getAndSet((ChunkUpgradeGenericStatusTask)this, true)) { -+ throw new IllegalStateException("Cannot double call schedule()"); -+ } -+ if (this.isEmptyTask()) { -+ if (this.generateTask.cancel()) { -+ this.run(); -+ } -+ } else { -+ this.generateTask.queue(); -+ } -+ } -+ -+ @Override -+ public void cancel() { -+ if (this.generateTask.cancel()) { -+ this.complete(null, null); -+ } -+ } -+ -+ @Override -+ public PrioritisedExecutor.Priority getPriority() { -+ return this.generateTask.getPriority(); -+ } -+ -+ @Override -+ public void lowerPriority(final PrioritisedExecutor.Priority priority) { -+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ this.generateTask.lowerPriority(priority); -+ } -+ -+ @Override -+ public void setPriority(final PrioritisedExecutor.Priority priority) { -+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ this.generateTask.setPriority(priority); -+ } -+ -+ @Override -+ public void raisePriority(final PrioritisedExecutor.Priority priority) { -+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ this.generateTask.raisePriority(priority); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/GenericDataLoadTask.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/GenericDataLoadTask.java -new file mode 100644 -index 0000000000000000000000000000000000000000..396d72c00e47cf1669ae20dc839c1c961b1f262a ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/GenericDataLoadTask.java -@@ -0,0 +1,746 @@ -+package io.papermc.paper.chunk.system.scheduling; -+ -+import ca.spottedleaf.concurrentutil.completable.Completable; -+import ca.spottedleaf.concurrentutil.executor.Cancellable; -+import ca.spottedleaf.concurrentutil.executor.standard.DelayedPrioritisedTask; -+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; -+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; -+import com.mojang.logging.LogUtils; -+import io.papermc.paper.chunk.system.io.RegionFileIOThread; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.server.level.ServerLevel; -+import org.slf4j.Logger; -+import java.lang.invoke.VarHandle; -+import java.util.Map; -+import java.util.concurrent.atomic.AtomicBoolean; -+import java.util.concurrent.atomic.AtomicLong; -+import java.util.function.BiConsumer; -+ -+public abstract class GenericDataLoadTask { -+ -+ private static final Logger LOGGER = LogUtils.getClassLogger(); -+ -+ protected static final CompoundTag CANCELLED_DATA = new CompoundTag(); -+ -+ // reference count is the upper 32 bits -+ protected final AtomicLong stageAndReferenceCount = new AtomicLong(STAGE_NOT_STARTED); -+ -+ protected static final long STAGE_MASK = 0xFFFFFFFFL; -+ protected static final long STAGE_CANCELLED = 0xFFFFFFFFL; -+ protected static final long STAGE_NOT_STARTED = 0L; -+ protected static final long STAGE_LOADING = 1L; -+ protected static final long STAGE_PROCESSING = 2L; -+ protected static final long STAGE_COMPLETED = 3L; -+ -+ // for loading data off disk -+ protected final LoadDataFromDiskTask loadDataFromDiskTask; -+ // processing off-main -+ protected final PrioritisedExecutor.PrioritisedTask processOffMain; -+ // processing on-main -+ protected final PrioritisedExecutor.PrioritisedTask processOnMain; -+ -+ protected final ChunkTaskScheduler scheduler; -+ protected final ServerLevel world; -+ protected final int chunkX; -+ protected final int chunkZ; -+ protected final RegionFileIOThread.RegionFileType type; -+ -+ public GenericDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, -+ final int chunkZ, final RegionFileIOThread.RegionFileType type, -+ final PrioritisedExecutor.Priority priority) { -+ this.scheduler = scheduler; -+ this.world = world; -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ this.type = type; -+ -+ final ProcessOnMainTask mainTask; -+ if (this.hasOnMain()) { -+ mainTask = new ProcessOnMainTask(); -+ this.processOnMain = this.createOnMain(mainTask, priority); -+ } else { -+ mainTask = null; -+ this.processOnMain = null; -+ } -+ -+ final ProcessOffMainTask offMainTask; -+ if (this.hasOffMain()) { -+ offMainTask = new ProcessOffMainTask(mainTask); -+ this.processOffMain = this.createOffMain(offMainTask, priority); -+ } else { -+ offMainTask = null; -+ this.processOffMain = null; -+ } -+ -+ if (this.processOffMain == null && this.processOnMain == null) { -+ throw new IllegalStateException("Illegal class implementation: " + this.getClass().getName() + ", should be able to schedule at least one task!"); -+ } -+ -+ this.loadDataFromDiskTask = new LoadDataFromDiskTask(world, chunkX, chunkZ, type, new DataLoadCallback(offMainTask, mainTask), priority); -+ } -+ -+ public static final record TaskResult(L left, R right) {} -+ -+ protected abstract boolean hasOffMain(); -+ -+ protected abstract boolean hasOnMain(); -+ -+ protected abstract PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority); -+ -+ protected abstract PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.Priority priority); -+ -+ protected abstract TaskResult runOffMain(final CompoundTag data, final Throwable throwable); -+ -+ protected abstract TaskResult runOnMain(final OnMain data, final Throwable throwable); -+ -+ protected abstract void onComplete(final TaskResult result); -+ -+ protected abstract TaskResult completeOnMainOffMain(final OnMain data, final Throwable throwable); -+ -+ @Override -+ public String toString() { -+ return "GenericDataLoadTask{class: " + this.getClass().getName() + ", world: " + this.world.getWorld().getName() + -+ ", chunk: (" + this.chunkX + "," + this.chunkZ + "), hashcode: " + System.identityHashCode(this) + ", priority: " + this.getPriority() + -+ ", type: " + this.type.toString() + "}"; -+ } -+ -+ public PrioritisedExecutor.Priority getPriority() { -+ if (this.processOnMain != null) { -+ return this.processOnMain.getPriority(); -+ } else { -+ return this.processOffMain.getPriority(); -+ } -+ } -+ -+ public void lowerPriority(final PrioritisedExecutor.Priority priority) { -+ // can't lower I/O tasks, we don't know what they affect -+ if (this.processOffMain != null) { -+ this.processOffMain.lowerPriority(priority); -+ } -+ if (this.processOnMain != null) { -+ this.processOnMain.lowerPriority(priority); -+ } -+ } -+ -+ public void setPriority(final PrioritisedExecutor.Priority priority) { -+ // can't lower I/O tasks, we don't know what they affect -+ this.loadDataFromDiskTask.raisePriority(priority); -+ if (this.processOffMain != null) { -+ this.processOffMain.setPriority(priority); -+ } -+ if (this.processOnMain != null) { -+ this.processOnMain.setPriority(priority); -+ } -+ } -+ -+ public void raisePriority(final PrioritisedExecutor.Priority priority) { -+ // can't lower I/O tasks, we don't know what they affect -+ this.loadDataFromDiskTask.raisePriority(priority); -+ if (this.processOffMain != null) { -+ this.processOffMain.raisePriority(priority); -+ } -+ if (this.processOnMain != null) { -+ this.processOnMain.raisePriority(priority); -+ } -+ } -+ -+ // returns whether scheduleNow() needs to be called -+ public boolean schedule(final boolean delay) { -+ if (this.stageAndReferenceCount.get() != STAGE_NOT_STARTED || -+ !this.stageAndReferenceCount.compareAndSet(STAGE_NOT_STARTED, (1L << 32) | STAGE_LOADING)) { -+ // try and increment reference count -+ int failures = 0; -+ for (long curr = this.stageAndReferenceCount.get();;) { -+ if ((curr & STAGE_MASK) == STAGE_CANCELLED || (curr & STAGE_MASK) == STAGE_COMPLETED) { -+ // cancelled or completed, nothing to do here -+ return false; -+ } -+ -+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, curr + (1L << 32)))) { -+ // successful -+ return false; -+ } -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ if (!delay) { -+ this.scheduleNow(); -+ return false; -+ } -+ return true; -+ } -+ -+ public void scheduleNow() { -+ this.loadDataFromDiskTask.schedule(); // will schedule the rest -+ } -+ -+ // assumes the current stage cannot be completed -+ // returns false if cancelled, returns true if can proceed -+ private boolean advanceStage(final long expect, final long to) { -+ int failures = 0; -+ for (long curr = this.stageAndReferenceCount.get();;) { -+ if ((curr & STAGE_MASK) != expect) { -+ // must be cancelled -+ return false; -+ } -+ -+ final long newVal = (curr & ~STAGE_MASK) | to; -+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { -+ return true; -+ } -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ public boolean cancel() { -+ int failures = 0; -+ for (long curr = this.stageAndReferenceCount.get();;) { -+ if ((curr & STAGE_MASK) == STAGE_COMPLETED || (curr & STAGE_MASK) == STAGE_CANCELLED) { -+ return false; -+ } -+ -+ if ((curr & STAGE_MASK) == STAGE_NOT_STARTED || (curr & ~STAGE_MASK) == (1L << 32)) { -+ // no other references, so we can cancel -+ final long newVal = STAGE_CANCELLED; -+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { -+ this.loadDataFromDiskTask.cancel(); -+ if (this.processOffMain != null) { -+ this.processOffMain.cancel(); -+ } -+ if (this.processOnMain != null) { -+ this.processOnMain.cancel(); -+ } -+ this.onComplete(null); -+ return true; -+ } -+ } else { -+ if ((curr & ~STAGE_MASK) == (0L << 32)) { -+ throw new IllegalStateException("Reference count cannot be zero here"); -+ } -+ // just decrease the reference count -+ final long newVal = curr - (1L << 32); -+ if (curr == (curr = this.stageAndReferenceCount.compareAndExchange(curr, newVal))) { -+ return false; -+ } -+ } -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ protected final class DataLoadCallback implements BiConsumer { -+ -+ protected final ProcessOffMainTask offMainTask; -+ protected final ProcessOnMainTask onMainTask; -+ -+ public DataLoadCallback(final ProcessOffMainTask offMainTask, final ProcessOnMainTask onMainTask) { -+ this.offMainTask = offMainTask; -+ this.onMainTask = onMainTask; -+ } -+ -+ @Override -+ public void accept(final CompoundTag compoundTag, final Throwable throwable) { -+ if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) { -+ // don't try to schedule further -+ return; -+ } -+ -+ try { -+ if (compoundTag == CANCELLED_DATA) { -+ // cancelled, except this isn't possible -+ LOGGER.error("Data callback says cancelled, but stage does not?"); -+ return; -+ } -+ -+ // get off of the regionfile callback ASAP, no clue what locks are held right now... -+ if (GenericDataLoadTask.this.processOffMain != null) { -+ this.offMainTask.data = compoundTag; -+ this.offMainTask.throwable = throwable; -+ GenericDataLoadTask.this.processOffMain.queue(); -+ return; -+ } else { -+ // no off-main task, so go straight to main -+ this.onMainTask.data = (OnMain)compoundTag; -+ this.onMainTask.throwable = throwable; -+ GenericDataLoadTask.this.processOnMain.queue(); -+ } -+ } catch (final ThreadDeath death) { -+ throw death; -+ } catch (final Throwable thr2) { -+ LOGGER.error("Failed I/O callback for task: " + GenericDataLoadTask.this.toString(), thr2); -+ GenericDataLoadTask.this.scheduler.unrecoverableChunkSystemFailure( -+ GenericDataLoadTask.this.chunkX, GenericDataLoadTask.this.chunkZ, Map.of( -+ "Callback throwable", ChunkTaskScheduler.stringIfNull(throwable) -+ ), thr2); -+ } -+ } -+ } -+ -+ protected final class ProcessOffMainTask implements Runnable { -+ -+ protected CompoundTag data; -+ protected Throwable throwable; -+ protected final ProcessOnMainTask schedule; -+ -+ public ProcessOffMainTask(final ProcessOnMainTask schedule) { -+ this.schedule = schedule; -+ } -+ -+ @Override -+ public void run() { -+ if (!GenericDataLoadTask.this.advanceStage(STAGE_LOADING, this.schedule == null ? STAGE_COMPLETED : STAGE_PROCESSING)) { -+ // cancelled -+ return; -+ } -+ final TaskResult newData = GenericDataLoadTask.this.runOffMain(this.data, this.throwable); -+ -+ if (GenericDataLoadTask.this.stageAndReferenceCount.get() == STAGE_CANCELLED) { -+ // don't try to schedule further -+ return; -+ } -+ -+ if (this.schedule != null) { -+ final TaskResult syncComplete = GenericDataLoadTask.this.completeOnMainOffMain(newData.left, newData.right); -+ -+ if (syncComplete != null) { -+ if (GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) { -+ GenericDataLoadTask.this.onComplete(syncComplete); -+ } // else: cancelled -+ return; -+ } -+ -+ this.schedule.data = newData.left; -+ this.schedule.throwable = newData.right; -+ -+ GenericDataLoadTask.this.processOnMain.queue(); -+ } else { -+ GenericDataLoadTask.this.onComplete((TaskResult)newData); -+ } -+ } -+ } -+ -+ protected final class ProcessOnMainTask implements Runnable { -+ -+ protected OnMain data; -+ protected Throwable throwable; -+ -+ @Override -+ public void run() { -+ if (!GenericDataLoadTask.this.advanceStage(STAGE_PROCESSING, STAGE_COMPLETED)) { -+ // cancelled -+ return; -+ } -+ final TaskResult result = GenericDataLoadTask.this.runOnMain(this.data, this.throwable); -+ -+ GenericDataLoadTask.this.onComplete(result); -+ } -+ } -+ -+ public static final class LoadDataFromDiskTask { -+ -+ protected volatile int priority; -+ protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(LoadDataFromDiskTask.class, "priority", int.class); -+ -+ protected static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 0; -+ protected static final int PRIORITY_LOAD_SCHEDULED = Integer.MIN_VALUE >>> 1; -+ protected static final int PRIORITY_UNLOAD_SCHEDULED = Integer.MIN_VALUE >>> 2; -+ -+ protected static final int PRIORITY_FLAGS = ~Character.MAX_VALUE; -+ -+ protected final int getPriorityVolatile() { -+ return (int)PRIORITY_HANDLE.getVolatile((LoadDataFromDiskTask)this); -+ } -+ -+ protected final int compareAndExchangePriorityVolatile(final int expect, final int update) { -+ return (int)PRIORITY_HANDLE.compareAndExchange((LoadDataFromDiskTask)this, (int)expect, (int)update); -+ } -+ -+ protected final int getAndOrPriorityVolatile(final int val) { -+ return (int)PRIORITY_HANDLE.getAndBitwiseOr((LoadDataFromDiskTask)this, (int)val); -+ } -+ -+ protected final void setPriorityPlain(final int val) { -+ PRIORITY_HANDLE.set((LoadDataFromDiskTask)this, (int)val); -+ } -+ -+ private final ServerLevel world; -+ private final int chunkX; -+ private final int chunkZ; -+ -+ private final RegionFileIOThread.RegionFileType type; -+ private Cancellable dataLoadTask; -+ private Cancellable dataUnloadCancellable; -+ private DelayedPrioritisedTask dataUnloadTask; -+ -+ private final BiConsumer onComplete; -+ -+ // onComplete should be caller sensitive, it may complete synchronously with schedule() - which does -+ // hold a priority lock. -+ public LoadDataFromDiskTask(final ServerLevel world, final int chunkX, final int chunkZ, -+ final RegionFileIOThread.RegionFileType type, -+ final BiConsumer onComplete, -+ final PrioritisedExecutor.Priority priority) { -+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ this.world = world; -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ this.type = type; -+ this.onComplete = onComplete; -+ this.setPriorityPlain(priority.priority); -+ } -+ -+ private void complete(final CompoundTag data, final Throwable throwable) { -+ try { -+ this.onComplete.accept(data, throwable); -+ } catch (final Throwable thr2) { -+ this.world.chunkTaskScheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( -+ "Completed throwable", ChunkTaskScheduler.stringIfNull(throwable), -+ "Regionfile type", ChunkTaskScheduler.stringIfNull(this.type) -+ ), thr2); -+ if (thr2 instanceof ThreadDeath) { -+ throw (ThreadDeath)thr2; -+ } -+ } -+ } -+ -+ protected boolean markExecuting() { -+ return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0; -+ } -+ -+ protected boolean isMarkedExecuted() { -+ return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0; -+ } -+ -+ public void lowerPriority(final PrioritisedExecutor.Priority priority) { -+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ -+ int failures = 0; -+ for (int curr = this.getPriorityVolatile();;) { -+ if ((curr & PRIORITY_EXECUTED) != 0) { -+ // cancelled or executed -+ return; -+ } -+ -+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { -+ RegionFileIOThread.lowerPriority(this.world, this.chunkX, this.chunkZ, this.type, priority); -+ return; -+ } -+ -+ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { -+ if (this.dataUnloadTask != null) { -+ this.dataUnloadTask.lowerPriority(priority); -+ } -+ // no return - we need to propagate priority -+ } -+ -+ if (!priority.isHigherPriority(curr & ~PRIORITY_FLAGS)) { -+ return; -+ } -+ -+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { -+ return; -+ } -+ -+ // failed, retry -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ public void setPriority(final PrioritisedExecutor.Priority priority) { -+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ -+ int failures = 0; -+ for (int curr = this.getPriorityVolatile();;) { -+ if ((curr & PRIORITY_EXECUTED) != 0) { -+ // cancelled or executed -+ return; -+ } -+ -+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { -+ RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, priority); -+ return; -+ } -+ -+ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { -+ if (this.dataUnloadTask != null) { -+ this.dataUnloadTask.setPriority(priority); -+ } -+ // no return - we need to propagate priority -+ } -+ -+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { -+ return; -+ } -+ -+ // failed, retry -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ public void raisePriority(final PrioritisedExecutor.Priority priority) { -+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ -+ int failures = 0; -+ for (int curr = this.getPriorityVolatile();;) { -+ if ((curr & PRIORITY_EXECUTED) != 0) { -+ // cancelled or executed -+ return; -+ } -+ -+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) { -+ RegionFileIOThread.raisePriority(this.world, this.chunkX, this.chunkZ, this.type, priority); -+ return; -+ } -+ -+ if ((curr & PRIORITY_UNLOAD_SCHEDULED) != 0) { -+ if (this.dataUnloadTask != null) { -+ this.dataUnloadTask.raisePriority(priority); -+ } -+ // no return - we need to propagate priority -+ } -+ -+ if (!priority.isLowerPriority(curr & ~PRIORITY_FLAGS)) { -+ return; -+ } -+ -+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority | (curr & PRIORITY_FLAGS)))) { -+ return; -+ } -+ -+ // failed, retry -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ public void cancel() { -+ if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) { -+ // cancelled or executed already -+ return; -+ } -+ -+ // OK if we miss the field read, the task cannot complete if the cancelled bit is set and -+ // the write to dataLoadTask will check for the cancelled bit -+ if (this.dataUnloadCancellable != null) { -+ this.dataUnloadCancellable.cancel(); -+ } -+ -+ if (this.dataLoadTask != null) { -+ this.dataLoadTask.cancel(); -+ } -+ -+ this.complete(CANCELLED_DATA, null); -+ } -+ -+ private final AtomicBoolean scheduled = new AtomicBoolean(); -+ -+ public void schedule() { -+ if (this.scheduled.getAndSet(true)) { -+ throw new IllegalStateException("schedule() called twice"); -+ } -+ int priority = this.getPriorityVolatile(); -+ -+ if ((priority & PRIORITY_EXECUTED) != 0) { -+ // cancelled -+ return; -+ } -+ -+ final BiConsumer consumer = (final CompoundTag data, final Throwable thr) -> { -+ // because cancelScheduled() cannot actually stop this task from executing in every case, we need -+ // to mark complete here to ensure we do not double complete -+ if (LoadDataFromDiskTask.this.markExecuting()) { -+ LoadDataFromDiskTask.this.complete(data, thr); -+ } // else: cancelled -+ }; -+ -+ final PrioritisedExecutor.Priority initialPriority = PrioritisedExecutor.Priority.getPriority(priority); -+ boolean scheduledUnload = false; -+ -+ final NewChunkHolder holder = this.world.chunkTaskScheduler.chunkHolderManager.getChunkHolder(this.chunkX, this.chunkZ); -+ if (holder != null) { -+ final BiConsumer unloadConsumer = (final CompoundTag data, final Throwable thr) -> { -+ if (data != null) { -+ consumer.accept(data, null); -+ } else { -+ // need to schedule task -+ LoadDataFromDiskTask.this.schedule(false, consumer, PrioritisedExecutor.Priority.getPriority(LoadDataFromDiskTask.this.getPriorityVolatile() & ~PRIORITY_FLAGS)); -+ } -+ }; -+ Cancellable unloadCancellable = null; -+ CompoundTag syncComplete = null; -+ final NewChunkHolder.UnloadTask unloadTask = holder.getUnloadTask(this.type); // can be null if no task exists -+ final Completable unloadCompletable = unloadTask == null ? null : unloadTask.completable(); -+ if (unloadCompletable != null) { -+ unloadCancellable = unloadCompletable.addAsynchronousWaiter(unloadConsumer); -+ if (unloadCancellable == null) { -+ syncComplete = unloadCompletable.getResult(); -+ } -+ } -+ -+ if (syncComplete != null) { -+ consumer.accept(syncComplete, null); -+ return; -+ } -+ -+ if (unloadCancellable != null) { -+ scheduledUnload = true; -+ this.dataUnloadCancellable = unloadCancellable; -+ this.dataUnloadTask = unloadTask.task(); -+ } -+ } -+ -+ this.schedule(scheduledUnload, consumer, initialPriority); -+ } -+ -+ private void schedule(final boolean scheduledUnload, final BiConsumer consumer, final PrioritisedExecutor.Priority initialPriority) { -+ int priority = this.getPriorityVolatile(); -+ -+ if ((priority & PRIORITY_EXECUTED) != 0) { -+ // cancelled -+ return; -+ } -+ -+ if (!scheduledUnload) { -+ this.dataLoadTask = RegionFileIOThread.loadDataAsync( -+ this.world, this.chunkX, this.chunkZ, this.type, consumer, -+ initialPriority.isHigherPriority(PrioritisedExecutor.Priority.NORMAL), initialPriority -+ ); -+ } -+ -+ int failures = 0; -+ for (;;) { -+ if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | (scheduledUnload ? PRIORITY_UNLOAD_SCHEDULED : PRIORITY_LOAD_SCHEDULED)))) { -+ return; -+ } -+ -+ if ((priority & PRIORITY_EXECUTED) != 0) { -+ // cancelled or executed -+ if (this.dataUnloadCancellable != null) { -+ this.dataUnloadCancellable.cancel(); -+ } -+ -+ if (this.dataLoadTask != null) { -+ this.dataLoadTask.cancel(); -+ } -+ return; -+ } -+ -+ if (scheduledUnload) { -+ if (this.dataUnloadTask != null) { -+ this.dataUnloadTask.setPriority(PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS)); -+ } -+ } else { -+ RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS)); -+ } -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ /* -+ private static final class LoadDataPriorityHolder extends PriorityHolder { -+ -+ protected final LoadDataFromDiskTask task; -+ -+ protected LoadDataPriorityHolder(final PrioritisedExecutor.Priority priority, final LoadDataFromDiskTask task) { -+ super(priority); -+ this.task = task; -+ } -+ -+ @Override -+ protected void cancelScheduled() { -+ final Cancellable dataLoadTask = this.task.dataLoadTask; -+ if (dataLoadTask != null) { -+ // OK if we miss the field read, the task cannot complete if the cancelled bit is set and -+ // the write to dataLoadTask will check for the cancelled bit -+ this.task.dataLoadTask.cancel(); -+ } -+ this.task.complete(CANCELLED_DATA, null); -+ } -+ -+ @Override -+ protected PrioritisedExecutor.Priority getScheduledPriority() { -+ final LoadDataFromDiskTask task = this.task; -+ return RegionFileIOThread.getPriority(task.world, task.chunkX, task.chunkZ, task.type); -+ } -+ -+ @Override -+ protected void scheduleTask(final PrioritisedExecutor.Priority priority) { -+ final LoadDataFromDiskTask task = this.task; -+ final BiConsumer consumer = (final CompoundTag data, final Throwable thr) -> { -+ // because cancelScheduled() cannot actually stop this task from executing in every case, we need -+ // to mark complete here to ensure we do not double complete -+ if (LoadDataPriorityHolder.this.markExecuting()) { -+ LoadDataPriorityHolder.this.task.complete(data, thr); -+ } // else: cancelled -+ }; -+ task.dataLoadTask = RegionFileIOThread.loadDataAsync( -+ task.world, task.chunkX, task.chunkZ, task.type, consumer, -+ priority.isHigherPriority(PrioritisedExecutor.Priority.NORMAL), priority -+ ); -+ if (this.isMarkedExecuted()) { -+ // if we are marked as completed, it could be: -+ // 1. we were cancelled -+ // 2. the consumer was completed -+ // in the 2nd case, cancel() does nothing -+ // in the 1st case, we ensure cancel() is called as it is possible for the cancelling thread -+ // to miss the field write here -+ task.dataLoadTask.cancel(); -+ } -+ } -+ -+ @Override -+ protected void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority) { -+ final LoadDataFromDiskTask task = this.task; -+ RegionFileIOThread.lowerPriority(task.world, task.chunkX, task.chunkZ, task.type, priority); -+ } -+ -+ @Override -+ protected void setPriorityScheduled(final PrioritisedExecutor.Priority priority) { -+ final LoadDataFromDiskTask task = this.task; -+ RegionFileIOThread.setPriority(task.world, task.chunkX, task.chunkZ, task.type, priority); -+ } -+ -+ @Override -+ protected void raisePriorityScheduled(final PrioritisedExecutor.Priority priority) { -+ final LoadDataFromDiskTask task = this.task; -+ RegionFileIOThread.raisePriority(task.world, task.chunkX, task.chunkZ, task.type, priority); -+ } -+ } -+ */ -+ } -+} -diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java -new file mode 100644 -index 0000000000000000000000000000000000000000..56b07a3306e5735816c8d89601b519cb0db6379a ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/NewChunkHolder.java -@@ -0,0 +1,2106 @@ -+package io.papermc.paper.chunk.system.scheduling; -+ -+import ca.spottedleaf.concurrentutil.completable.Completable; -+import ca.spottedleaf.concurrentutil.executor.Cancellable; -+import ca.spottedleaf.concurrentutil.executor.standard.DelayedPrioritisedTask; -+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; -+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; -+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; -+import com.google.gson.JsonArray; -+import com.google.gson.JsonElement; -+import com.google.gson.JsonObject; -+import com.google.gson.JsonPrimitive; -+import com.mojang.logging.LogUtils; -+import io.papermc.paper.chunk.system.io.RegionFileIOThread; -+import io.papermc.paper.chunk.system.poi.PoiChunk; -+import io.papermc.paper.util.CoordinateUtils; -+import io.papermc.paper.util.TickThread; -+import io.papermc.paper.util.WorldUtil; -+import io.papermc.paper.world.ChunkEntitySlices; -+import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap; -+import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; -+import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; -+import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.server.level.ChunkHolder; -+import net.minecraft.server.level.ChunkLevel; -+import net.minecraft.server.level.FullChunkStatus; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.TicketType; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.chunk.ChunkAccess; -+import net.minecraft.world.level.chunk.status.ChunkStatus; -+import net.minecraft.world.level.chunk.ImposterProtoChunk; -+import net.minecraft.world.level.chunk.LevelChunk; -+import net.minecraft.world.level.chunk.storage.ChunkSerializer; -+import net.minecraft.world.level.chunk.storage.EntityStorage; -+import org.slf4j.Logger; -+import java.lang.invoke.VarHandle; -+import java.util.ArrayList; -+import java.util.Iterator; -+import java.util.List; -+import java.util.Map; -+import java.util.Objects; -+import java.util.concurrent.atomic.AtomicBoolean; -+import java.util.function.Consumer; -+ -+public final class NewChunkHolder { -+ -+ private static final Logger LOGGER = LogUtils.getClassLogger(); -+ -+ public static final Thread.UncaughtExceptionHandler CHUNKSYSTEM_UNCAUGHT_EXCEPTION_HANDLER = new Thread.UncaughtExceptionHandler() { -+ @Override -+ public void uncaughtException(final Thread thread, final Throwable throwable) { -+ if (!(throwable instanceof ThreadDeath)) { -+ LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable); -+ } -+ } -+ }; -+ -+ public final ServerLevel world; -+ public final int chunkX; -+ public final int chunkZ; -+ -+ public final ChunkTaskScheduler scheduler; -+ -+ // load/unload state -+ -+ // chunk data state -+ -+ private ChunkEntitySlices entityChunk; -+ // entity chunk that is loaded, but not yet deserialized -+ private CompoundTag pendingEntityChunk; -+ -+ ChunkEntitySlices loadInEntityChunk(final boolean transientChunk) { -+ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot sync load entity data off-main"); -+ final CompoundTag entityChunk; -+ final ChunkEntitySlices ret; -+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ if (this.entityChunk != null && (transientChunk || !this.entityChunk.isTransient())) { -+ return this.entityChunk; -+ } -+ final CompoundTag pendingEntityChunk = this.pendingEntityChunk; -+ if (!transientChunk && pendingEntityChunk == null) { -+ throw new IllegalStateException("Must load entity data from disk before loading in the entity chunk!"); -+ } -+ -+ if (this.entityChunk == null) { -+ ret = this.entityChunk = new ChunkEntitySlices( -+ this.world, this.chunkX, this.chunkZ, this.getChunkStatus(), -+ WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world) -+ ); -+ -+ ret.setTransient(transientChunk); -+ -+ this.world.getEntityLookup().entitySectionLoad(this.chunkX, this.chunkZ, ret); -+ } else { -+ // transientChunk = false here -+ ret = this.entityChunk; -+ this.entityChunk.setTransient(false); -+ } -+ -+ if (!transientChunk) { -+ this.pendingEntityChunk = null; -+ entityChunk = pendingEntityChunk == EMPTY_ENTITY_CHUNK ? null : pendingEntityChunk; -+ } else { -+ entityChunk = null; -+ } -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ -+ if (!transientChunk) { -+ if (entityChunk != null) { -+ final List entities = EntityStorage.readEntities(this.world, entityChunk); -+ -+ this.world.getEntityLookup().addEntityChunkEntities(entities, new ChunkPos(this.chunkX, this.chunkZ)); -+ } -+ } -+ -+ return ret; -+ } -+ -+ // needed to distinguish whether the entity chunk has been read from disk but is empty or whether it has _not_ -+ // been read from disk -+ private static final CompoundTag EMPTY_ENTITY_CHUNK = new CompoundTag(); -+ -+ private ChunkLoadTask.EntityDataLoadTask entityDataLoadTask; -+ // note: if entityDataLoadTask is cancelled, but on its completion entityDataLoadTaskWaiters.size() != 0, -+ // then the task is rescheduled -+ private List entityDataLoadTaskWaiters; -+ -+ public ChunkLoadTask.EntityDataLoadTask getEntityDataLoadTask() { -+ return this.entityDataLoadTask; -+ } -+ -+ // must hold schedule lock for the two below functions -+ -+ // returns only if the data has been loaded from disk, DOES NOT relate to whether it has been deserialized -+ // or added into the world (or even into entityChunk) -+ public boolean isEntityChunkNBTLoaded() { -+ return (this.entityChunk != null && !this.entityChunk.isTransient()) || this.pendingEntityChunk != null; -+ } -+ -+ private void completeEntityLoad(final GenericDataLoadTask.TaskResult result) { -+ final List completeWaiters; -+ ChunkLoadTask.EntityDataLoadTask entityDataLoadTask = null; -+ boolean scheduleEntityTask = false; -+ ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ final List waiters = this.entityDataLoadTaskWaiters; -+ this.entityDataLoadTask = null; -+ if (result != null) { -+ this.entityDataLoadTaskWaiters = null; -+ this.pendingEntityChunk = result.left() == null ? EMPTY_ENTITY_CHUNK : result.left(); -+ if (result.right() != null) { -+ LOGGER.error("Unhandled entity data load exception, data data will be lost: ", result.right()); -+ } -+ -+ for (final GenericDataLoadTaskCallback callback : waiters) { -+ callback.markCompleted(); -+ } -+ -+ completeWaiters = waiters; -+ } else { -+ // cancelled -+ completeWaiters = null; -+ -+ // need to re-schedule? -+ if (waiters.isEmpty()) { -+ this.entityDataLoadTaskWaiters = null; -+ // no tasks to schedule _for_ -+ } else { -+ entityDataLoadTask = this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask( -+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority() -+ ); -+ entityDataLoadTask.addCallback(this::completeEntityLoad); -+ // need one schedule() per waiter -+ for (final GenericDataLoadTaskCallback callback : waiters) { -+ scheduleEntityTask |= entityDataLoadTask.schedule(true); -+ } -+ } -+ } -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ -+ if (scheduleEntityTask) { -+ entityDataLoadTask.scheduleNow(); -+ } -+ -+ // avoid holding the scheduling lock while completing -+ if (completeWaiters != null) { -+ for (final GenericDataLoadTaskCallback callback : completeWaiters) { -+ callback.acceptCompleted(result); -+ } -+ } -+ -+ schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ this.checkUnload(); -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ } -+ -+ // note: it is guaranteed that the consumer cannot be called for the entirety that the schedule lock is held -+ // however, when the consumer is invoked, it will hold the schedule lock -+ public GenericDataLoadTaskCallback getOrLoadEntityData(final Consumer> consumer) { -+ if (this.isEntityChunkNBTLoaded()) { -+ throw new IllegalStateException("Cannot load entity data, it is already loaded"); -+ } -+ // why not just acquire the lock? because the caller NEEDS to call isEntityChunkNBTLoaded before this! -+ if (!this.scheduler.schedulingLockArea.isHeldByCurrentThread(this.chunkX, this.chunkZ)) { -+ throw new IllegalStateException("Must hold scheduling lock"); -+ } -+ -+ final GenericDataLoadTaskCallback ret = new EntityDataLoadTaskCallback((Consumer)consumer, this); -+ -+ if (this.entityDataLoadTask == null) { -+ this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask( -+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority() -+ ); -+ this.entityDataLoadTask.addCallback(this::completeEntityLoad); -+ this.entityDataLoadTaskWaiters = new ArrayList<>(); -+ } -+ this.entityDataLoadTaskWaiters.add(ret); -+ if (this.entityDataLoadTask.schedule(true)) { -+ ret.schedule = this.entityDataLoadTask; -+ } -+ this.checkUnload(); -+ -+ return ret; -+ } -+ -+ private static final class EntityDataLoadTaskCallback extends GenericDataLoadTaskCallback { -+ -+ public EntityDataLoadTaskCallback(final Consumer> consumer, final NewChunkHolder chunkHolder) { -+ super(consumer, chunkHolder); -+ } -+ -+ @Override -+ void internalCancel() { -+ this.chunkHolder.entityDataLoadTaskWaiters.remove(this); -+ this.chunkHolder.entityDataLoadTask.cancel(); -+ } -+ } -+ -+ private PoiChunk poiChunk; -+ -+ private ChunkLoadTask.PoiDataLoadTask poiDataLoadTask; -+ // note: if entityDataLoadTask is cancelled, but on its completion entityDataLoadTaskWaiters.size() != 0, -+ // then the task is rescheduled -+ private List poiDataLoadTaskWaiters; -+ -+ public ChunkLoadTask.PoiDataLoadTask getPoiDataLoadTask() { -+ return this.poiDataLoadTask; -+ } -+ -+ // must hold schedule lock for the two below functions -+ -+ public boolean isPoiChunkLoaded() { -+ return this.poiChunk != null; -+ } -+ -+ private void completePoiLoad(final GenericDataLoadTask.TaskResult result) { -+ final List completeWaiters; -+ ChunkLoadTask.PoiDataLoadTask poiDataLoadTask = null; -+ boolean schedulePoiTask = false; -+ ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ final List waiters = this.poiDataLoadTaskWaiters; -+ this.poiDataLoadTask = null; -+ if (result != null) { -+ this.poiDataLoadTaskWaiters = null; -+ this.poiChunk = result.left(); -+ if (result.right() != null) { -+ LOGGER.error("Unhandled poi load exception, poi data will be lost: ", result.right()); -+ } -+ -+ for (final GenericDataLoadTaskCallback callback : waiters) { -+ callback.markCompleted(); -+ } -+ -+ completeWaiters = waiters; -+ } else { -+ // cancelled -+ completeWaiters = null; -+ -+ // need to re-schedule? -+ if (waiters.isEmpty()) { -+ this.poiDataLoadTaskWaiters = null; -+ // no tasks to schedule _for_ -+ } else { -+ poiDataLoadTask = this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask( -+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority() -+ ); -+ poiDataLoadTask.addCallback(this::completePoiLoad); -+ // need one schedule() per waiter -+ for (final GenericDataLoadTaskCallback callback : waiters) { -+ schedulePoiTask |= poiDataLoadTask.schedule(true); -+ } -+ } -+ } -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ -+ if (schedulePoiTask) { -+ poiDataLoadTask.scheduleNow(); -+ } -+ -+ // avoid holding the scheduling lock while completing -+ if (completeWaiters != null) { -+ for (final GenericDataLoadTaskCallback callback : completeWaiters) { -+ callback.acceptCompleted(result); -+ } -+ } -+ schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ this.checkUnload(); -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ } -+ -+ // note: it is guaranteed that the consumer cannot be called for the entirety that the schedule lock is held -+ // however, when the consumer is invoked, it will hold the schedule lock -+ public GenericDataLoadTaskCallback getOrLoadPoiData(final Consumer> consumer) { -+ if (this.isPoiChunkLoaded()) { -+ throw new IllegalStateException("Cannot load poi data, it is already loaded"); -+ } -+ // why not just acquire the lock? because the caller NEEDS to call isPoiChunkLoaded before this! -+ if (!this.scheduler.schedulingLockArea.isHeldByCurrentThread(this.chunkX, this.chunkZ)) { -+ throw new IllegalStateException("Must hold scheduling lock"); -+ } -+ -+ final GenericDataLoadTaskCallback ret = new PoiDataLoadTaskCallback((Consumer)consumer, this); -+ -+ if (this.poiDataLoadTask == null) { -+ this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask( -+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority() -+ ); -+ this.poiDataLoadTask.addCallback(this::completePoiLoad); -+ this.poiDataLoadTaskWaiters = new ArrayList<>(); -+ } -+ this.poiDataLoadTaskWaiters.add(ret); -+ if (this.poiDataLoadTask.schedule(true)) { -+ ret.schedule = this.poiDataLoadTask; -+ } -+ this.checkUnload(); -+ -+ return ret; -+ } -+ -+ private static final class PoiDataLoadTaskCallback extends GenericDataLoadTaskCallback { -+ -+ public PoiDataLoadTaskCallback(final Consumer> consumer, final NewChunkHolder chunkHolder) { -+ super(consumer, chunkHolder); -+ } -+ -+ @Override -+ void internalCancel() { -+ this.chunkHolder.poiDataLoadTaskWaiters.remove(this); -+ this.chunkHolder.poiDataLoadTask.cancel(); -+ } -+ } -+ -+ public static abstract class GenericDataLoadTaskCallback implements Cancellable { -+ -+ protected final Consumer> consumer; -+ protected final NewChunkHolder chunkHolder; -+ protected boolean completed; -+ protected GenericDataLoadTask schedule; -+ protected final AtomicBoolean scheduled = new AtomicBoolean(); -+ -+ public GenericDataLoadTaskCallback(final Consumer> consumer, -+ final NewChunkHolder chunkHolder) { -+ this.consumer = consumer; -+ this.chunkHolder = chunkHolder; -+ } -+ -+ public void schedule() { -+ if (this.scheduled.getAndSet(true)) { -+ throw new IllegalStateException("Double calling schedule()"); -+ } -+ if (this.schedule != null) { -+ this.schedule.scheduleNow(); -+ this.schedule = null; -+ } -+ } -+ -+ boolean isCompleted() { -+ return this.completed; -+ } -+ -+ // must hold scheduling lock -+ private boolean setCompleted() { -+ if (this.completed) { -+ return false; -+ } -+ return this.completed = true; -+ } -+ -+ // must hold scheduling lock -+ void markCompleted() { -+ if (this.completed) { -+ throw new IllegalStateException("May not be completed here"); -+ } -+ this.completed = true; -+ } -+ -+ void acceptCompleted(final GenericDataLoadTask.TaskResult result) { -+ if (result != null) { -+ if (this.completed) { -+ this.consumer.accept(result); -+ } else { -+ throw new IllegalStateException("Cannot be uncompleted at this point"); -+ } -+ } else { -+ throw new NullPointerException("Result cannot be null (cancelled)"); -+ } -+ } -+ -+ // holds scheduling lock -+ abstract void internalCancel(); -+ -+ @Override -+ public boolean cancel() { -+ final NewChunkHolder holder = this.chunkHolder; // Folia - use area based lock to reduce contention -+ final ReentrantAreaLock.Node schedulingLock = holder.scheduler.schedulingLockArea.lock(holder.chunkX, holder.chunkZ); -+ try { -+ if (!this.completed) { -+ this.completed = true; -+ this.internalCancel(); -+ return true; -+ } -+ return false; -+ } finally { -+ holder.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ } -+ } -+ -+ private ChunkAccess currentChunk; -+ -+ // generation status state -+ -+ /** -+ * Current status the chunk has been brought up to by the chunk system. null indicates no work at all -+ */ -+ private ChunkStatus currentGenStatus; -+ -+ // This allows unsynchronised access to the chunk and last gen status -+ private volatile ChunkCompletion lastChunkCompletion; -+ -+ public ChunkCompletion getLastChunkCompletion() { -+ return this.lastChunkCompletion; -+ } -+ -+ public static final record ChunkCompletion(ChunkAccess chunk, ChunkStatus genStatus) {}; -+ -+ /** -+ * The target final chunk status the chunk system will bring the chunk to. -+ */ -+ private ChunkStatus requestedGenStatus; -+ -+ private ChunkProgressionTask generationTask; -+ private ChunkStatus generationTaskStatus; -+ -+ /** -+ * contains the neighbours that this chunk generation is blocking on -+ */ -+ protected final ReferenceLinkedOpenHashSet neighboursBlockingGenTask = new ReferenceLinkedOpenHashSet<>(4); -+ -+ /** -+ * map of ChunkHolder -> Required Status for this chunk -+ */ -+ protected final Reference2ObjectLinkedOpenHashMap neighboursWaitingForUs = new Reference2ObjectLinkedOpenHashMap<>(); -+ -+ public void addGenerationBlockingNeighbour(final NewChunkHolder neighbour) { -+ this.neighboursBlockingGenTask.add(neighbour); -+ } -+ -+ public void addWaitingNeighbour(final NewChunkHolder neighbour, final ChunkStatus requiredStatus) { -+ final boolean wasEmpty = this.neighboursWaitingForUs.isEmpty(); -+ this.neighboursWaitingForUs.put(neighbour, requiredStatus); -+ if (wasEmpty) { -+ this.checkUnload(); -+ } -+ } -+ -+ // priority state -+ -+ // the target priority for this chunk to generate at -+ // TODO this will screw over scheduling at lower priorities to neighbours, fix -+ private PrioritisedExecutor.Priority priority = PrioritisedExecutor.Priority.NORMAL; -+ private boolean priorityLocked; -+ -+ // the priority neighbouring chunks have requested this chunk generate at -+ private PrioritisedExecutor.Priority neighbourRequestedPriority = PrioritisedExecutor.Priority.IDLE; -+ -+ public PrioritisedExecutor.Priority getEffectivePriority() { -+ return PrioritisedExecutor.Priority.max(this.priority, this.neighbourRequestedPriority); -+ } -+ -+ protected void recalculateNeighbourRequestedPriority() { -+ if (this.neighboursWaitingForUs.isEmpty()) { -+ this.neighbourRequestedPriority = PrioritisedExecutor.Priority.IDLE; -+ return; -+ } -+ -+ PrioritisedExecutor.Priority max = PrioritisedExecutor.Priority.IDLE; -+ -+ for (final NewChunkHolder holder : this.neighboursWaitingForUs.keySet()) { -+ final PrioritisedExecutor.Priority neighbourPriority = holder.getEffectivePriority(); -+ if (neighbourPriority.isHigherPriority(max)) { -+ max = neighbourPriority; -+ } -+ } -+ -+ final PrioritisedExecutor.Priority current = this.getEffectivePriority(); -+ this.neighbourRequestedPriority = max; -+ final PrioritisedExecutor.Priority next = this.getEffectivePriority(); -+ -+ if (current == next) { -+ return; -+ } -+ -+ // our effective priority has changed, so change our task -+ if (this.generationTask != null) { -+ this.generationTask.setPriority(next); -+ } -+ -+ // now propagate this to our neighbours -+ this.recalculateNeighbourPriorities(); -+ } -+ -+ public void recalculateNeighbourPriorities() { -+ for (final NewChunkHolder holder : this.neighboursBlockingGenTask) { -+ holder.recalculateNeighbourRequestedPriority(); -+ } -+ } -+ -+ // must hold scheduling lock -+ public void raisePriority(final PrioritisedExecutor.Priority priority) { -+ if (this.priority != null && this.priority.isHigherOrEqualPriority(priority)) { -+ return; -+ } -+ this.setPriority(priority); -+ } -+ -+ private void lockPriority() { -+ this.priority = PrioritisedExecutor.Priority.NORMAL; -+ this.priorityLocked = true; -+ } -+ -+ // must hold scheduling lock -+ public void setPriority(final PrioritisedExecutor.Priority priority) { -+ if (this.priorityLocked) { -+ return; -+ } -+ final PrioritisedExecutor.Priority old = this.getEffectivePriority(); -+ this.priority = priority; -+ final PrioritisedExecutor.Priority newPriority = this.getEffectivePriority(); -+ -+ if (old != newPriority) { -+ if (this.generationTask != null) { -+ this.generationTask.setPriority(newPriority); -+ } -+ } -+ -+ this.recalculateNeighbourPriorities(); -+ } -+ -+ // must hold scheduling lock -+ public void lowerPriority(final PrioritisedExecutor.Priority priority) { -+ if (this.priority != null && this.priority.isLowerOrEqualPriority(priority)) { -+ return; -+ } -+ this.setPriority(priority); -+ } -+ -+ // error handling state -+ private ChunkStatus failedGenStatus; -+ private Throwable genTaskException; -+ private Thread genTaskFailedThread; -+ -+ private boolean failedLightUpdate; -+ -+ public void failedLightUpdate() { -+ this.failedLightUpdate = true; -+ } -+ -+ public boolean hasFailedGeneration() { -+ return this.genTaskException != null; -+ } -+ -+ // ticket level state -+ private int oldTicketLevel = ChunkLevel.MAX_LEVEL + 1; -+ private int currentTicketLevel = ChunkLevel.MAX_LEVEL + 1; -+ -+ public int getTicketLevel() { -+ return this.currentTicketLevel; -+ } -+ -+ public final ChunkHolder vanillaChunkHolder; -+ -+ public NewChunkHolder(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkTaskScheduler scheduler) { -+ this.world = world; -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ this.scheduler = scheduler; -+ this.vanillaChunkHolder = new ChunkHolder(new ChunkPos(chunkX, chunkZ), world, world.getLightEngine(), world.chunkSource.chunkMap, this); -+ } -+ -+ protected ImposterProtoChunk wrappedChunkForNeighbour; -+ -+ // holds scheduling lock -+ public ChunkAccess getChunkForNeighbourAccess() { -+ // Vanilla overrides the status futures with an imposter chunk to prevent writes to full chunks -+ // But we don't store per-status futures, so we need this hack -+ if (this.wrappedChunkForNeighbour != null) { -+ return this.wrappedChunkForNeighbour; -+ } -+ final ChunkAccess ret = this.currentChunk; -+ return ret instanceof LevelChunk fullChunk ? this.wrappedChunkForNeighbour = new ImposterProtoChunk(fullChunk, false) : ret; -+ } -+ -+ public ChunkAccess getCurrentChunk() { -+ return this.currentChunk; -+ } -+ -+ int getCurrentTicketLevel() { -+ return this.currentTicketLevel; -+ } -+ -+ void updateTicketLevel(final int toLevel) { -+ this.currentTicketLevel = toLevel; -+ } -+ -+ private int totalNeighboursUsingThisChunk = 0; -+ -+ // holds schedule lock -+ public void addNeighbourUsingChunk() { -+ final int now = ++this.totalNeighboursUsingThisChunk; -+ -+ if (now == 1) { -+ this.checkUnload(); -+ } -+ } -+ -+ // holds schedule lock -+ public void removeNeighbourUsingChunk() { -+ final int now = --this.totalNeighboursUsingThisChunk; -+ -+ if (now == 0) { -+ this.checkUnload(); -+ } -+ -+ if (now < 0) { -+ throw new IllegalStateException("Neighbours using this chunk cannot be negative"); -+ } -+ } -+ -+ // must hold scheduling lock -+ // returns string reason for why chunk should remain loaded, null otherwise -+ public final String isSafeToUnload() { -+ // is ticket level below threshold? -+ if (this.oldTicketLevel <= ChunkHolderManager.MAX_TICKET_LEVEL) { -+ return "ticket_level"; -+ } -+ -+ // are we being used by another chunk for generation? -+ if (this.totalNeighboursUsingThisChunk != 0) { -+ return "neighbours_generating"; -+ } -+ -+ // are we going to be used by another chunk for generation? -+ if (!this.neighboursWaitingForUs.isEmpty()) { -+ return "neighbours_waiting"; -+ } -+ -+ // chunk must be marked inaccessible (i.e unloaded to plugins) -+ if (this.getChunkStatus() != FullChunkStatus.INACCESSIBLE) { -+ return "fullchunkstatus"; -+ } -+ -+ // are we currently generating anything, or have requested generation? -+ if (this.generationTask != null) { -+ return "generating"; -+ } -+ if (this.requestedGenStatus != null) { -+ return "requested_generation"; -+ } -+ -+ // entity data requested? -+ if (this.entityDataLoadTask != null) { -+ return "entity_data_requested"; -+ } -+ -+ // poi data requested? -+ if (this.poiDataLoadTask != null) { -+ return "poi_data_requested"; -+ } -+ -+ // are we pending serialization? -+ if (this.entityDataUnload != null) { -+ return "entity_serialization"; -+ } -+ if (this.poiDataUnload != null) { -+ return "poi_serialization"; -+ } -+ if (this.chunkDataUnload != null) { -+ return "chunk_serialization"; -+ } -+ -+ // Note: light tasks do not need a check, as they add a ticket. -+ -+ // nothing is using this chunk, so it should be unloaded -+ return null; -+ } -+ -+ /** Unloaded from chunk map */ -+ boolean killed; -+ -+ // must hold scheduling lock -+ private void checkUnload() { -+ if (this.killed) { -+ return; -+ } -+ if (this.isSafeToUnload() == null) { -+ // ensure in unload queue -+ this.scheduler.chunkHolderManager.unloadQueue.addChunk(this.chunkX, this.chunkZ); -+ } else { -+ // ensure not in unload queue -+ this.scheduler.chunkHolderManager.unloadQueue.removeChunk(this.chunkX, this.chunkZ); -+ } -+ } -+ -+ static final record UnloadState(NewChunkHolder holder, ChunkAccess chunk, ChunkEntitySlices entityChunk, PoiChunk poiChunk) {}; -+ -+ // note: these are completed with null to indicate that no write occurred -+ // they are also completed with null to indicate a null write occurred -+ private UnloadTask chunkDataUnload; -+ private UnloadTask entityDataUnload; -+ private UnloadTask poiDataUnload; -+ -+ public static final record UnloadTask(Completable completable, DelayedPrioritisedTask task) {} -+ -+ public UnloadTask getUnloadTask(final RegionFileIOThread.RegionFileType type) { -+ switch (type) { -+ case CHUNK_DATA: -+ return this.chunkDataUnload; -+ case ENTITY_DATA: -+ return this.entityDataUnload; -+ case POI_DATA: -+ return this.poiDataUnload; -+ default: -+ throw new IllegalStateException("Unknown regionfile type " + type); -+ } -+ } -+ -+ private UnloadState unloadState; -+ -+ // holds schedule lock -+ UnloadState unloadStage1() { -+ // because we hold the scheduling lock, we cannot actually unload anything -+ // so we need to null this chunk's state -+ ChunkAccess chunk = this.currentChunk; -+ ChunkEntitySlices entityChunk = this.entityChunk; -+ PoiChunk poiChunk = this.poiChunk; -+ // chunk state -+ this.currentChunk = null; -+ this.currentGenStatus = null; -+ this.wrappedChunkForNeighbour = null; -+ this.lastChunkCompletion = null; -+ // entity chunk state -+ this.entityChunk = null; -+ this.pendingEntityChunk = null; -+ -+ // poi chunk state -+ this.poiChunk = null; -+ -+ // priority state -+ this.priorityLocked = false; -+ -+ if (chunk != null) { -+ this.chunkDataUnload = new UnloadTask(new Completable<>(), new DelayedPrioritisedTask(PrioritisedExecutor.Priority.NORMAL)); -+ } -+ if (poiChunk != null) { -+ this.poiDataUnload = new UnloadTask(new Completable<>(), null); -+ } -+ if (entityChunk != null) { -+ this.entityDataUnload = new UnloadTask(new Completable<>(), null); -+ } -+ -+ return this.unloadState = (chunk != null || entityChunk != null || poiChunk != null) ? new UnloadState(this, chunk, entityChunk, poiChunk) : null; -+ } -+ -+ // data is null if failed or does not need to be saved -+ void completeAsyncChunkDataSave(final CompoundTag data) { -+ if (data != null) { -+ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, data, RegionFileIOThread.RegionFileType.CHUNK_DATA); -+ } -+ this.chunkDataUnload.completable().complete(data); -+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ // can only write to these fields while holding the schedule lock -+ this.chunkDataUnload = null; -+ this.checkUnload(); -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ } -+ -+ void unloadStage2(final UnloadState state) { -+ this.unloadState = null; -+ final ChunkAccess chunk = state.chunk(); -+ final ChunkEntitySlices entityChunk = state.entityChunk(); -+ final PoiChunk poiChunk = state.poiChunk(); -+ -+ final boolean shouldLevelChunkNotSave = (chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave); -+ -+ // unload chunk data -+ if (chunk != null) { -+ if (chunk instanceof LevelChunk levelChunk) { -+ levelChunk.setLoaded(false); -+ } -+ -+ if (!shouldLevelChunkNotSave) { -+ this.saveChunk(chunk, true); -+ } else { -+ this.completeAsyncChunkDataSave(null); -+ } -+ -+ if (chunk instanceof LevelChunk levelChunk) { -+ this.world.unload(levelChunk); -+ } -+ } -+ -+ // unload entity data -+ if (entityChunk != null) { -+ this.saveEntities(entityChunk, true); -+ // yes this is a hack to pass the compound tag through... -+ final CompoundTag lastEntityUnload = this.lastEntityUnload; -+ this.lastEntityUnload = null; -+ -+ if (entityChunk.unload()) { -+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ entityChunk.setTransient(true); -+ this.entityChunk = entityChunk; -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ } else { -+ this.world.getEntityLookup().entitySectionUnload(this.chunkX, this.chunkZ); -+ } -+ // we need to delay the callback until after determining transience, otherwise a potential loader could -+ // set entityChunk before we do -+ this.entityDataUnload.completable().complete(lastEntityUnload); -+ } -+ -+ // unload poi data -+ if (poiChunk != null) { -+ if (poiChunk.isDirty() && !shouldLevelChunkNotSave) { -+ this.savePOI(poiChunk, true); -+ } else { -+ this.poiDataUnload.completable().complete(null); -+ } -+ -+ if (poiChunk.isLoaded()) { -+ this.world.getPoiManager().onUnload(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ)); -+ } -+ } -+ } -+ -+ boolean unloadStage3() { -+ // can only write to these while holding the schedule lock, and we instantly complete them in stage2 -+ this.poiDataUnload = null; -+ this.entityDataUnload = null; -+ -+ // we need to check if anything has been loaded in the meantime (or if we have transient entities) -+ if (this.entityChunk != null || this.poiChunk != null || this.currentChunk != null) { -+ return false; -+ } -+ -+ return this.isSafeToUnload() == null; -+ } -+ -+ private void cancelGenTask() { -+ if (this.generationTask != null) { -+ this.generationTask.cancel(); -+ } else { -+ // otherwise, we are blocking on neighbours, so remove them -+ if (!this.neighboursBlockingGenTask.isEmpty()) { -+ for (final NewChunkHolder neighbour : this.neighboursBlockingGenTask) { -+ if (neighbour.neighboursWaitingForUs.remove(this) == null) { -+ throw new IllegalStateException("Corrupt state"); -+ } -+ if (neighbour.neighboursWaitingForUs.isEmpty()) { -+ neighbour.checkUnload(); -+ } -+ } -+ this.neighboursBlockingGenTask.clear(); -+ this.checkUnload(); -+ } -+ } -+ } -+ -+ // holds: ticket level update lock -+ // holds: schedule lock -+ public void processTicketLevelUpdate(final List scheduledTasks, final List changedLoadStatus) { -+ final int oldLevel = this.oldTicketLevel; -+ final int newLevel = this.currentTicketLevel; -+ -+ if (oldLevel == newLevel) { -+ return; -+ } -+ -+ this.oldTicketLevel = newLevel; -+ -+ final FullChunkStatus oldState = ChunkLevel.fullStatus(oldLevel); -+ final FullChunkStatus newState = ChunkLevel.fullStatus(newLevel); -+ final boolean oldUnloaded = oldLevel > ChunkHolderManager.MAX_TICKET_LEVEL; -+ final boolean newUnloaded = newLevel > ChunkHolderManager.MAX_TICKET_LEVEL; -+ -+ final ChunkStatus maxGenerationStatusOld = ChunkLevel.generationStatus(oldLevel); -+ final ChunkStatus maxGenerationStatusNew = ChunkLevel.generationStatus(newLevel); -+ -+ // check for cancellations from downgrading ticket level -+ if (this.requestedGenStatus != null && !newState.isOrAfter(FullChunkStatus.FULL) && newLevel > oldLevel) { -+ // note: cancel() may invoke onChunkGenComplete synchronously here -+ if (newUnloaded) { -+ // need to cancel all tasks -+ // note: requested status must be set to null here before cancellation, to indicate to the -+ // completion logic that we do not want rescheduling to occur -+ this.requestedGenStatus = null; -+ this.cancelGenTask(); -+ } else { -+ final ChunkStatus toCancel = maxGenerationStatusNew.getNextStatus(); -+ final ChunkStatus currentRequestedStatus = this.requestedGenStatus; -+ -+ if (currentRequestedStatus.isOrAfter(toCancel)) { -+ // we do have to cancel something here -+ // clamp requested status to the maximum -+ if (this.currentGenStatus != null && this.currentGenStatus.isOrAfter(maxGenerationStatusNew)) { -+ // already generated to status, so we must cancel -+ this.requestedGenStatus = null; -+ this.cancelGenTask(); -+ } else { -+ // not generated to status, so we may have to cancel -+ // note: gen task is always 1 status above current gen status if not null -+ this.requestedGenStatus = maxGenerationStatusNew; -+ if (this.generationTaskStatus != null && this.generationTaskStatus.isOrAfter(toCancel)) { -+ // TOOD is this even possible? i don't think so -+ throw new IllegalStateException("?????"); -+ } -+ } -+ } -+ } -+ } -+ -+ if (newState != oldState) { -+ if (newState.isOrAfter(oldState)) { -+ // status upgrade -+ if (!oldState.isOrAfter(FullChunkStatus.FULL) && newState.isOrAfter(FullChunkStatus.FULL)) { -+ // may need to schedule full load -+ if (this.currentGenStatus != ChunkStatus.FULL) { -+ if (this.requestedGenStatus != null) { -+ this.requestedGenStatus = ChunkStatus.FULL; -+ } else { -+ this.scheduler.schedule( -+ this.chunkX, this.chunkZ, ChunkStatus.FULL, this, scheduledTasks -+ ); -+ } -+ } else { -+ // now we are fully loaded -+ this.queueBorderFullStatus(true, changedLoadStatus); -+ } -+ } -+ } else { -+ // status downgrade -+ if (!newState.isOrAfter(FullChunkStatus.ENTITY_TICKING) && oldState.isOrAfter(FullChunkStatus.ENTITY_TICKING)) { -+ this.completeFullStatusConsumers(FullChunkStatus.ENTITY_TICKING, null); -+ } -+ -+ if (!newState.isOrAfter(FullChunkStatus.BLOCK_TICKING) && oldState.isOrAfter(FullChunkStatus.BLOCK_TICKING)) { -+ this.completeFullStatusConsumers(FullChunkStatus.BLOCK_TICKING, null); -+ } -+ -+ if (!newState.isOrAfter(FullChunkStatus.FULL) && oldState.isOrAfter(FullChunkStatus.FULL)) { -+ this.completeFullStatusConsumers(FullChunkStatus.FULL, null); -+ } -+ } -+ } -+ -+ if (oldState != newState) { -+ if (this.onTicketUpdate(oldState, newState)) { -+ changedLoadStatus.add(this); -+ } -+ } -+ -+ if (oldUnloaded != newUnloaded) { -+ this.checkUnload(); -+ } -+ } -+ -+ /* -+ For full chunks, vanilla just loads chunks around it up to FEATURES, 1 radius -+ -+ For ticking chunks, it updates the persistent entity manager (soon to be completely nuked by EntitySliceManager, which -+ will also need to be updated but with far less implications) -+ It also shoves the scheduled block ticks into the tick scheduler -+ -+ For entity ticking chunks, updates the entity manager (see above) -+ */ -+ -+ static final int NEIGHBOUR_RADIUS = 2; -+ private long fullNeighbourChunksLoadedBitset; -+ -+ private static int getFullNeighbourIndex(final int relativeX, final int relativeZ) { -+ // index = (relativeX + NEIGHBOUR_CACHE_RADIUS) + (relativeZ + NEIGHBOUR_CACHE_RADIUS) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1) -+ // optimised variant of the above by moving some of the ops to compile time -+ return relativeX + (relativeZ * (NEIGHBOUR_RADIUS * 2 + 1)) + (NEIGHBOUR_RADIUS + NEIGHBOUR_RADIUS * ((NEIGHBOUR_RADIUS * 2 + 1))); -+ } -+ public final boolean isNeighbourFullLoaded(final int relativeX, final int relativeZ) { -+ return (this.fullNeighbourChunksLoadedBitset & (1L << getFullNeighbourIndex(relativeX, relativeZ))) != 0; -+ } -+ -+ // returns true if this chunk changed full status -+ public final boolean setNeighbourFullLoaded(final int relativeX, final int relativeZ) { -+ final long before = this.fullNeighbourChunksLoadedBitset; -+ final int index = getFullNeighbourIndex(relativeX, relativeZ); -+ this.fullNeighbourChunksLoadedBitset |= (1L << index); -+ return this.onNeighbourChange(before, this.fullNeighbourChunksLoadedBitset); -+ } -+ -+ // returns true if this chunk changed full status -+ public final boolean setNeighbourFullUnloaded(final int relativeX, final int relativeZ) { -+ final long before = this.fullNeighbourChunksLoadedBitset; -+ final int index = getFullNeighbourIndex(relativeX, relativeZ); -+ this.fullNeighbourChunksLoadedBitset &= ~(1L << index); -+ return this.onNeighbourChange(before, this.fullNeighbourChunksLoadedBitset); -+ } -+ -+ public static boolean areNeighboursFullLoaded(final long bitset, final int radius) { -+ // index = relativeX + (relativeZ * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)) + (NEIGHBOUR_CACHE_RADIUS + NEIGHBOUR_CACHE_RADIUS * ((NEIGHBOUR_CACHE_RADIUS * 2 + 1))) -+ switch (radius) { -+ case 0: { -+ return (bitset & (1L << getFullNeighbourIndex(0, 0))) != 0L; -+ } -+ case 1: { -+ long mask = 0L; -+ for (int dx = -1; dx <= 1; ++dx) { -+ for (int dz = -1; dz <= 1; ++dz) { -+ mask |= (1L << getFullNeighbourIndex(dx, dz)); -+ } -+ } -+ return (bitset & mask) == mask; -+ } -+ case 2: { -+ long mask = 0L; -+ for (int dx = -2; dx <= 2; ++dx) { -+ for (int dz = -2; dz <= 2; ++dz) { -+ mask |= (1L << getFullNeighbourIndex(dx, dz)); -+ } -+ } -+ return (bitset & mask) == mask; -+ } -+ -+ default: { -+ throw new IllegalArgumentException("Radius not recognized: " + radius); -+ } -+ } -+ } -+ -+ // upper 16 bits are pending status, lower 16 bits are current status -+ private volatile long chunkStatus; -+ private static final long PENDING_STATUS_MASK = Long.MIN_VALUE >> 31; -+ private static final FullChunkStatus[] CHUNK_STATUS_BY_ID = FullChunkStatus.values(); -+ private static final VarHandle CHUNK_STATUS_HANDLE = ConcurrentUtil.getVarHandle(NewChunkHolder.class, "chunkStatus", long.class); -+ -+ public static FullChunkStatus getCurrentChunkStatus(final long encoded) { -+ return CHUNK_STATUS_BY_ID[(int)encoded]; -+ } -+ -+ public static FullChunkStatus getPendingChunkStatus(final long encoded) { -+ return CHUNK_STATUS_BY_ID[(int)(encoded >>> 32)]; -+ } -+ -+ public FullChunkStatus getChunkStatus() { -+ return getCurrentChunkStatus(((long)CHUNK_STATUS_HANDLE.getVolatile((NewChunkHolder)this))); -+ } -+ -+ public boolean isEntityTickingReady() { -+ return this.getChunkStatus().isOrAfter(FullChunkStatus.ENTITY_TICKING); -+ } -+ -+ public boolean isTickingReady() { -+ return this.getChunkStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING); -+ } -+ -+ public boolean isFullChunkReady() { -+ return this.getChunkStatus().isOrAfter(FullChunkStatus.FULL); -+ } -+ -+ private static FullChunkStatus getStatusForBitset(final long bitset) { -+ if (areNeighboursFullLoaded(bitset, 2)) { -+ return FullChunkStatus.ENTITY_TICKING; -+ } else if (areNeighboursFullLoaded(bitset, 1)) { -+ return FullChunkStatus.BLOCK_TICKING; -+ } else if (areNeighboursFullLoaded(bitset, 0)) { -+ return FullChunkStatus.FULL; -+ } else { -+ return FullChunkStatus.INACCESSIBLE; -+ } -+ } -+ -+ // note: only while updating ticket level, so holds ticket update lock + scheduling lock -+ protected final boolean onTicketUpdate(final FullChunkStatus oldState, final FullChunkStatus newState) { -+ if (oldState == newState) { -+ return false; -+ } -+ -+ // preserve border request after full status complete, as it does not set anything in the bitset -+ FullChunkStatus byNeighbours = getStatusForBitset(this.fullNeighbourChunksLoadedBitset); -+ if (byNeighbours == FullChunkStatus.INACCESSIBLE && newState.isOrAfter(FullChunkStatus.FULL) && this.currentGenStatus == ChunkStatus.FULL) { -+ byNeighbours = FullChunkStatus.FULL; -+ } -+ -+ final FullChunkStatus toSet; -+ -+ if (newState.isOrAfter(byNeighbours)) { -+ // must clamp to neighbours level, even though we have the ticket level -+ toSet = byNeighbours; -+ } else { -+ // must clamp to ticket level, even though we have the neighbours -+ toSet = newState; -+ } -+ -+ long curr = (long)CHUNK_STATUS_HANDLE.getVolatile((NewChunkHolder)this); -+ -+ if (curr == ((long)toSet.ordinal() | ((long)toSet.ordinal() << 32))) { -+ // nothing to do -+ return false; -+ } -+ -+ int failures = 0; -+ for (;;) { -+ final long update = (curr & ~PENDING_STATUS_MASK) | ((long)toSet.ordinal() << 32); -+ if (curr == (curr = (long)CHUNK_STATUS_HANDLE.compareAndExchange((NewChunkHolder)this, curr, update))) { -+ return true; -+ } -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ protected final boolean onNeighbourChange(final long bitsetBefore, final long bitsetAfter) { -+ FullChunkStatus oldState = getStatusForBitset(bitsetBefore); -+ FullChunkStatus newState = getStatusForBitset(bitsetAfter); -+ final FullChunkStatus currStateTicketLevel = ChunkLevel.fullStatus(this.oldTicketLevel); -+ if (oldState.isOrAfter(currStateTicketLevel)) { -+ oldState = currStateTicketLevel; -+ } -+ if (newState.isOrAfter(currStateTicketLevel)) { -+ newState = currStateTicketLevel; -+ } -+ // preserve border request after full status complete, as it does not set anything in the bitset -+ if (newState == FullChunkStatus.INACCESSIBLE && currStateTicketLevel.isOrAfter(FullChunkStatus.FULL) && this.currentGenStatus == ChunkStatus.FULL) { -+ newState = FullChunkStatus.FULL; -+ } -+ -+ if (oldState == newState) { -+ return false; -+ } -+ -+ int failures = 0; -+ for (long curr = (long)CHUNK_STATUS_HANDLE.getVolatile((NewChunkHolder)this);;) { -+ final long update = (curr & ~PENDING_STATUS_MASK) | ((long)newState.ordinal() << 32); -+ if (curr == (curr = (long)CHUNK_STATUS_HANDLE.compareAndExchange((NewChunkHolder)this, curr, update))) { -+ return true; -+ } -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ private boolean queueBorderFullStatus(final boolean loaded, final List changedFullStatus) { -+ final FullChunkStatus toStatus = loaded ? FullChunkStatus.FULL : FullChunkStatus.INACCESSIBLE; -+ -+ int failures = 0; -+ for (long curr = (long)CHUNK_STATUS_HANDLE.getVolatile((NewChunkHolder)this);;) { -+ final FullChunkStatus currPending = getPendingChunkStatus(curr); -+ if (loaded && currPending != FullChunkStatus.INACCESSIBLE) { -+ throw new IllegalStateException("Expected " + FullChunkStatus.INACCESSIBLE + " for pending, but got " + currPending); -+ } -+ -+ final long update = (curr & ~PENDING_STATUS_MASK) | ((long)toStatus.ordinal() << 32); -+ if (curr == (curr = (long)CHUNK_STATUS_HANDLE.compareAndExchange((NewChunkHolder)this, curr, update))) { -+ if ((int)(update) != (int)(update >>> 32)) { -+ changedFullStatus.add(this); -+ return true; -+ } -+ return false; -+ } -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ // only call on main thread, must hold ticket level and scheduling lock -+ private void onFullChunkLoadChange(final boolean loaded, final List changedFullStatus) { -+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ, NEIGHBOUR_RADIUS); -+ try { -+ for (int dz = -NEIGHBOUR_RADIUS; dz <= NEIGHBOUR_RADIUS; ++dz) { -+ for (int dx = -NEIGHBOUR_RADIUS; dx <= NEIGHBOUR_RADIUS; ++dx) { -+ final NewChunkHolder holder = (dx | dz) == 0 ? this : this.scheduler.chunkHolderManager.getChunkHolder(dx + this.chunkX, dz + this.chunkZ); -+ if (loaded) { -+ if (holder.setNeighbourFullLoaded(-dx, -dz)) { -+ changedFullStatus.add(holder); -+ } -+ } else { -+ if (holder != null && holder.setNeighbourFullUnloaded(-dx, -dz)) { -+ changedFullStatus.add(holder); -+ } -+ } -+ } -+ } -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ } -+ -+ private FullChunkStatus updateCurrentState(final FullChunkStatus to) { -+ int failures = 0; -+ for (long curr = (long)CHUNK_STATUS_HANDLE.getVolatile((NewChunkHolder)this);;) { -+ final long update = (curr & PENDING_STATUS_MASK) | (long)to.ordinal(); -+ if (curr == (curr = (long)CHUNK_STATUS_HANDLE.compareAndExchange((NewChunkHolder)this, curr, update))) { -+ return getPendingChunkStatus(curr); -+ } -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ private void changeEntityChunkStatus(final FullChunkStatus toStatus) { -+ this.world.getEntityLookup().chunkStatusChange(this.chunkX, this.chunkZ, toStatus); -+ } -+ -+ private boolean processingFullStatus = false; -+ -+ // only to be called on the main thread, no locks need to be held -+ public boolean handleFullStatusChange(final List changedFullStatus) { -+ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot update full status thread off-main"); -+ -+ boolean ret = false; -+ -+ if (this.processingFullStatus) { -+ // we cannot process updates recursively -+ return ret; -+ } -+ -+ // note: use opaque reads for chunk status read since we need it to be atomic -+ -+ // test if anything changed -+ long statusCheck = (long)CHUNK_STATUS_HANDLE.getOpaque((NewChunkHolder)this); -+ if ((int)statusCheck == (int)(statusCheck >>> 32)) { -+ // nothing changed -+ return ret; -+ } -+ -+ final ChunkTaskScheduler scheduler = this.scheduler; -+ final ChunkHolderManager holderManager = scheduler.chunkHolderManager; -+ final int ticketKeep; -+ final Long ticketId = Long.valueOf(holderManager.getNextStatusUpgradeId()); -+ final ReentrantAreaLock.Node ticketLock = holderManager.ticketLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ ticketKeep = this.currentTicketLevel; -+ statusCheck = (long)CHUNK_STATUS_HANDLE.getOpaque((NewChunkHolder)this); -+ // handle race condition where ticket level and target status is updated concurrently -+ if ((int)statusCheck == (int)(statusCheck >>> 32)) { -+ // nothing changed -+ return ret; -+ } -+ holderManager.addTicketAtLevel(TicketType.STATUS_UPGRADE, CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ), ticketKeep, ticketId, false); -+ } finally { -+ holderManager.ticketLockArea.unlock(ticketLock); -+ } -+ -+ this.processingFullStatus = true; -+ try { -+ for (;;) { -+ final long currStateEncoded = (long)CHUNK_STATUS_HANDLE.getOpaque((NewChunkHolder)this); -+ final FullChunkStatus currState = getCurrentChunkStatus(currStateEncoded); -+ FullChunkStatus nextState = getPendingChunkStatus(currStateEncoded); -+ if (currState == nextState) { -+ if (nextState == FullChunkStatus.INACCESSIBLE) { -+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ); -+ try { -+ this.checkUnload(); -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ } -+ break; -+ } -+ -+ // chunks cannot downgrade state while status is pending a change -+ final LevelChunk chunk = (LevelChunk)this.currentChunk; -+ -+ // Note: we assume that only load/unload contain plugin logic -+ // plugin logic is anything stupid enough to possibly change the chunk status while it is already -+ // being changed (i.e during load it is possible it will try to set to full ticking) -+ // in order to allow this change, we also need this plugin logic to be contained strictly after all -+ // of the chunk system load callbacks are invoked -+ if (nextState.isOrAfter(currState)) { -+ // state upgrade -+ if (!currState.isOrAfter(FullChunkStatus.FULL) && nextState.isOrAfter(FullChunkStatus.FULL)) { -+ nextState = this.updateCurrentState(FullChunkStatus.FULL); -+ holderManager.ensureInAutosave(this); -+ chunk.pushChunkIntoLoadedMap(); -+ this.changeEntityChunkStatus(FullChunkStatus.FULL); -+ chunk.onChunkLoad(this); -+ this.onFullChunkLoadChange(true, changedFullStatus); -+ this.completeFullStatusConsumers(FullChunkStatus.FULL, chunk); -+ } -+ -+ if (!currState.isOrAfter(FullChunkStatus.BLOCK_TICKING) && nextState.isOrAfter(FullChunkStatus.BLOCK_TICKING)) { -+ nextState = this.updateCurrentState(FullChunkStatus.BLOCK_TICKING); -+ this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING); -+ chunk.onChunkTicking(this); -+ this.completeFullStatusConsumers(FullChunkStatus.BLOCK_TICKING, chunk); -+ } -+ -+ if (!currState.isOrAfter(FullChunkStatus.ENTITY_TICKING) && nextState.isOrAfter(FullChunkStatus.ENTITY_TICKING)) { -+ nextState = this.updateCurrentState(FullChunkStatus.ENTITY_TICKING); -+ this.changeEntityChunkStatus(FullChunkStatus.ENTITY_TICKING); -+ chunk.onChunkEntityTicking(this); -+ this.completeFullStatusConsumers(FullChunkStatus.ENTITY_TICKING, chunk); -+ } -+ } else { -+ if (currState.isOrAfter(FullChunkStatus.ENTITY_TICKING) && !nextState.isOrAfter(FullChunkStatus.ENTITY_TICKING)) { -+ this.changeEntityChunkStatus(FullChunkStatus.BLOCK_TICKING); -+ chunk.onChunkNotEntityTicking(this); -+ nextState = this.updateCurrentState(FullChunkStatus.BLOCK_TICKING); -+ } -+ -+ if (currState.isOrAfter(FullChunkStatus.BLOCK_TICKING) && !nextState.isOrAfter(FullChunkStatus.BLOCK_TICKING)) { -+ this.changeEntityChunkStatus(FullChunkStatus.FULL); -+ chunk.onChunkNotTicking(this); -+ nextState = this.updateCurrentState(FullChunkStatus.FULL); -+ } -+ -+ if (currState.isOrAfter(FullChunkStatus.FULL) && !nextState.isOrAfter(FullChunkStatus.FULL)) { -+ this.onFullChunkLoadChange(false, changedFullStatus); -+ this.changeEntityChunkStatus(FullChunkStatus.INACCESSIBLE); -+ chunk.onChunkUnload(this); -+ nextState = this.updateCurrentState(FullChunkStatus.INACCESSIBLE); -+ } -+ } -+ -+ ret = true; -+ } -+ } finally { -+ this.processingFullStatus = false; -+ holderManager.removeTicketAtLevel(TicketType.STATUS_UPGRADE, this.chunkX, this.chunkZ, ticketKeep, ticketId); -+ } -+ -+ return ret; -+ } -+ -+ // note: must hold scheduling lock -+ // rets true if the current requested gen status is not null (effectively, whether further scheduling is not needed) -+ boolean upgradeGenTarget(final ChunkStatus toStatus) { -+ if (toStatus == null) { -+ throw new NullPointerException("toStatus cannot be null"); -+ } -+ if (this.requestedGenStatus == null && this.generationTask == null) { -+ return false; -+ } -+ if (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(toStatus)) { -+ this.requestedGenStatus = toStatus; -+ } -+ return true; -+ } -+ -+ public void setGenerationTarget(final ChunkStatus toStatus) { -+ this.requestedGenStatus = toStatus; -+ } -+ -+ public boolean hasGenerationTask() { -+ return this.generationTask != null; -+ } -+ -+ public ChunkStatus getCurrentGenStatus() { -+ return this.currentGenStatus; -+ } -+ -+ public ChunkStatus getRequestedGenStatus() { -+ return this.requestedGenStatus; -+ } -+ -+ private final Reference2ObjectOpenHashMap>> statusWaiters = new Reference2ObjectOpenHashMap<>(); -+ -+ void addStatusConsumer(final ChunkStatus status, final Consumer consumer) { -+ this.statusWaiters.computeIfAbsent(status, (final ChunkStatus keyInMap) -> { -+ return new ArrayList<>(4); -+ }).add(consumer); -+ } -+ -+ private void completeStatusConsumers(ChunkStatus status, final ChunkAccess chunk) { -+ // need to tell future statuses to complete if cancelled -+ do { -+ this.completeStatusConsumers0(status, chunk); -+ } while (chunk == null && status != (status = status.getNextStatus())); -+ } -+ -+ private void completeStatusConsumers0(final ChunkStatus status, final ChunkAccess chunk) { -+ final List> consumers; -+ consumers = this.statusWaiters.remove(status); -+ -+ if (consumers == null) { -+ return; -+ } -+ -+ // must be scheduled to main, we do not trust the callback to not do anything stupid -+ this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { -+ for (final Consumer consumer : consumers) { -+ try { -+ consumer.accept(chunk); -+ } catch (final ThreadDeath thr) { -+ throw thr; -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to process chunk status callback", thr); -+ } -+ } -+ }, PrioritisedExecutor.Priority.HIGHEST); -+ } -+ -+ private final Reference2ObjectOpenHashMap>> fullStatusWaiters = new Reference2ObjectOpenHashMap<>(); -+ -+ void addFullStatusConsumer(final FullChunkStatus status, final Consumer consumer) { -+ this.fullStatusWaiters.computeIfAbsent(status, (final FullChunkStatus keyInMap) -> { -+ return new ArrayList<>(4); -+ }).add(consumer); -+ } -+ -+ private void completeFullStatusConsumers(FullChunkStatus status, final LevelChunk chunk) { -+ // need to tell future statuses to complete if cancelled -+ final FullChunkStatus max = CHUNK_STATUS_BY_ID[CHUNK_STATUS_BY_ID.length - 1]; -+ -+ for (;;) { -+ this.completeFullStatusConsumers0(status, chunk); -+ if (chunk != null || status == max) { -+ break; -+ } -+ status = CHUNK_STATUS_BY_ID[status.ordinal() + 1]; -+ } -+ } -+ -+ private void completeFullStatusConsumers0(final FullChunkStatus status, final LevelChunk chunk) { -+ final List> consumers; -+ consumers = this.fullStatusWaiters.remove(status); -+ -+ if (consumers == null) { -+ return; -+ } -+ -+ // must be scheduled to main, we do not trust the callback to not do anything stupid -+ this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { -+ for (final Consumer consumer : consumers) { -+ try { -+ consumer.accept(chunk); -+ } catch (final ThreadDeath thr) { -+ throw thr; -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to process chunk status callback", thr); -+ } -+ } -+ }, PrioritisedExecutor.Priority.HIGHEST); -+ } -+ -+ // note: must hold scheduling lock -+ private void onChunkGenComplete(final ChunkAccess newChunk, final ChunkStatus newStatus, -+ final List scheduleList, final List changedLoadStatus) { -+ if (!this.neighboursBlockingGenTask.isEmpty()) { -+ throw new IllegalStateException("Cannot have neighbours blocking this gen task"); -+ } -+ if (newChunk != null || (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(newStatus))) { -+ this.completeStatusConsumers(newStatus, newChunk); -+ } -+ // done now, clear state (must be done before scheduling new tasks) -+ this.generationTask = null; -+ this.generationTaskStatus = null; -+ if (newChunk == null) { -+ // task was cancelled -+ // should be careful as this could be called while holding the schedule lock and/or inside the -+ // ticket level update -+ // while a task may be cancelled, it is possible for it to be later re-scheduled -+ // however, because generationTask is only set to null on _completion_, the scheduler leaves -+ // the rescheduling logic to us here -+ final ChunkStatus requestedGenStatus = this.requestedGenStatus; -+ this.requestedGenStatus = null; -+ if (requestedGenStatus != null) { -+ // it looks like it has been requested, so we must reschedule -+ if (!this.neighboursWaitingForUs.isEmpty()) { -+ for (final Iterator> iterator = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { -+ final Reference2ObjectMap.Entry entry = iterator.next(); -+ -+ final NewChunkHolder chunkHolder = entry.getKey(); -+ final ChunkStatus toStatus = entry.getValue(); -+ -+ if (!requestedGenStatus.isOrAfter(toStatus)) { -+ // if we were cancelled, we are responsible for removing the waiter -+ if (!chunkHolder.neighboursBlockingGenTask.remove(this)) { -+ throw new IllegalStateException("Corrupt state"); -+ } -+ if (chunkHolder.neighboursBlockingGenTask.isEmpty()) { -+ chunkHolder.checkUnload(); -+ } -+ iterator.remove(); -+ continue; -+ } -+ } -+ } -+ -+ // note: only after generationTask -> null, generationTaskStatus -> null, and requestedGenStatus -> null -+ this.scheduler.schedule( -+ this.chunkX, this.chunkZ, requestedGenStatus, this, scheduleList -+ ); -+ -+ // return, can't do anything further -+ return; -+ } -+ -+ if (!this.neighboursWaitingForUs.isEmpty()) { -+ for (final NewChunkHolder chunkHolder : this.neighboursWaitingForUs.keySet()) { -+ if (!chunkHolder.neighboursBlockingGenTask.remove(this)) { -+ throw new IllegalStateException("Corrupt state"); -+ } -+ if (chunkHolder.neighboursBlockingGenTask.isEmpty()) { -+ chunkHolder.checkUnload(); -+ } -+ } -+ this.neighboursWaitingForUs.clear(); -+ } -+ // reset priority, we have nothing left to generate to -+ this.setPriority(PrioritisedExecutor.Priority.NORMAL); -+ this.checkUnload(); -+ return; -+ } -+ -+ this.currentChunk = newChunk; -+ this.currentGenStatus = newStatus; -+ this.lastChunkCompletion = new ChunkCompletion(newChunk, newStatus); -+ -+ final ChunkStatus requestedGenStatus = this.requestedGenStatus; -+ -+ List needsScheduling = null; -+ boolean recalculatePriority = false; -+ for (final Iterator> iterator -+ = this.neighboursWaitingForUs.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { -+ final Reference2ObjectMap.Entry entry = iterator.next(); -+ final NewChunkHolder neighbour = entry.getKey(); -+ final ChunkStatus requiredStatus = entry.getValue(); -+ -+ if (!newStatus.isOrAfter(requiredStatus)) { -+ if (requestedGenStatus == null || !requestedGenStatus.isOrAfter(requiredStatus)) { -+ // if we're cancelled, still need to clear this map -+ if (!neighbour.neighboursBlockingGenTask.remove(this)) { -+ throw new IllegalStateException("Neighbour is not waiting for us?"); -+ } -+ if (neighbour.neighboursBlockingGenTask.isEmpty()) { -+ neighbour.checkUnload(); -+ } -+ -+ iterator.remove(); -+ } -+ continue; -+ } -+ -+ // doesn't matter what isCancelled is here, we need to schedule if we can -+ -+ recalculatePriority = true; -+ if (!neighbour.neighboursBlockingGenTask.remove(this)) { -+ throw new IllegalStateException("Neighbour is not waiting for us?"); -+ } -+ -+ if (neighbour.neighboursBlockingGenTask.isEmpty()) { -+ if (neighbour.requestedGenStatus != null) { -+ if (needsScheduling == null) { -+ needsScheduling = new ArrayList<>(); -+ } -+ needsScheduling.add(neighbour); -+ } else { -+ neighbour.checkUnload(); -+ } -+ } -+ -+ // remove last; access to entry will throw if removed -+ iterator.remove(); -+ } -+ -+ if (newStatus == ChunkStatus.FULL) { -+ this.lockPriority(); -+ // must use oldTicketLevel, we hold the schedule lock but not the ticket level lock -+ // however, schedule lock needs to be held for ticket level callback, so we're fine here -+ if (ChunkLevel.fullStatus(this.oldTicketLevel).isOrAfter(FullChunkStatus.FULL)) { -+ this.queueBorderFullStatus(true, changedLoadStatus); -+ } -+ } -+ -+ if (recalculatePriority) { -+ this.recalculateNeighbourRequestedPriority(); -+ } -+ -+ if (requestedGenStatus != null && !newStatus.isOrAfter(requestedGenStatus)) { -+ this.scheduleNeighbours(needsScheduling, scheduleList); -+ -+ // we need to schedule more tasks now -+ this.scheduler.schedule( -+ this.chunkX, this.chunkZ, requestedGenStatus, this, scheduleList -+ ); -+ } else { -+ // we're done now -+ if (requestedGenStatus != null) { -+ this.requestedGenStatus = null; -+ } -+ // reached final stage, so stop scheduling now -+ this.setPriority(PrioritisedExecutor.Priority.NORMAL); -+ this.checkUnload(); -+ -+ this.scheduleNeighbours(needsScheduling, scheduleList); -+ } -+ } -+ -+ private void scheduleNeighbours(final List needsScheduling, final List scheduleList) { -+ if (needsScheduling != null) { -+ for (int i = 0, len = needsScheduling.size(); i < len; ++i) { -+ final NewChunkHolder neighbour = needsScheduling.get(i); -+ -+ this.scheduler.schedule( -+ neighbour.chunkX, neighbour.chunkZ, neighbour.requestedGenStatus, neighbour, scheduleList -+ ); -+ } -+ } -+ } -+ -+ public void setGenerationTask(final ChunkProgressionTask generationTask, final ChunkStatus taskStatus, -+ final List neighbours) { -+ if (this.generationTask != null || (this.currentGenStatus != null && this.currentGenStatus.isOrAfter(taskStatus))) { -+ throw new IllegalStateException("Currently generating or provided task is trying to generate to a level we are already at!"); -+ } -+ if (this.requestedGenStatus == null || !this.requestedGenStatus.isOrAfter(taskStatus)) { -+ throw new IllegalStateException("Cannot schedule generation task when not requested"); -+ } -+ this.generationTask = generationTask; -+ this.generationTaskStatus = taskStatus; -+ -+ for (int i = 0, len = neighbours.size(); i < len; ++i) { -+ neighbours.get(i).addNeighbourUsingChunk(); -+ } -+ -+ this.checkUnload(); -+ -+ generationTask.onComplete((final ChunkAccess access, final Throwable thr) -> { -+ if (generationTask != this.generationTask) { -+ throw new IllegalStateException( -+ "Cannot complete generation task '" + generationTask + "' because we are waiting on '" + this.generationTask + "' instead!" -+ ); -+ } -+ if (thr != null) { -+ if (this.genTaskException != null) { -+ // first one is probably the TRUE problem -+ return; -+ } -+ // don't set generation task to null, so that scheduling will not attempt to create another task and it -+ // will automatically block any further scheduling usage of this chunk as it will wait forever for a failed -+ // task to complete -+ this.genTaskException = thr; -+ this.failedGenStatus = taskStatus; -+ this.genTaskFailedThread = Thread.currentThread(); -+ -+ this.scheduler.unrecoverableChunkSystemFailure(this.chunkX, this.chunkZ, Map.of( -+ "Generation task", ChunkTaskScheduler.stringIfNull(generationTask), -+ "Task to status", ChunkTaskScheduler.stringIfNull(taskStatus) -+ ), thr); -+ return; -+ } -+ -+ final boolean scheduleTasks; -+ List tasks = ChunkHolderManager.getCurrentTicketUpdateScheduling(); -+ if (tasks == null) { -+ scheduleTasks = true; -+ tasks = new ArrayList<>(); -+ } else { -+ scheduleTasks = false; -+ // we are currently updating ticket levels, so we already hold the schedule lock -+ // this means we have to leave the ticket level update to handle the scheduling -+ } -+ final List changedLoadStatus = new ArrayList<>(); -+ // theoretically, we could schedule a chunk at the max radius which performs another max radius access. So we need to double the radius. -+ final ReentrantAreaLock.Node schedulingLock = this.scheduler.schedulingLockArea.lock(this.chunkX, this.chunkZ, 2 * ChunkTaskScheduler.getMaxAccessRadius()); -+ try { -+ for (int i = 0, len = neighbours.size(); i < len; ++i) { -+ neighbours.get(i).removeNeighbourUsingChunk(); -+ } -+ this.onChunkGenComplete(access, taskStatus, tasks, changedLoadStatus); -+ } finally { -+ this.scheduler.schedulingLockArea.unlock(schedulingLock); -+ } -+ this.scheduler.chunkHolderManager.addChangedStatuses(changedLoadStatus); -+ -+ if (scheduleTasks) { -+ // can't hold the lock while scheduling, so we have to build the tasks and then schedule after -+ for (int i = 0, len = tasks.size(); i < len; ++i) { -+ tasks.get(i).schedule(); -+ } -+ } -+ }); -+ } -+ -+ public PoiChunk getPoiChunk() { -+ return this.poiChunk; -+ } -+ -+ public ChunkEntitySlices getEntityChunk() { -+ return this.entityChunk; -+ } -+ -+ public long lastAutoSave; -+ -+ public static final record SaveStat(boolean savedChunk, boolean savedEntityChunk, boolean savedPoiChunk) {} -+ -+ public SaveStat save(final boolean shutdown, final boolean unloading) { -+ TickThread.ensureTickThread(this.world, this.chunkX, this.chunkZ, "Cannot save data off-main"); -+ -+ ChunkAccess chunk = this.getCurrentChunk(); -+ PoiChunk poi = this.getPoiChunk(); -+ ChunkEntitySlices entities = this.getEntityChunk(); -+ boolean executedUnloadTask = false; -+ -+ if (shutdown) { -+ // make sure that the async unloads complete -+ if (this.unloadState != null) { -+ // must have errored during unload -+ chunk = this.unloadState.chunk(); -+ poi = this.unloadState.poiChunk(); -+ entities = this.unloadState.entityChunk(); -+ } -+ final UnloadTask chunkUnloadTask = this.chunkDataUnload; -+ final DelayedPrioritisedTask chunkDataUnloadTask = chunkUnloadTask == null ? null : chunkUnloadTask.task(); -+ if (chunkDataUnloadTask != null) { -+ final PrioritisedExecutor.PrioritisedTask unloadTask = chunkDataUnloadTask.getTask(); -+ if (unloadTask != null) { -+ executedUnloadTask = unloadTask.execute(); -+ } -+ } -+ } -+ -+ boolean canSaveChunk = !(chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave) && -+ (chunk != null && ((shutdown || chunk instanceof LevelChunk) && chunk.isUnsaved())); -+ boolean canSavePOI = !(chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave) && (poi != null && poi.isDirty()); -+ boolean canSaveEntities = entities != null; -+ -+ try (co.aikar.timings.Timing ignored = this.world.timings.chunkSave.startTiming()) { // Paper -+ if (canSaveChunk) { -+ canSaveChunk = this.saveChunk(chunk, unloading); -+ } -+ if (canSavePOI) { -+ canSavePOI = this.savePOI(poi, unloading); -+ } -+ if (canSaveEntities) { -+ // on shutdown, we need to force transient entity chunks to save -+ canSaveEntities = this.saveEntities(entities, unloading || shutdown); -+ if (unloading || shutdown) { -+ this.lastEntityUnload = null; -+ } -+ } -+ } -+ -+ return executedUnloadTask | canSaveChunk | canSaveEntities | canSavePOI ? new SaveStat(executedUnloadTask || canSaveChunk, canSaveEntities, canSavePOI): null; -+ } -+ -+ static final class AsyncChunkSerializeTask implements Runnable { -+ -+ private final ServerLevel world; -+ private final ChunkAccess chunk; -+ private final ChunkSerializer.AsyncSaveData asyncSaveData; -+ private final NewChunkHolder toComplete; -+ -+ public AsyncChunkSerializeTask(final ServerLevel world, final ChunkAccess chunk, final ChunkSerializer.AsyncSaveData asyncSaveData, -+ final NewChunkHolder toComplete) { -+ this.world = world; -+ this.chunk = chunk; -+ this.asyncSaveData = asyncSaveData; -+ this.toComplete = toComplete; -+ } -+ -+ @Override -+ public void run() { -+ final CompoundTag toSerialize; -+ try { -+ toSerialize = ChunkSerializer.saveChunk(this.world, this.chunk, this.asyncSaveData); -+ } catch (final ThreadDeath death) { -+ throw death; -+ } catch (final Throwable throwable) { -+ LOGGER.error("Failed to asynchronously save chunk " + this.chunk.getPos() + " for world '" + this.world.getWorld().getName() + "', falling back to synchronous save", throwable); -+ this.world.chunkTaskScheduler.scheduleChunkTask(this.chunk.locX, this.chunk.locZ, () -> { -+ final CompoundTag synchronousSave; -+ try { -+ synchronousSave = ChunkSerializer.saveChunk(AsyncChunkSerializeTask.this.world, AsyncChunkSerializeTask.this.chunk, AsyncChunkSerializeTask.this.asyncSaveData); -+ } catch (final ThreadDeath death) { -+ throw death; -+ } catch (final Throwable throwable2) { -+ LOGGER.error("Failed to synchronously save chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + AsyncChunkSerializeTask.this.world.getWorld().getName() + "', chunk data will be lost", throwable2); -+ AsyncChunkSerializeTask.this.toComplete.completeAsyncChunkDataSave(null); -+ return; -+ } -+ -+ AsyncChunkSerializeTask.this.toComplete.completeAsyncChunkDataSave(synchronousSave); -+ LOGGER.info("Successfully serialized chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + AsyncChunkSerializeTask.this.world.getWorld().getName() + "' synchronously"); -+ -+ }, PrioritisedExecutor.Priority.HIGHEST); -+ return; -+ } -+ this.toComplete.completeAsyncChunkDataSave(toSerialize); -+ } -+ -+ @Override -+ public String toString() { -+ return "AsyncChunkSerializeTask{" + -+ "chunk={pos=" + this.chunk.getPos() + ",world=\"" + this.world.getWorld().getName() + "\"}" + -+ "}"; -+ } -+ } -+ -+ private boolean saveChunk(final ChunkAccess chunk, final boolean unloading) { -+ if (!chunk.isUnsaved()) { -+ if (unloading) { -+ this.completeAsyncChunkDataSave(null); -+ } -+ return false; -+ } -+ boolean completing = false; -+ try { -+ if (unloading) { -+ try { -+ final ChunkSerializer.AsyncSaveData asyncSaveData = ChunkSerializer.getAsyncSaveData(this.world, chunk); -+ -+ final PrioritisedExecutor.PrioritisedTask task = this.scheduler.loadExecutor.createTask(new AsyncChunkSerializeTask(this.world, chunk, asyncSaveData, this)); -+ -+ this.chunkDataUnload.task().setTask(task); -+ -+ task.queue(); -+ -+ chunk.setUnsaved(false); -+ -+ return true; -+ } catch (final ThreadDeath death) { -+ throw death; -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to prepare async chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "', falling back to synchronous save", thr); -+ // fall through to synchronous save -+ } -+ } -+ -+ final CompoundTag save = ChunkSerializer.saveChunk(this.world, chunk, null); -+ -+ if (unloading) { -+ completing = true; -+ this.completeAsyncChunkDataSave(save); -+ LOGGER.info("Successfully serialized chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "' synchronously"); -+ } else { -+ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.CHUNK_DATA); -+ } -+ chunk.setUnsaved(false); -+ } catch (final ThreadDeath death) { -+ throw death; -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to save chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'"); -+ if (unloading && !completing) { -+ this.completeAsyncChunkDataSave(null); -+ } -+ } -+ -+ return true; -+ } -+ -+ private boolean lastEntitySaveNull; -+ private CompoundTag lastEntityUnload; -+ private boolean saveEntities(final ChunkEntitySlices entities, final boolean unloading) { -+ try { -+ CompoundTag mergeFrom = null; -+ if (entities.isTransient()) { -+ if (!unloading) { -+ // if we're a transient chunk, we cannot save until unloading because otherwise a double save will -+ // result in double adding the entities -+ return false; -+ } -+ try { -+ mergeFrom = RegionFileIOThread.loadData(this.world, this.chunkX, this.chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, PrioritisedExecutor.Priority.BLOCKING); -+ } catch (final Exception ex) { -+ LOGGER.error("Cannot merge transient entities for chunk (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "', data on disk will be replaced", ex); -+ } -+ } -+ -+ final CompoundTag save = entities.save(); -+ if (mergeFrom != null) { -+ if (save == null) { -+ // don't override the data on disk with nothing -+ return false; -+ } else { -+ EntityStorage.copyEntities(mergeFrom, save); -+ } -+ } -+ if (save == null && this.lastEntitySaveNull) { -+ return false; -+ } -+ -+ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.ENTITY_DATA); -+ this.lastEntitySaveNull = save == null; -+ if (unloading) { -+ this.lastEntityUnload = save; -+ } -+ } catch (final ThreadDeath death) { -+ throw death; -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to save entity data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'"); -+ } -+ -+ return true; -+ } -+ -+ private boolean lastPoiSaveNull; -+ private boolean savePOI(final PoiChunk poi, final boolean unloading) { -+ try { -+ final CompoundTag save = poi.save(); -+ poi.setDirty(false); -+ if (save == null && this.lastPoiSaveNull) { -+ if (unloading) { -+ this.poiDataUnload.completable().complete(null); -+ } -+ return false; -+ } -+ -+ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.POI_DATA); -+ this.lastPoiSaveNull = save == null; -+ if (unloading) { -+ this.poiDataUnload.completable().complete(save); -+ } -+ } catch (final ThreadDeath death) { -+ throw death; -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to save poi data (" + this.chunkX + "," + this.chunkZ + ") in world '" + this.world.getWorld().getName() + "'"); -+ } -+ -+ return true; -+ } -+ -+ @Override -+ public String toString() { -+ final ChunkCompletion lastCompletion = this.lastChunkCompletion; -+ final ChunkEntitySlices entityChunk = this.entityChunk; -+ final long chunkStatus = this.chunkStatus; -+ final int fullChunkStatus = (int)chunkStatus; -+ final int pendingChunkStatus = (int)(chunkStatus >>> 32); -+ final FullChunkStatus currentFullStatus = fullChunkStatus < 0 || fullChunkStatus >= CHUNK_STATUS_BY_ID.length ? null : CHUNK_STATUS_BY_ID[fullChunkStatus]; -+ final FullChunkStatus pendingFullStatus = pendingChunkStatus < 0 || pendingChunkStatus >= CHUNK_STATUS_BY_ID.length ? null : CHUNK_STATUS_BY_ID[pendingChunkStatus]; -+ return "NewChunkHolder{" + -+ "world=" + this.world.getWorld().getName() + -+ ", chunkX=" + this.chunkX + -+ ", chunkZ=" + this.chunkZ + -+ ", entityChunkFromDisk=" + (entityChunk != null && !entityChunk.isTransient()) + -+ ", lastChunkCompletion={chunk_class=" + (lastCompletion == null || lastCompletion.chunk() == null ? "null" : lastCompletion.chunk().getClass().getName()) + ",status=" + (lastCompletion == null ? "null" : lastCompletion.genStatus()) + "}" + -+ ", currentGenStatus=" + this.currentGenStatus + -+ ", requestedGenStatus=" + this.requestedGenStatus + -+ ", generationTask=" + this.generationTask + -+ ", generationTaskStatus=" + this.generationTaskStatus + -+ ", priority=" + this.priority + -+ ", priorityLocked=" + this.priorityLocked + -+ ", neighbourRequestedPriority=" + this.neighbourRequestedPriority + -+ ", effective_priority=" + this.getEffectivePriority() + -+ ", oldTicketLevel=" + this.oldTicketLevel + -+ ", currentTicketLevel=" + this.currentTicketLevel + -+ ", totalNeighboursUsingThisChunk=" + this.totalNeighboursUsingThisChunk + -+ ", fullNeighbourChunksLoadedBitset=" + this.fullNeighbourChunksLoadedBitset + -+ ", chunkStatusRaw=" + chunkStatus + -+ ", currentChunkStatus=" + currentFullStatus + -+ ", pendingChunkStatus=" + pendingFullStatus + -+ ", is_unload_safe=" + this.isSafeToUnload() + -+ ", killed=" + this.killed + -+ '}'; -+ } -+ -+ private static JsonElement serializeCompletable(final Completable completable) { -+ if (completable == null) { -+ return new JsonPrimitive("null"); -+ } -+ -+ final JsonObject ret = new JsonObject(); -+ final boolean isCompleted = completable.isCompleted(); -+ ret.addProperty("completed", Boolean.valueOf(isCompleted)); -+ -+ if (isCompleted) { -+ ret.addProperty("completed_exceptionally", Boolean.valueOf(completable.getThrowable() != null)); -+ } -+ -+ return ret; -+ } -+ -+ // holds ticket and scheduling lock -+ public JsonObject getDebugJson() { -+ final JsonObject ret = new JsonObject(); -+ -+ final ChunkCompletion lastCompletion = this.lastChunkCompletion; -+ final ChunkEntitySlices slices = this.entityChunk; -+ final PoiChunk poiChunk = this.poiChunk; -+ -+ ret.addProperty("chunkX", Integer.valueOf(this.chunkX)); -+ ret.addProperty("chunkZ", Integer.valueOf(this.chunkZ)); -+ ret.addProperty("entity_chunk", slices == null ? "null" : "transient=" + slices.isTransient()); -+ ret.addProperty("poi_chunk", "null=" + (poiChunk == null)); -+ ret.addProperty("completed_chunk_class", lastCompletion == null ? "null" : lastCompletion.chunk().getClass().getName()); -+ ret.addProperty("completed_gen_status", lastCompletion == null ? "null" : lastCompletion.genStatus().toString()); -+ ret.addProperty("priority", Objects.toString(this.priority)); -+ ret.addProperty("neighbour_requested_priority", Objects.toString(this.neighbourRequestedPriority)); -+ ret.addProperty("generation_task", Objects.toString(this.generationTask)); -+ ret.addProperty("is_safe_unload", Objects.toString(this.isSafeToUnload())); -+ ret.addProperty("old_ticket_level", Integer.valueOf(this.oldTicketLevel)); -+ ret.addProperty("current_ticket_level", Integer.valueOf(this.currentTicketLevel)); -+ ret.addProperty("neighbours_using_chunk", Integer.valueOf(this.totalNeighboursUsingThisChunk)); -+ -+ final JsonObject neighbourWaitState = new JsonObject(); -+ ret.add("neighbour_state", neighbourWaitState); -+ -+ final JsonArray blockingGenNeighbours = new JsonArray(); -+ neighbourWaitState.add("blocking_gen_task", blockingGenNeighbours); -+ for (final NewChunkHolder blockingGenNeighbour : this.neighboursBlockingGenTask) { -+ final JsonObject neighbour = new JsonObject(); -+ blockingGenNeighbours.add(neighbour); -+ -+ neighbour.addProperty("chunkX", Integer.valueOf(blockingGenNeighbour.chunkX)); -+ neighbour.addProperty("chunkZ", Integer.valueOf(blockingGenNeighbour.chunkZ)); -+ } -+ -+ final JsonArray neighboursWaitingForUs = new JsonArray(); -+ neighbourWaitState.add("neighbours_waiting_on_us", neighboursWaitingForUs); -+ for (final Reference2ObjectMap.Entry entry : this.neighboursWaitingForUs.reference2ObjectEntrySet()) { -+ final NewChunkHolder holder = entry.getKey(); -+ final ChunkStatus status = entry.getValue(); -+ -+ final JsonObject neighbour = new JsonObject(); -+ neighboursWaitingForUs.add(neighbour); -+ -+ -+ neighbour.addProperty("chunkX", Integer.valueOf(holder.chunkX)); -+ neighbour.addProperty("chunkZ", Integer.valueOf(holder.chunkZ)); -+ neighbour.addProperty("waiting_for", Objects.toString(status)); -+ } -+ -+ ret.addProperty("fullchunkstatus", Objects.toString(this.getChunkStatus())); -+ ret.addProperty("fullchunkstatus_raw", Long.valueOf(this.chunkStatus)); -+ ret.addProperty("generation_task", Objects.toString(this.generationTask)); -+ ret.addProperty("requested_generation", Objects.toString(this.requestedGenStatus)); -+ ret.addProperty("has_entity_load_task", Boolean.valueOf(this.entityDataLoadTask != null)); -+ ret.addProperty("has_poi_load_task", Boolean.valueOf(this.poiDataLoadTask != null)); -+ -+ final UnloadTask entityDataUnload = this.entityDataUnload; -+ final UnloadTask poiDataUnload = this.poiDataUnload; -+ final UnloadTask chunkDataUnload = this.chunkDataUnload; -+ -+ ret.add("entity_unload_completable", serializeCompletable(entityDataUnload == null ? null : entityDataUnload.completable())); -+ ret.add("poi_unload_completable", serializeCompletable(poiDataUnload == null ? null : poiDataUnload.completable())); -+ ret.add("chunk_unload_completable", serializeCompletable(chunkDataUnload == null ? null : chunkDataUnload.completable())); -+ -+ final DelayedPrioritisedTask unloadTask = chunkDataUnload == null ? null : chunkDataUnload.task(); -+ if (unloadTask == null) { -+ ret.addProperty("unload_task_priority", "null"); -+ ret.addProperty("unload_task_priority_raw", "null"); -+ } else { -+ ret.addProperty("unload_task_priority", Objects.toString(unloadTask.getPriority())); -+ ret.addProperty("unload_task_priority_raw", Integer.valueOf(unloadTask.getPriorityInternal())); -+ } -+ -+ ret.addProperty("killed", Boolean.valueOf(this.killed)); -+ -+ return ret; -+ } -+} -diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/PriorityHolder.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/PriorityHolder.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b4c56bf12dc8dd17452210ece4fd67411cc6b2fd ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/PriorityHolder.java -@@ -0,0 +1,215 @@ -+package io.papermc.paper.chunk.system.scheduling; -+ -+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; -+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; -+import java.lang.invoke.VarHandle; -+ -+public abstract class PriorityHolder { -+ -+ protected volatile int priority; -+ protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(PriorityHolder.class, "priority", int.class); -+ -+ protected static final int PRIORITY_SCHEDULED = Integer.MIN_VALUE >>> 0; -+ protected static final int PRIORITY_EXECUTED = Integer.MIN_VALUE >>> 1; -+ -+ protected final int getPriorityVolatile() { -+ return (int)PRIORITY_HANDLE.getVolatile((PriorityHolder)this); -+ } -+ -+ protected final int compareAndExchangePriorityVolatile(final int expect, final int update) { -+ return (int)PRIORITY_HANDLE.compareAndExchange((PriorityHolder)this, (int)expect, (int)update); -+ } -+ -+ protected final int getAndOrPriorityVolatile(final int val) { -+ return (int)PRIORITY_HANDLE.getAndBitwiseOr((PriorityHolder)this, (int)val); -+ } -+ -+ protected final void setPriorityPlain(final int val) { -+ PRIORITY_HANDLE.set((PriorityHolder)this, (int)val); -+ } -+ -+ protected PriorityHolder(final PrioritisedExecutor.Priority priority) { -+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ this.setPriorityPlain(priority.priority); -+ } -+ -+ // used only for debug json -+ public boolean isScheduled() { -+ return (this.getPriorityVolatile() & PRIORITY_SCHEDULED) != 0; -+ } -+ -+ // returns false if cancelled -+ protected boolean markExecuting() { -+ return (this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) == 0; -+ } -+ -+ protected boolean isMarkedExecuted() { -+ return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0; -+ } -+ -+ public void cancel() { -+ if ((this.getAndOrPriorityVolatile(PRIORITY_EXECUTED) & PRIORITY_EXECUTED) != 0) { -+ // cancelled already -+ return; -+ } -+ this.cancelScheduled(); -+ } -+ -+ public void schedule() { -+ int priority = this.getPriorityVolatile(); -+ -+ if ((priority & PRIORITY_SCHEDULED) != 0) { -+ throw new IllegalStateException("schedule() called twice"); -+ } -+ -+ if ((priority & PRIORITY_EXECUTED) != 0) { -+ // cancelled -+ return; -+ } -+ -+ this.scheduleTask(PrioritisedExecutor.Priority.getPriority(priority)); -+ -+ int failures = 0; -+ for (;;) { -+ if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | PRIORITY_SCHEDULED))) { -+ return; -+ } -+ -+ if ((priority & PRIORITY_SCHEDULED) != 0) { -+ throw new IllegalStateException("schedule() called twice"); -+ } -+ -+ if ((priority & PRIORITY_EXECUTED) != 0) { -+ // cancelled or executed -+ return; -+ } -+ -+ this.setPriorityScheduled(PrioritisedExecutor.Priority.getPriority(priority)); -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ public final PrioritisedExecutor.Priority getPriority() { -+ final int ret = this.getPriorityVolatile(); -+ if ((ret & PRIORITY_EXECUTED) != 0) { -+ return PrioritisedExecutor.Priority.COMPLETING; -+ } -+ if ((ret & PRIORITY_SCHEDULED) != 0) { -+ return this.getScheduledPriority(); -+ } -+ return PrioritisedExecutor.Priority.getPriority(ret); -+ } -+ -+ public final void lowerPriority(final PrioritisedExecutor.Priority priority) { -+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ -+ int failures = 0; -+ for (int curr = this.getPriorityVolatile();;) { -+ if ((curr & PRIORITY_EXECUTED) != 0) { -+ return; -+ } -+ -+ if ((curr & PRIORITY_SCHEDULED) != 0) { -+ this.lowerPriorityScheduled(priority); -+ return; -+ } -+ -+ if (!priority.isLowerPriority(curr)) { -+ return; -+ } -+ -+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { -+ return; -+ } -+ -+ // failed, retry -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ public final void setPriority(final PrioritisedExecutor.Priority priority) { -+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ -+ int failures = 0; -+ for (int curr = this.getPriorityVolatile();;) { -+ if ((curr & PRIORITY_EXECUTED) != 0) { -+ return; -+ } -+ -+ if ((curr & PRIORITY_SCHEDULED) != 0) { -+ this.setPriorityScheduled(priority); -+ return; -+ } -+ -+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { -+ return; -+ } -+ -+ // failed, retry -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ public final void raisePriority(final PrioritisedExecutor.Priority priority) { -+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ -+ int failures = 0; -+ for (int curr = this.getPriorityVolatile();;) { -+ if ((curr & PRIORITY_EXECUTED) != 0) { -+ return; -+ } -+ -+ if ((curr & PRIORITY_SCHEDULED) != 0) { -+ this.raisePriorityScheduled(priority); -+ return; -+ } -+ -+ if (!priority.isHigherPriority(curr)) { -+ return; -+ } -+ -+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { -+ return; -+ } -+ -+ // failed, retry -+ -+ ++failures; -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ } -+ } -+ -+ protected abstract void cancelScheduled(); -+ -+ protected abstract PrioritisedExecutor.Priority getScheduledPriority(); -+ -+ protected abstract void scheduleTask(final PrioritisedExecutor.Priority priority); -+ -+ protected abstract void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority); -+ -+ protected abstract void setPriorityScheduled(final PrioritisedExecutor.Priority priority); -+ -+ protected abstract void raisePriorityScheduled(final PrioritisedExecutor.Priority priority); -+} -diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ThreadedTicketLevelPropagator.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ThreadedTicketLevelPropagator.java -new file mode 100644 -index 0000000000000000000000000000000000000000..287240ed3b440f2f5733c368416e4276f626405d ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ThreadedTicketLevelPropagator.java -@@ -0,0 +1,1477 @@ -+package io.papermc.paper.chunk.system.scheduling; -+ -+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; -+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; -+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; -+import it.unimi.dsi.fastutil.HashCommon; -+import it.unimi.dsi.fastutil.longs.Long2ByteLinkedOpenHashMap; -+import it.unimi.dsi.fastutil.shorts.Short2ByteLinkedOpenHashMap; -+import it.unimi.dsi.fastutil.shorts.Short2ByteMap; -+import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; -+import java.lang.invoke.VarHandle; -+import java.util.ArrayDeque; -+import java.util.Arrays; -+import java.util.Iterator; -+import java.util.List; -+import java.util.concurrent.ConcurrentHashMap; -+import java.util.concurrent.locks.LockSupport; -+ -+public abstract class ThreadedTicketLevelPropagator { -+ -+ // sections are 64 in length -+ public static final int SECTION_SHIFT = 6; -+ public static final int SECTION_SIZE = 1 << SECTION_SHIFT; -+ private static final int LEVEL_BITS = SECTION_SHIFT; -+ private static final int LEVEL_COUNT = 1 << LEVEL_BITS; -+ private static final int MIN_SOURCE_LEVEL = 1; -+ // we limit the max source to 62 because the depropagation code _must_ attempt to depropagate -+ // a 1 level to 0; and if a source was 63 then it may cross more than 2 sections in depropagation -+ private static final int MAX_SOURCE_LEVEL = 62; -+ -+ private final UpdateQueue updateQueue; -+ private final ConcurrentHashMap sections = new ConcurrentHashMap<>(); -+ -+ public ThreadedTicketLevelPropagator() { -+ this.updateQueue = new UpdateQueue(); -+ } -+ -+ // must hold ticket lock for: -+ // (posX & ~(SECTION_SIZE - 1), posZ & ~(SECTION_SIZE - 1)) to (posX | (SECTION_SIZE - 1), posZ | (SECTION_SIZE - 1)) -+ public void setSource(final int posX, final int posZ, final int to) { -+ if (to < 1 || to > MAX_SOURCE_LEVEL) { -+ throw new IllegalArgumentException("Source: " + to); -+ } -+ -+ final int sectionX = posX >> SECTION_SHIFT; -+ final int sectionZ = posZ >> SECTION_SHIFT; -+ -+ final Coordinate coordinate = new Coordinate(sectionX, sectionZ); -+ Section section = this.sections.get(coordinate); -+ if (section == null) { -+ if (null != this.sections.putIfAbsent(coordinate, section = new Section(sectionX, sectionZ))) { -+ throw new IllegalStateException("Race condition while creating new section"); -+ } -+ } -+ -+ final int localIdx = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); -+ final short sLocalIdx = (short)localIdx; -+ -+ final short sourceAndLevel = section.levels[localIdx]; -+ final int currentSource = (sourceAndLevel >>> 8) & 0xFF; -+ -+ if (currentSource == to) { -+ // nothing to do -+ // make sure to kill the current update, if any -+ section.queuedSources.replace(sLocalIdx, (byte)to); -+ return; -+ } -+ -+ if (section.queuedSources.put(sLocalIdx, (byte)to) == Section.NO_QUEUED_UPDATE && section.queuedSources.size() == 1) { -+ this.queueSectionUpdate(section); -+ } -+ } -+ -+ // must hold ticket lock for: -+ // (posX & ~(SECTION_SIZE - 1), posZ & ~(SECTION_SIZE - 1)) to (posX | (SECTION_SIZE - 1), posZ | (SECTION_SIZE - 1)) -+ public void removeSource(final int posX, final int posZ) { -+ final int sectionX = posX >> SECTION_SHIFT; -+ final int sectionZ = posZ >> SECTION_SHIFT; -+ -+ final Coordinate coordinate = new Coordinate(sectionX, sectionZ); -+ final Section section = this.sections.get(coordinate); -+ -+ if (section == null) { -+ return; -+ } -+ -+ final int localIdx = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); -+ final short sLocalIdx = (short)localIdx; -+ -+ final int currentSource = (section.levels[localIdx] >>> 8) & 0xFF; -+ -+ if (currentSource == 0) { -+ // we use replace here so that we do not possibly multi-queue a section for an update -+ section.queuedSources.replace(sLocalIdx, (byte)0); -+ return; -+ } -+ -+ if (section.queuedSources.put(sLocalIdx, (byte)0) == Section.NO_QUEUED_UPDATE && section.queuedSources.size() == 1) { -+ this.queueSectionUpdate(section); -+ } -+ } -+ -+ private void queueSectionUpdate(final Section section) { -+ this.updateQueue.append(new UpdateQueue.UpdateQueueNode(section, null)); -+ } -+ -+ public boolean hasPendingUpdates() { -+ return !this.updateQueue.isEmpty(); -+ } -+ -+ // holds ticket lock for every chunk section represented by any position in the key set -+ // updates is modifiable and passed to processSchedulingUpdates after this call -+ protected abstract void processLevelUpdates(final Long2ByteLinkedOpenHashMap updates); -+ -+ // holds ticket lock for every chunk section represented by any position in the key set -+ // holds scheduling lock in max access radius for every position held by the ticket lock -+ // updates is cleared after this call -+ protected abstract void processSchedulingUpdates(final Long2ByteLinkedOpenHashMap updates, final List scheduledTasks, -+ final List changedFullStatus); -+ -+ // must hold ticket lock for every position in the sections in one radius around sectionX,sectionZ -+ public boolean performUpdate(final int sectionX, final int sectionZ, final ReentrantAreaLock schedulingLock, -+ final List scheduledTasks, final List changedFullStatus) { -+ if (!this.hasPendingUpdates()) { -+ return false; -+ } -+ -+ final Coordinate coordinate = new Coordinate(Coordinate.key(sectionX, sectionZ)); -+ final Section section = this.sections.get(coordinate); -+ -+ if (section == null || section.queuedSources.isEmpty()) { -+ // no section or no updates -+ return false; -+ } -+ -+ final Propagator propagator = Propagator.acquirePropagator(); -+ final boolean ret = this.performUpdate(section, null, propagator, -+ null, schedulingLock, scheduledTasks, changedFullStatus -+ ); -+ Propagator.returnPropagator(propagator); -+ return ret; -+ } -+ -+ private boolean performUpdate(final Section section, final UpdateQueue.UpdateQueueNode node, final Propagator propagator, -+ final ReentrantAreaLock ticketLock, final ReentrantAreaLock schedulingLock, -+ final List scheduledTasks, final List changedFullStatus) { -+ final int sectionX = section.sectionX; -+ final int sectionZ = section.sectionZ; -+ -+ final int rad1MinX = (sectionX - 1) << SECTION_SHIFT; -+ final int rad1MinZ = (sectionZ - 1) << SECTION_SHIFT; -+ final int rad1MaxX = ((sectionX + 1) << SECTION_SHIFT) | (SECTION_SIZE - 1); -+ final int rad1MaxZ = ((sectionZ + 1) << SECTION_SHIFT) | (SECTION_SIZE - 1); -+ -+ // set up encode offset first as we need to queue level changes _before_ -+ propagator.setupEncodeOffset(sectionX, sectionZ); -+ -+ final int coordinateOffset = propagator.coordinateOffset; -+ -+ final ReentrantAreaLock.Node ticketNode = ticketLock == null ? null : ticketLock.lock(rad1MinX, rad1MinZ, rad1MaxX, rad1MaxZ); -+ final boolean ret; -+ try { -+ // first, check if this update was stolen -+ if (section != this.sections.get(new Coordinate(sectionX, sectionZ))) { -+ // occurs when a stolen update deletes this section -+ // it is possible that another update is scheduled, but that one will have the correct section -+ if (node != null) { -+ this.updateQueue.remove(node); -+ } -+ return false; -+ } -+ -+ final int oldSourceSize = section.sources.size(); -+ -+ // process pending sources -+ for (final Iterator iterator = section.queuedSources.short2ByteEntrySet().fastIterator(); iterator.hasNext();) { -+ final Short2ByteMap.Entry entry = iterator.next(); -+ final int pos = (int)entry.getShortKey(); -+ final int posX = (pos & (SECTION_SIZE - 1)) | (sectionX << SECTION_SHIFT); -+ final int posZ = ((pos >> SECTION_SHIFT) & (SECTION_SIZE - 1)) | (sectionZ << SECTION_SHIFT); -+ final int newSource = (int)entry.getByteValue(); -+ -+ final short currentEncoded = section.levels[pos]; -+ final int currLevel = currentEncoded & 0xFF; -+ final int prevSource = (currentEncoded >>> 8) & 0xFF; -+ -+ if (prevSource == newSource) { -+ // nothing changed -+ continue; -+ } -+ -+ if ((prevSource < currLevel && newSource <= currLevel) || newSource == currLevel) { -+ // just update the source, don't need to propagate change -+ section.levels[pos] = (short)(currLevel | (newSource << 8)); -+ // level is unchanged, don't add to changed positions -+ } else { -+ // set current level and current source to new source -+ section.levels[pos] = (short)(newSource | (newSource << 8)); -+ // must add to updated positions in case this is final -+ propagator.updatedPositions.put(Coordinate.key(posX, posZ), (byte)newSource); -+ if (newSource != 0) { -+ // queue increase with new source level -+ propagator.appendToIncreaseQueue( -+ ((long)(posX + (posZ << Propagator.COORDINATE_BITS) + coordinateOffset) & ((1L << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) - 1)) | -+ ((newSource & (LEVEL_COUNT - 1L)) << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) | -+ (Propagator.ALL_DIRECTIONS_BITSET << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS + LEVEL_BITS)) -+ ); -+ } -+ // queue decrease with previous level -+ if (newSource < currLevel) { -+ propagator.appendToDecreaseQueue( -+ ((long)(posX + (posZ << Propagator.COORDINATE_BITS) + coordinateOffset) & ((1L << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) - 1)) | -+ ((currLevel & (LEVEL_COUNT - 1L)) << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS)) | -+ (Propagator.ALL_DIRECTIONS_BITSET << (Propagator.COORDINATE_BITS + Propagator.COORDINATE_BITS + LEVEL_BITS)) -+ ); -+ } -+ } -+ -+ if (newSource == 0) { -+ // prevSource != newSource, so we are removing this source -+ section.sources.remove((short)pos); -+ } else if (prevSource == 0) { -+ // prevSource != newSource, so we are adding this source -+ section.sources.add((short)pos); -+ } -+ } -+ -+ section.queuedSources.clear(); -+ -+ final int newSourceSize = section.sources.size(); -+ -+ if (oldSourceSize == 0 && newSourceSize != 0) { -+ // need to make sure the sections in 1 radius are initialised -+ for (int dz = -1; dz <= 1; ++dz) { -+ for (int dx = -1; dx <= 1; ++dx) { -+ if ((dx | dz) == 0) { -+ continue; -+ } -+ final int offX = dx + sectionX; -+ final int offZ = dz + sectionZ; -+ final Coordinate coordinate = new Coordinate(offX, offZ); -+ final Section neighbour = this.sections.computeIfAbsent(coordinate, (final Coordinate keyInMap) -> { -+ return new Section(Coordinate.x(keyInMap.key), Coordinate.z(keyInMap.key)); -+ }); -+ -+ // increase ref count -+ ++neighbour.oneRadNeighboursWithSources; -+ if (neighbour.oneRadNeighboursWithSources <= 0 || neighbour.oneRadNeighboursWithSources > 8) { -+ throw new IllegalStateException(Integer.toString(neighbour.oneRadNeighboursWithSources)); -+ } -+ } -+ } -+ } -+ -+ if (propagator.hasUpdates()) { -+ propagator.setupCaches(this, sectionX, sectionZ, 1); -+ propagator.performDecrease(); -+ // don't need try-finally, as any exception will cause the propagator to not be returned -+ propagator.destroyCaches(); -+ } -+ -+ if (newSourceSize == 0) { -+ final boolean decrementRef = oldSourceSize != 0; -+ // check for section de-init -+ for (int dz = -1; dz <= 1; ++dz) { -+ for (int dx = -1; dx <= 1; ++dx) { -+ final int offX = dx + sectionX; -+ final int offZ = dz + sectionZ; -+ final Coordinate coordinate = new Coordinate(offX, offZ); -+ final Section neighbour = this.sections.get(coordinate); -+ -+ if (neighbour == null) { -+ if (oldSourceSize == 0 && (dx | dz) != 0) { -+ // since we don't have sources, this section is allowed to null -+ continue; -+ } -+ throw new IllegalStateException("??"); -+ } -+ -+ if (decrementRef && (dx | dz) != 0) { -+ // decrease ref count, but only for neighbours -+ --neighbour.oneRadNeighboursWithSources; -+ } -+ -+ // we need to check the current section for de-init as well -+ if (neighbour.oneRadNeighboursWithSources == 0) { -+ if (neighbour.queuedSources.isEmpty() && neighbour.sources.isEmpty()) { -+ // need to de-init -+ this.sections.remove(coordinate); -+ } // else: neighbour is queued for an update, and it will de-init itself -+ } else if (neighbour.oneRadNeighboursWithSources < 0 || neighbour.oneRadNeighboursWithSources > 8) { -+ throw new IllegalStateException(Integer.toString(neighbour.oneRadNeighboursWithSources)); -+ } -+ } -+ } -+ } -+ -+ -+ ret = !propagator.updatedPositions.isEmpty(); -+ -+ if (ret) { -+ this.processLevelUpdates(propagator.updatedPositions); -+ -+ if (!propagator.updatedPositions.isEmpty()) { -+ // now we can actually update the ticket levels in the chunk holders -+ final int maxScheduleRadius = 2 * ChunkTaskScheduler.getMaxAccessRadius(); -+ -+ // allow the chunkholders to process ticket level updates without needing to acquire the schedule lock every time -+ final ReentrantAreaLock.Node schedulingNode = schedulingLock.lock( -+ rad1MinX - maxScheduleRadius, rad1MinZ - maxScheduleRadius, -+ rad1MaxX + maxScheduleRadius, rad1MaxZ + maxScheduleRadius -+ ); -+ try { -+ this.processSchedulingUpdates(propagator.updatedPositions, scheduledTasks, changedFullStatus); -+ } finally { -+ schedulingLock.unlock(schedulingNode); -+ } -+ } -+ -+ propagator.updatedPositions.clear(); -+ } -+ } finally { -+ if (ticketLock != null) { -+ ticketLock.unlock(ticketNode); -+ } -+ } -+ -+ // finished -+ if (node != null) { -+ this.updateQueue.remove(node); -+ } -+ -+ return ret; -+ } -+ -+ public boolean performUpdates(final ReentrantAreaLock ticketLock, final ReentrantAreaLock schedulingLock, -+ final List scheduledTasks, final List changedFullStatus) { -+ if (this.updateQueue.isEmpty()) { -+ return false; -+ } -+ -+ final long maxOrder = this.updateQueue.getLastOrder(); -+ -+ boolean updated = false; -+ Propagator propagator = null; -+ -+ for (;;) { -+ final UpdateQueue.UpdateQueueNode toUpdate = this.updateQueue.acquireNextToUpdate(maxOrder); -+ if (toUpdate == null) { -+ this.updateQueue.awaitFirst(maxOrder); -+ -+ if (!this.updateQueue.hasRemainingUpdates(maxOrder)) { -+ if (propagator != null) { -+ Propagator.returnPropagator(propagator); -+ } -+ return updated; -+ } -+ -+ continue; -+ } -+ -+ if (propagator == null) { -+ propagator = Propagator.acquirePropagator(); -+ } -+ -+ updated |= this.performUpdate(toUpdate.section, toUpdate, propagator, ticketLock, schedulingLock, scheduledTasks, changedFullStatus); -+ } -+ } -+ -+ private static final class UpdateQueue { -+ -+ private volatile UpdateQueueNode head; -+ private volatile UpdateQueueNode tail; -+ private volatile UpdateQueueNode lastUpdating; -+ -+ protected static final VarHandle HEAD_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "head", UpdateQueueNode.class); -+ protected static final VarHandle TAIL_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueue.class, "tail", UpdateQueueNode.class); -+ protected static final VarHandle LAST_UPDATING = ConcurrentUtil.getVarHandle(UpdateQueue.class, "lastUpdating", UpdateQueueNode.class); -+ -+ /* head */ -+ -+ protected final void setHeadPlain(final UpdateQueueNode newHead) { -+ HEAD_HANDLE.set(this, newHead); -+ } -+ -+ protected final void setHeadOpaque(final UpdateQueueNode newHead) { -+ HEAD_HANDLE.setOpaque(this, newHead); -+ } -+ -+ protected final UpdateQueueNode getHeadPlain() { -+ return (UpdateQueueNode)HEAD_HANDLE.get(this); -+ } -+ -+ protected final UpdateQueueNode getHeadOpaque() { -+ return (UpdateQueueNode)HEAD_HANDLE.getOpaque(this); -+ } -+ -+ protected final UpdateQueueNode getHeadAcquire() { -+ return (UpdateQueueNode)HEAD_HANDLE.getAcquire(this); -+ } -+ -+ /* tail */ -+ -+ protected final void setTailPlain(final UpdateQueueNode newTail) { -+ TAIL_HANDLE.set(this, newTail); -+ } -+ -+ protected final void setTailOpaque(final UpdateQueueNode newTail) { -+ TAIL_HANDLE.setOpaque(this, newTail); -+ } -+ -+ protected final UpdateQueueNode getTailPlain() { -+ return (UpdateQueueNode)TAIL_HANDLE.get(this); -+ } -+ -+ protected final UpdateQueueNode getTailOpaque() { -+ return (UpdateQueueNode)TAIL_HANDLE.getOpaque(this); -+ } -+ -+ /* lastUpdating */ -+ -+ protected final UpdateQueueNode getLastUpdatingVolatile() { -+ return (UpdateQueueNode)LAST_UPDATING.getVolatile(this); -+ } -+ -+ protected final UpdateQueueNode compareAndExchangeLastUpdatingVolatile(final UpdateQueueNode expect, final UpdateQueueNode update) { -+ return (UpdateQueueNode)LAST_UPDATING.compareAndExchange(this, expect, update); -+ } -+ -+ public UpdateQueue() { -+ final UpdateQueueNode dummy = new UpdateQueueNode(null, null); -+ dummy.order = -1L; -+ dummy.preventAdds(); -+ -+ this.setHeadPlain(dummy); -+ this.setTailPlain(dummy); -+ } -+ -+ public boolean isEmpty() { -+ return this.peek() == null; -+ } -+ -+ public boolean hasRemainingUpdates(final long maxUpdate) { -+ final UpdateQueueNode node = this.peek(); -+ return node != null && node.order <= maxUpdate; -+ } -+ -+ public long getLastOrder() { -+ for (UpdateQueueNode tail = this.getTailOpaque(), curr = tail;;) { -+ final UpdateQueueNode next = curr.getNextVolatile(); -+ if (next == null) { -+ // try to update stale tail -+ if (this.getTailOpaque() == tail && curr != tail) { -+ this.setTailOpaque(curr); -+ } -+ return curr.order; -+ } -+ curr = next; -+ } -+ } -+ -+ public UpdateQueueNode acquireNextToUpdate(final long maxOrder) { -+ int failures = 0; -+ for (UpdateQueueNode prev = this.getLastUpdatingVolatile();;) { -+ UpdateQueueNode next = prev == null ? this.peek() : prev.next; -+ -+ if (next == null || next.order > maxOrder) { -+ return null; -+ } -+ -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ -+ if (prev == (prev = this.compareAndExchangeLastUpdatingVolatile(prev, next))) { -+ return next; -+ } -+ -+ ++failures; -+ } -+ } -+ -+ public void awaitFirst(final long maxOrder) { -+ final UpdateQueueNode earliest = this.peek(); -+ if (earliest == null || earliest.order > maxOrder) { -+ return; -+ } -+ -+ final Thread currThread = Thread.currentThread(); -+ // we do not use add-blocking because we use the nullability of the section to block -+ // remove() does not begin to poll from the wait queue until the section is null'd, -+ // and so provided we check the nullability before parking there is no ordering of these operations -+ // such that remove() finishes polling from the wait queue while section is not null -+ earliest.add(currThread); -+ -+ // wait until completed -+ while (earliest.getSectionVolatile() != null) { -+ LockSupport.park(); -+ } -+ } -+ -+ public UpdateQueueNode peek() { -+ for (UpdateQueueNode head = this.getHeadOpaque(), curr = head;;) { -+ final UpdateQueueNode next = curr.getNextVolatile(); -+ final Section element = curr.getSectionVolatile(); /* Likely in sync */ -+ -+ if (element != null) { -+ if (this.getHeadOpaque() == head && curr != head) { -+ this.setHeadOpaque(curr); -+ } -+ return curr; -+ } -+ -+ if (next == null) { -+ if (this.getHeadOpaque() == head && curr != head) { -+ this.setHeadOpaque(curr); -+ } -+ return null; -+ } -+ curr = next; -+ } -+ } -+ -+ public void remove(final UpdateQueueNode node) { -+ // mark as removed -+ node.setSectionVolatile(null); -+ -+ // use peek to advance head -+ this.peek(); -+ -+ // unpark any waiters / block the wait queue -+ Thread unpark; -+ while ((unpark = node.poll()) != null) { -+ LockSupport.unpark(unpark); -+ } -+ } -+ -+ public void append(final UpdateQueueNode node) { -+ int failures = 0; -+ -+ for (UpdateQueueNode currTail = this.getTailOpaque(), curr = currTail;;) { -+ /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */ -+ /* It is likely due to a cache miss caused by another write to the next field */ -+ final UpdateQueueNode next = curr.getNextVolatile(); -+ -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ -+ if (next == null) { -+ node.order = curr.order + 1L; -+ final UpdateQueueNode compared = curr.compareExchangeNextVolatile(null, node); -+ -+ if (compared == null) { -+ /* Added */ -+ /* Avoid CASing on tail more than we need to */ -+ /* CAS to avoid setting an out-of-date tail */ -+ if (this.getTailOpaque() == currTail) { -+ this.setTailOpaque(node); -+ } -+ return; -+ } -+ -+ ++failures; -+ curr = compared; -+ continue; -+ } -+ -+ if (curr == currTail) { -+ /* Tail is likely not up-to-date */ -+ curr = next; -+ } else { -+ /* Try to update to tail */ -+ if (currTail == (currTail = this.getTailOpaque())) { -+ curr = next; -+ } else { -+ curr = currTail; -+ } -+ } -+ } -+ } -+ -+ // each node also represents a set of waiters, represented by the MTQ -+ // if the queue is add-blocked, then the update is complete -+ private static final class UpdateQueueNode extends MultiThreadedQueue { -+ private long order; -+ private Section section; -+ private volatile UpdateQueueNode next; -+ -+ protected static final VarHandle SECTION_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "section", Section.class); -+ protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(UpdateQueueNode.class, "next", UpdateQueueNode.class); -+ -+ public UpdateQueueNode(final Section section, final UpdateQueueNode next) { -+ SECTION_HANDLE.set(this, section); -+ NEXT_HANDLE.set(this, next); -+ } -+ -+ /* section */ -+ -+ protected final Section getSectionPlain() { -+ return (Section)SECTION_HANDLE.get(this); -+ } -+ -+ protected final Section getSectionVolatile() { -+ return (Section)SECTION_HANDLE.getVolatile(this); -+ } -+ -+ protected final void setSectionPlain(final Section update) { -+ SECTION_HANDLE.set(this, update); -+ } -+ -+ protected final void setSectionOpaque(final Section update) { -+ SECTION_HANDLE.setOpaque(this, update); -+ } -+ -+ protected final void setSectionVolatile(final Section update) { -+ SECTION_HANDLE.setVolatile(this, update); -+ } -+ -+ protected final Section getAndSetSectionVolatile(final Section update) { -+ return (Section)SECTION_HANDLE.getAndSet(this, update); -+ } -+ -+ protected final Section compareExchangeSectionVolatile(final Section expect, final Section update) { -+ return (Section)SECTION_HANDLE.compareAndExchange(this, expect, update); -+ } -+ -+ /* next */ -+ -+ protected final UpdateQueueNode getNextPlain() { -+ return (UpdateQueueNode)NEXT_HANDLE.get(this); -+ } -+ -+ protected final UpdateQueueNode getNextOpaque() { -+ return (UpdateQueueNode)NEXT_HANDLE.getOpaque(this); -+ } -+ -+ protected final UpdateQueueNode getNextAcquire() { -+ return (UpdateQueueNode)NEXT_HANDLE.getAcquire(this); -+ } -+ -+ protected final UpdateQueueNode getNextVolatile() { -+ return (UpdateQueueNode)NEXT_HANDLE.getVolatile(this); -+ } -+ -+ protected final void setNextPlain(final UpdateQueueNode next) { -+ NEXT_HANDLE.set(this, next); -+ } -+ -+ protected final void setNextVolatile(final UpdateQueueNode next) { -+ NEXT_HANDLE.setVolatile(this, next); -+ } -+ -+ protected final UpdateQueueNode compareExchangeNextVolatile(final UpdateQueueNode expect, final UpdateQueueNode set) { -+ return (UpdateQueueNode)NEXT_HANDLE.compareAndExchange(this, expect, set); -+ } -+ } -+ } -+ -+ private static final class Section { -+ -+ // upper 8 bits: sources, lower 8 bits: level -+ // if we REALLY wanted to get crazy, we could make the increase propagator use MethodHandles#byteArrayViewVarHandle -+ // to read and write the lower 8 bits of this array directly rather than reading, updating the bits, then writing back. -+ private final short[] levels = new short[SECTION_SIZE * SECTION_SIZE]; -+ // set of local positions that represent sources -+ private final ShortOpenHashSet sources = new ShortOpenHashSet(); -+ // map of local index to new source level -+ // the source level _cannot_ be updated in the backing storage immediately since the update -+ private static final byte NO_QUEUED_UPDATE = (byte)-1; -+ private final Short2ByteLinkedOpenHashMap queuedSources = new Short2ByteLinkedOpenHashMap(); -+ { -+ this.queuedSources.defaultReturnValue(NO_QUEUED_UPDATE); -+ } -+ private int oneRadNeighboursWithSources = 0; -+ -+ public final int sectionX; -+ public final int sectionZ; -+ -+ public Section(final int sectionX, final int sectionZ) { -+ this.sectionX = sectionX; -+ this.sectionZ = sectionZ; -+ } -+ -+ public boolean isZero() { -+ for (final short val : this.levels) { -+ if (val != 0) { -+ return false; -+ } -+ } -+ return true; -+ } -+ -+ @Override -+ public String toString() { -+ final StringBuilder ret = new StringBuilder(); -+ -+ for (int x = 0; x < SECTION_SIZE; ++x) { -+ ret.append("levels x=").append(x).append("\n"); -+ for (int z = 0; z < SECTION_SIZE; ++z) { -+ final short v = this.levels[x | (z << SECTION_SHIFT)]; -+ ret.append(v & 0xFF).append("."); -+ } -+ ret.append("\n"); -+ ret.append("sources x=").append(x).append("\n"); -+ for (int z = 0; z < SECTION_SIZE; ++z) { -+ final short v = this.levels[x | (z << SECTION_SHIFT)]; -+ ret.append((v >>> 8) & 0xFF).append("."); -+ } -+ ret.append("\n\n"); -+ } -+ -+ return ret.toString(); -+ } -+ } -+ -+ -+ private static final class Propagator { -+ -+ private static final ArrayDeque CACHED_PROPAGATORS = new ArrayDeque<>(); -+ private static final int MAX_PROPAGATORS = Runtime.getRuntime().availableProcessors() * 2; -+ -+ private static Propagator acquirePropagator() { -+ synchronized (CACHED_PROPAGATORS) { -+ final Propagator ret = CACHED_PROPAGATORS.pollFirst(); -+ if (ret != null) { -+ return ret; -+ } -+ } -+ return new Propagator(); -+ } -+ -+ private static void returnPropagator(final Propagator propagator) { -+ synchronized (CACHED_PROPAGATORS) { -+ if (CACHED_PROPAGATORS.size() < MAX_PROPAGATORS) { -+ CACHED_PROPAGATORS.add(propagator); -+ } -+ } -+ } -+ -+ private static final int SECTION_RADIUS = 2; -+ private static final int SECTION_CACHE_WIDTH = 2 * SECTION_RADIUS + 1; -+ // minimum number of bits to represent [0, SECTION_SIZE * SECTION_CACHE_WIDTH) -+ private static final int COORDINATE_BITS = 9; -+ private static final int COORDINATE_SIZE = 1 << COORDINATE_BITS; -+ static { -+ if ((SECTION_SIZE * SECTION_CACHE_WIDTH) > (1 << COORDINATE_BITS)) { -+ throw new IllegalStateException("Adjust COORDINATE_BITS"); -+ } -+ } -+ // index = x + (z * SECTION_CACHE_WIDTH) -+ // (this requires x >= 0 and z >= 0) -+ private final Section[] sections = new Section[SECTION_CACHE_WIDTH * SECTION_CACHE_WIDTH]; -+ -+ private int encodeOffsetX; -+ private int encodeOffsetZ; -+ -+ private int coordinateOffset; -+ -+ private int encodeSectionOffsetX; -+ private int encodeSectionOffsetZ; -+ -+ private int sectionIndexOffset; -+ -+ public final boolean hasUpdates() { -+ return this.decreaseQueueInitialLength != 0 || this.increaseQueueInitialLength != 0; -+ } -+ -+ protected final void setupEncodeOffset(final int centerSectionX, final int centerSectionZ) { -+ final int maxCoordinate = (SECTION_RADIUS * SECTION_SIZE - 1); -+ // must have that encoded >= 0 -+ // coordinates can range from [-maxCoordinate + centerSection*SECTION_SIZE, maxCoordinate + centerSection*SECTION_SIZE] -+ // we want a range of [0, maxCoordinate*2] -+ // so, 0 = -maxCoordinate + centerSection*SECTION_SIZE + offset -+ this.encodeOffsetX = maxCoordinate - (centerSectionX << SECTION_SHIFT); -+ this.encodeOffsetZ = maxCoordinate - (centerSectionZ << SECTION_SHIFT); -+ -+ // encoded coordinates range from [0, SECTION_SIZE * SECTION_CACHE_WIDTH) -+ // coordinate index = (x + encodeOffsetX) + ((z + encodeOffsetZ) << COORDINATE_BITS) -+ this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << COORDINATE_BITS); -+ -+ // need encoded values to be >= 0 -+ // so, 0 = (-SECTION_RADIUS + centerSectionX) + encodeOffset -+ this.encodeSectionOffsetX = SECTION_RADIUS - centerSectionX; -+ this.encodeSectionOffsetZ = SECTION_RADIUS - centerSectionZ; -+ -+ // section index = (secX + encodeSectionOffsetX) + ((secZ + encodeSectionOffsetZ) * SECTION_CACHE_WIDTH) -+ this.sectionIndexOffset = this.encodeSectionOffsetX + (this.encodeSectionOffsetZ * SECTION_CACHE_WIDTH); -+ } -+ -+ // must hold ticket lock for (centerSectionX,centerSectionZ) in radius rad -+ // must call setupEncodeOffset -+ protected final void setupCaches(final ThreadedTicketLevelPropagator propagator, -+ final int centerSectionX, final int centerSectionZ, -+ final int rad) { -+ for (int dz = -rad; dz <= rad; ++dz) { -+ for (int dx = -rad; dx <= rad; ++dx) { -+ final int sectionX = centerSectionX + dx; -+ final int sectionZ = centerSectionZ + dz; -+ final Coordinate coordinate = new Coordinate(sectionX, sectionZ); -+ final Section section = propagator.sections.get(coordinate); -+ -+ if (section == null) { -+ throw new IllegalStateException("Section at " + coordinate + " should not be null"); -+ } -+ -+ this.setSectionInCache(sectionX, sectionZ, section); -+ } -+ } -+ } -+ -+ protected final void setSectionInCache(final int sectionX, final int sectionZ, final Section section) { -+ this.sections[sectionX + SECTION_CACHE_WIDTH*sectionZ + this.sectionIndexOffset] = section; -+ } -+ -+ protected final Section getSection(final int sectionX, final int sectionZ) { -+ return this.sections[sectionX + SECTION_CACHE_WIDTH*sectionZ + this.sectionIndexOffset]; -+ } -+ -+ protected final int getLevel(final int posX, final int posZ) { -+ final Section section = this.sections[(posX >> SECTION_SHIFT) + SECTION_CACHE_WIDTH*(posZ >> SECTION_SHIFT) + this.sectionIndexOffset]; -+ if (section != null) { -+ return (int)section.levels[(posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT)] & 0xFF; -+ } -+ -+ return 0; -+ } -+ -+ protected final void setLevel(final int posX, final int posZ, final int to) { -+ final Section section = this.sections[(posX >> SECTION_SHIFT) + SECTION_CACHE_WIDTH*(posZ >> SECTION_SHIFT) + this.sectionIndexOffset]; -+ if (section != null) { -+ final int index = (posX & (SECTION_SIZE - 1)) | ((posZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); -+ final short level = section.levels[index]; -+ section.levels[index] = (short)((level & ~0xFF) | (to & 0xFF)); -+ this.updatedPositions.put(Coordinate.key(posX, posZ), (byte)to); -+ } -+ } -+ -+ protected final void destroyCaches() { -+ Arrays.fill(this.sections, null); -+ } -+ -+ // contains: -+ // lower (COORDINATE_BITS(9) + COORDINATE_BITS(9) = 18) bits encoded position: (x | (z << COORDINATE_BITS)) -+ // next LEVEL_BITS (6) bits: propagated level [0, 63] -+ // propagation directions bitset (16 bits): -+ protected static final long ALL_DIRECTIONS_BITSET = ( -+ // z = -1 -+ (1L << ((1 - 1) | ((1 - 1) << 2))) | -+ (1L << ((1 + 0) | ((1 - 1) << 2))) | -+ (1L << ((1 + 1) | ((1 - 1) << 2))) | -+ -+ // z = 0 -+ (1L << ((1 - 1) | ((1 + 0) << 2))) | -+ //(1L << ((1 + 0) | ((1 + 0) << 2))) | // exclude (0,0) -+ (1L << ((1 + 1) | ((1 + 0) << 2))) | -+ -+ // z = 1 -+ (1L << ((1 - 1) | ((1 + 1) << 2))) | -+ (1L << ((1 + 0) | ((1 + 1) << 2))) | -+ (1L << ((1 + 1) | ((1 + 1) << 2))) -+ ); -+ -+ private void ex(int bitset) { -+ for (int i = 0, len = Integer.bitCount(bitset); i < len; ++i) { -+ final int set = Integer.numberOfTrailingZeros(bitset); -+ final int tailingBit = (-bitset) & bitset; -+ // XOR to remove the trailing bit -+ bitset ^= tailingBit; -+ -+ // the encoded value set is (x_val) | (z_val << 2), totaling 4 bits -+ // thus, the bitset is 16 bits wide where each one represents a direction to propagate and the -+ // index of the set bit is the encoded value -+ // the encoded coordinate has 3 valid states: -+ // 0b00 (0) -> -1 -+ // 0b01 (1) -> 0 -+ // 0b10 (2) -> 1 -+ // the decode operation then is val - 1, and the encode operation is val + 1 -+ final int xOff = (set & 3) - 1; -+ final int zOff = ((set >>> 2) & 3) - 1; -+ System.out.println("Encoded: (" + xOff + "," + zOff + ")"); -+ } -+ } -+ -+ private void ch(long bs, int shift) { -+ int bitset = (int)(bs >>> shift); -+ for (int i = 0, len = Integer.bitCount(bitset); i < len; ++i) { -+ final int set = Integer.numberOfTrailingZeros(bitset); -+ final int tailingBit = (-bitset) & bitset; -+ // XOR to remove the trailing bit -+ bitset ^= tailingBit; -+ -+ // the encoded value set is (x_val) | (z_val << 2), totaling 4 bits -+ // thus, the bitset is 16 bits wide where each one represents a direction to propagate and the -+ // index of the set bit is the encoded value -+ // the encoded coordinate has 3 valid states: -+ // 0b00 (0) -> -1 -+ // 0b01 (1) -> 0 -+ // 0b10 (2) -> 1 -+ // the decode operation then is val - 1, and the encode operation is val + 1 -+ final int xOff = (set & 3) - 1; -+ final int zOff = ((set >>> 2) & 3) - 1; -+ if (Math.abs(xOff) > 1 || Math.abs(zOff) > 1 || (xOff | zOff) == 0) { -+ throw new IllegalStateException(); -+ } -+ } -+ } -+ -+ // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading -+ // updates for sources -+ protected static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 1; -+ // 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 >>> 0; -+ -+ protected long[] increaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2]; -+ protected int increaseQueueInitialLength; -+ protected long[] decreaseQueue = new long[SECTION_SIZE * SECTION_SIZE * 2]; -+ protected int decreaseQueueInitialLength; -+ -+ protected final Long2ByteLinkedOpenHashMap updatedPositions = new Long2ByteLinkedOpenHashMap(); -+ -+ 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; -+ return; -+ } else { -+ queue[idx] = value; -+ return; -+ } -+ } -+ -+ 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; -+ return; -+ } else { -+ queue[idx] = value; -+ return; -+ } -+ } -+ -+ protected final void performIncrease() { -+ long[] queue = this.increaseQueue; -+ int queueReadIndex = 0; -+ int queueLength = this.increaseQueueInitialLength; -+ this.increaseQueueInitialLength = 0; -+ final int decodeOffsetX = -this.encodeOffsetX; -+ final int decodeOffsetZ = -this.encodeOffsetZ; -+ final int encodeOffset = this.coordinateOffset; -+ final int sectionOffset = this.sectionIndexOffset; -+ -+ final Long2ByteLinkedOpenHashMap updatedPositions = this.updatedPositions; -+ -+ while (queueReadIndex < queueLength) { -+ final long queueValue = queue[queueReadIndex++]; -+ -+ final int posX = ((int)queueValue & (COORDINATE_SIZE - 1)) + decodeOffsetX; -+ final int posZ = (((int)queueValue >>> COORDINATE_BITS) & (COORDINATE_SIZE - 1)) + decodeOffsetZ; -+ final int propagatedLevel = ((int)queueValue >>> (COORDINATE_BITS + COORDINATE_BITS)) & (LEVEL_COUNT - 1); -+ // note: the above code requires coordinate bits * 2 < 32 -+ // bitset is 16 bits -+ int propagateDirectionBitset = (int)(queueValue >>> (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) & ((1 << 16) - 1); -+ -+ if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) { -+ if (this.getLevel(posX, posZ) != propagatedLevel) { -+ // not at the level we expect, so something changed. -+ continue; -+ } -+ } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) { -+ // these are used to restore sources after a propagation decrease -+ this.setLevel(posX, posZ, propagatedLevel); -+ } -+ -+ // this bitset represents the values that we have not propagated to -+ // this bitset lets us determine what directions the neighbours we set should propagate to, in most cases -+ // significantly reducing the total number of ops -+ // since we propagate in a 1 radius, we need a 2 radius bitset to hold all possible values we would possibly need -+ // but if we use only 5x5 bits, then we need to use div/mod to retrieve coordinates from the bitset, so instead -+ // we use an 8x8 bitset and luckily that can be fit into only one long value (64 bits) -+ // to make things easy, we use positions [0, 4] in the bitset, with current position being 2 -+ // index = x | (z << 3) -+ -+ // to start, we eliminate everything 1 radius from the current position as the previous propagator -+ // must guarantee that either we propagate everything in 1 radius or we partially propagate for 1 radius -+ // but the rest not propagated are already handled -+ long currentPropagation = ~( -+ // z = -1 -+ (1L << ((2 - 1) | ((2 - 1) << 3))) | -+ (1L << ((2 + 0) | ((2 - 1) << 3))) | -+ (1L << ((2 + 1) | ((2 - 1) << 3))) | -+ -+ // z = 0 -+ (1L << ((2 - 1) | ((2 + 0) << 3))) | -+ (1L << ((2 + 0) | ((2 + 0) << 3))) | -+ (1L << ((2 + 1) | ((2 + 0) << 3))) | -+ -+ // z = 1 -+ (1L << ((2 - 1) | ((2 + 1) << 3))) | -+ (1L << ((2 + 0) | ((2 + 1) << 3))) | -+ (1L << ((2 + 1) | ((2 + 1) << 3))) -+ ); -+ -+ final int toPropagate = propagatedLevel - 1; -+ -+ // we could use while (propagateDirectionBitset != 0), but it's not a predictable branch. By counting -+ // the bits, the cpu loop predictor should perfectly predict the loop. -+ for (int l = 0, len = Integer.bitCount(propagateDirectionBitset); l < len; ++l) { -+ final int set = Integer.numberOfTrailingZeros(propagateDirectionBitset); -+ final int tailingBit = (-propagateDirectionBitset) & propagateDirectionBitset; -+ propagateDirectionBitset ^= tailingBit; -+ -+ // pDecode is from [0, 2], and 1 must be subtracted to fully decode the offset -+ // it has been split to save some cycles via parallelism -+ final int pDecodeX = (set & 3); -+ final int pDecodeZ = ((set >>> 2) & 3); -+ -+ // re-ordered -1 on the position decode into pos - 1 to occur in parallel with determining pDecodeX -+ final int offX = (posX - 1) + pDecodeX; -+ final int offZ = (posZ - 1) + pDecodeZ; -+ -+ final int sectionIndex = (offX >> SECTION_SHIFT) + ((offZ >> SECTION_SHIFT) * SECTION_CACHE_WIDTH) + sectionOffset; -+ final int localIndex = (offX & (SECTION_SIZE - 1)) | ((offZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); -+ -+ // to retrieve a set of bits from a long value: (n_bitmask << (nstartidx)) & bitset -+ // bitset idx = x | (z << 3) -+ -+ // read three bits, so we need 7L -+ // note that generally: off - pos = (pos - 1) + pDecode - pos = pDecode - 1 -+ // nstartidx1 = x rel -1 for z rel -1 -+ // = (offX - posX - 1 + 2) | ((offZ - posZ - 1 + 2) << 3) -+ // = (pDecodeX - 1 - 1 + 2) | ((pDecodeZ - 1 - 1 + 2) << 3) -+ // = pDecodeX | (pDecodeZ << 3) = start -+ final int start = pDecodeX | (pDecodeZ << 3); -+ final long bitsetLine1 = currentPropagation & (7L << (start)); -+ -+ // nstartidx2 = x rel -1 for z rel 0 = line after line1, so we can just add 8 (row length of bitset) -+ final long bitsetLine2 = currentPropagation & (7L << (start + 8)); -+ -+ // nstartidx2 = x rel -1 for z rel 0 = line after line2, so we can just add 8 (row length of bitset) -+ final long bitsetLine3 = currentPropagation & (7L << (start + (8 + 8))); -+ -+ // remove ("take") lines from bitset -+ currentPropagation ^= (bitsetLine1 | bitsetLine2 | bitsetLine3); -+ -+ // now try to propagate -+ final Section section = this.sections[sectionIndex]; -+ -+ // lower 8 bits are current level, next upper 7 bits are source level, next 1 bit is updated source flag -+ final short currentStoredLevel = section.levels[localIndex]; -+ final int currentLevel = currentStoredLevel & 0xFF; -+ -+ if (currentLevel >= toPropagate) { -+ continue; // already at the level we want -+ } -+ -+ // update level -+ section.levels[localIndex] = (short)((currentStoredLevel & ~0xFF) | (toPropagate & 0xFF)); -+ updatedPositions.putAndMoveToLast(Coordinate.key(offX, offZ), (byte)toPropagate); -+ -+ // queue next -+ if (toPropagate > 1) { -+ // now combine into one bitset to pass to child -+ // the child bitset is 4x4, so we just shift each line by 4 -+ // add the propagation bitset offset to each line to make it easy to OR it into the propagation queue value -+ final long childPropagation = -+ ((bitsetLine1 >>> (start)) << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = -1 -+ ((bitsetLine2 >>> (start + 8)) << (4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = 0 -+ ((bitsetLine3 >>> (start + (8 + 8))) << (4 + 4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); // z = 1 -+ -+ // don't queue update if toPropagate cannot propagate anything to neighbours -+ // (for increase, propagating 0 to neighbours is useless) -+ if (queueLength >= queue.length) { -+ queue = this.resizeIncreaseQueue(); -+ } -+ queue[queueLength++] = -+ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | -+ ((toPropagate & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | -+ childPropagation; //(ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); -+ continue; -+ } -+ continue; -+ } -+ } -+ } -+ -+ protected final void performDecrease() { -+ 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 decodeOffsetZ = -this.encodeOffsetZ; -+ final int encodeOffset = this.coordinateOffset; -+ final int sectionOffset = this.sectionIndexOffset; -+ -+ final Long2ByteLinkedOpenHashMap updatedPositions = this.updatedPositions; -+ -+ while (queueReadIndex < queueLength) { -+ final long queueValue = queue[queueReadIndex++]; -+ -+ final int posX = ((int)queueValue & (COORDINATE_SIZE - 1)) + decodeOffsetX; -+ final int posZ = (((int)queueValue >>> COORDINATE_BITS) & (COORDINATE_SIZE - 1)) + decodeOffsetZ; -+ final int propagatedLevel = ((int)queueValue >>> (COORDINATE_BITS + COORDINATE_BITS)) & (LEVEL_COUNT - 1); -+ // note: the above code requires coordinate bits * 2 < 32 -+ // bitset is 16 bits -+ int propagateDirectionBitset = (int)(queueValue >>> (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) & ((1 << 16) - 1); -+ -+ // this bitset represents the values that we have not propagated to -+ // this bitset lets us determine what directions the neighbours we set should propagate to, in most cases -+ // significantly reducing the total number of ops -+ // since we propagate in a 1 radius, we need a 2 radius bitset to hold all possible values we would possibly need -+ // but if we use only 5x5 bits, then we need to use div/mod to retrieve coordinates from the bitset, so instead -+ // we use an 8x8 bitset and luckily that can be fit into only one long value (64 bits) -+ // to make things easy, we use positions [0, 4] in the bitset, with current position being 2 -+ // index = x | (z << 3) -+ -+ // to start, we eliminate everything 1 radius from the current position as the previous propagator -+ // must guarantee that either we propagate everything in 1 radius or we partially propagate for 1 radius -+ // but the rest not propagated are already handled -+ long currentPropagation = ~( -+ // z = -1 -+ (1L << ((2 - 1) | ((2 - 1) << 3))) | -+ (1L << ((2 + 0) | ((2 - 1) << 3))) | -+ (1L << ((2 + 1) | ((2 - 1) << 3))) | -+ -+ // z = 0 -+ (1L << ((2 - 1) | ((2 + 0) << 3))) | -+ (1L << ((2 + 0) | ((2 + 0) << 3))) | -+ (1L << ((2 + 1) | ((2 + 0) << 3))) | -+ -+ // z = 1 -+ (1L << ((2 - 1) | ((2 + 1) << 3))) | -+ (1L << ((2 + 0) | ((2 + 1) << 3))) | -+ (1L << ((2 + 1) | ((2 + 1) << 3))) -+ ); -+ -+ final int toPropagate = propagatedLevel - 1; -+ -+ // we could use while (propagateDirectionBitset != 0), but it's not a predictable branch. By counting -+ // the bits, the cpu loop predictor should perfectly predict the loop. -+ for (int l = 0, len = Integer.bitCount(propagateDirectionBitset); l < len; ++l) { -+ final int set = Integer.numberOfTrailingZeros(propagateDirectionBitset); -+ final int tailingBit = (-propagateDirectionBitset) & propagateDirectionBitset; -+ propagateDirectionBitset ^= tailingBit; -+ -+ -+ // pDecode is from [0, 2], and 1 must be subtracted to fully decode the offset -+ // it has been split to save some cycles via parallelism -+ final int pDecodeX = (set & 3); -+ final int pDecodeZ = ((set >>> 2) & 3); -+ -+ // re-ordered -1 on the position decode into pos - 1 to occur in parallel with determining pDecodeX -+ final int offX = (posX - 1) + pDecodeX; -+ final int offZ = (posZ - 1) + pDecodeZ; -+ -+ final int sectionIndex = (offX >> SECTION_SHIFT) + ((offZ >> SECTION_SHIFT) * SECTION_CACHE_WIDTH) + sectionOffset; -+ final int localIndex = (offX & (SECTION_SIZE - 1)) | ((offZ & (SECTION_SIZE - 1)) << SECTION_SHIFT); -+ -+ // to retrieve a set of bits from a long value: (n_bitmask << (nstartidx)) & bitset -+ // bitset idx = x | (z << 3) -+ -+ // read three bits, so we need 7L -+ // note that generally: off - pos = (pos - 1) + pDecode - pos = pDecode - 1 -+ // nstartidx1 = x rel -1 for z rel -1 -+ // = (offX - posX - 1 + 2) | ((offZ - posZ - 1 + 2) << 3) -+ // = (pDecodeX - 1 - 1 + 2) | ((pDecodeZ - 1 - 1 + 2) << 3) -+ // = pDecodeX | (pDecodeZ << 3) = start -+ final int start = pDecodeX | (pDecodeZ << 3); -+ final long bitsetLine1 = currentPropagation & (7L << (start)); -+ -+ // nstartidx2 = x rel -1 for z rel 0 = line after line1, so we can just add 8 (row length of bitset) -+ final long bitsetLine2 = currentPropagation & (7L << (start + 8)); -+ -+ // nstartidx2 = x rel -1 for z rel 0 = line after line2, so we can just add 8 (row length of bitset) -+ final long bitsetLine3 = currentPropagation & (7L << (start + (8 + 8))); -+ -+ // now try to propagate -+ final Section section = this.sections[sectionIndex]; -+ -+ // lower 8 bits are current level, next upper 7 bits are source level, next 1 bit is updated source flag -+ final short currentStoredLevel = section.levels[localIndex]; -+ final int currentLevel = currentStoredLevel & 0xFF; -+ final int sourceLevel = (currentStoredLevel >>> 8) & 0xFF; -+ -+ if (currentLevel == 0) { -+ continue; // already at the level we want -+ } -+ -+ if (currentLevel > toPropagate) { -+ // it looks like another source propagated here, so re-propagate it -+ if (increaseQueueLength >= increaseQueue.length) { -+ increaseQueue = this.resizeIncreaseQueue(); -+ } -+ increaseQueue[increaseQueueLength++] = -+ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | -+ ((currentLevel & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | -+ (FLAG_RECHECK_LEVEL | (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS))); -+ continue; -+ } -+ -+ // remove ("take") lines from bitset -+ // can't do this during decrease, TODO WHY? -+ //currentPropagation ^= (bitsetLine1 | bitsetLine2 | bitsetLine3); -+ -+ // update level -+ section.levels[localIndex] = (short)((currentStoredLevel & ~0xFF)); -+ updatedPositions.putAndMoveToLast(Coordinate.key(offX, offZ), (byte)0); -+ -+ if (sourceLevel != 0) { -+ // re-propagate source -+ // note: do not set recheck level, or else the propagation will fail -+ if (increaseQueueLength >= increaseQueue.length) { -+ increaseQueue = this.resizeIncreaseQueue(); -+ } -+ increaseQueue[increaseQueueLength++] = -+ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | -+ ((sourceLevel & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | -+ (FLAG_WRITE_LEVEL | (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS))); -+ } -+ -+ // queue next -+ // note: targetLevel > 0 here, since toPropagate >= currentLevel and currentLevel > 0 -+ // now combine into one bitset to pass to child -+ // the child bitset is 4x4, so we just shift each line by 4 -+ // add the propagation bitset offset to each line to make it easy to OR it into the propagation queue value -+ final long childPropagation = -+ ((bitsetLine1 >>> (start)) << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = -1 -+ ((bitsetLine2 >>> (start + 8)) << (4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)) | // z = 0 -+ ((bitsetLine3 >>> (start + (8 + 8))) << (4 + 4 + COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); // z = 1 -+ -+ // don't queue update if toPropagate cannot propagate anything to neighbours -+ // (for increase, propagating 0 to neighbours is useless) -+ if (queueLength >= queue.length) { -+ queue = this.resizeDecreaseQueue(); -+ } -+ queue[queueLength++] = -+ ((long)(offX + (offZ << COORDINATE_BITS) + encodeOffset) & ((1L << (COORDINATE_BITS + COORDINATE_BITS)) - 1)) | -+ ((toPropagate & (LEVEL_COUNT - 1L)) << (COORDINATE_BITS + COORDINATE_BITS)) | -+ (ALL_DIRECTIONS_BITSET << (COORDINATE_BITS + COORDINATE_BITS + LEVEL_BITS)); //childPropagation; -+ continue; -+ } -+ } -+ -+ // propagate sources we clobbered -+ this.increaseQueueInitialLength = increaseQueueLength; -+ this.performIncrease(); -+ } -+ } -+ -+ private static final class Coordinate implements Comparable { -+ -+ public final long key; -+ -+ public Coordinate(final long key) { -+ this.key = key; -+ } -+ -+ public Coordinate(final int x, final int z) { -+ this.key = key(x, z); -+ } -+ -+ public static long key(final int x, final int z) { -+ return ((long)z << 32) | (x & 0xFFFFFFFFL); -+ } -+ -+ public static int x(final long key) { -+ return (int)key; -+ } -+ -+ public static int z(final long key) { -+ return (int)(key >>> 32); -+ } -+ -+ @Override -+ public int hashCode() { -+ return (int)HashCommon.mix(this.key); -+ } -+ -+ @Override -+ public boolean equals(final Object obj) { -+ if (this == obj) { -+ return true; -+ } -+ -+ if (!(obj instanceof Coordinate other)) { -+ return false; -+ } -+ -+ return this.key == other.key; -+ } -+ -+ // This class is intended for HashMap/ConcurrentHashMap usage, which do treeify bin nodes if the chain -+ // is too large. So we should implement compareTo to help. -+ @Override -+ public int compareTo(final Coordinate other) { -+ return Long.compare(this.key, other.key); -+ } -+ -+ @Override -+ public String toString() { -+ return "[" + x(this.key) + "," + z(this.key) + "]"; -+ } -+ } -+ -+ /* -+ private static final java.util.Random random = new java.util.Random(4L); -+ private static final List> walkers = -+ new java.util.ArrayList<>(); -+ static final int PLAYERS = 0; -+ static final int RAD_BLOCKS = 10000; -+ static final int RAD = RAD_BLOCKS >> 4; -+ static final int RAD_BIG_BLOCKS = 100_000; -+ static final int RAD_BIG = RAD_BIG_BLOCKS >> 4; -+ static final int VD = 4; -+ static final int BIG_PLAYERS = 50; -+ static final double WALK_CHANCE = 0.10; -+ static final double TP_CHANCE = 0.01; -+ static final int TP_BACK_PLAYERS = 200; -+ static final double TP_BACK_CHANCE = 0.25; -+ static final double TP_STEAL_CHANCE = 0.25; -+ private static final List> tpBack = -+ new java.util.ArrayList<>(); -+ -+ public static void main(final String[] args) { -+ final ReentrantAreaLock ticketLock = new ReentrantAreaLock(SECTION_SHIFT); -+ final ReentrantAreaLock schedulingLock = new ReentrantAreaLock(SECTION_SHIFT); -+ final Long2ByteLinkedOpenHashMap levelMap = new Long2ByteLinkedOpenHashMap(); -+ final Long2ByteLinkedOpenHashMap refMap = new Long2ByteLinkedOpenHashMap(); -+ final io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D ref = new io.papermc.paper.util.misc.Delayed8WayDistancePropagator2D((final long coordinate, final byte oldLevel, final byte newLevel) -> { -+ if (newLevel == 0) { -+ refMap.remove(coordinate); -+ } else { -+ refMap.put(coordinate, newLevel); -+ } -+ }); -+ final ThreadedTicketLevelPropagator propagator = new ThreadedTicketLevelPropagator() { -+ @Override -+ protected void processLevelUpdates(Long2ByteLinkedOpenHashMap updates) { -+ for (final long key : updates.keySet()) { -+ final byte val = updates.get(key); -+ if (val == 0) { -+ levelMap.remove(key); -+ } else { -+ levelMap.put(key, val); -+ } -+ } -+ } -+ -+ @Override -+ protected void processSchedulingUpdates(Long2ByteLinkedOpenHashMap updates, List scheduledTasks, List changedFullStatus) {} -+ }; -+ -+ for (;;) { -+ if (walkers.isEmpty() && tpBack.isEmpty()) { -+ for (int i = 0; i < PLAYERS; ++i) { -+ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD; -+ int posX = random.nextInt(-rad, rad + 1); -+ int posZ = random.nextInt(-rad, rad + 1); -+ -+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) { -+ @Override -+ protected void addCallback(Void parameter, int chunkX, int chunkZ) { -+ int src = 45 - 31 + 1; -+ ref.setSource(chunkX, chunkZ, src); -+ propagator.setSource(chunkX, chunkZ, src); -+ } -+ -+ @Override -+ protected void removeCallback(Void parameter, int chunkX, int chunkZ) { -+ ref.removeSource(chunkX, chunkZ); -+ propagator.removeSource(chunkX, chunkZ); -+ } -+ }; -+ -+ map.add(posX, posZ, VD); -+ -+ walkers.add(map); -+ } -+ for (int i = 0; i < TP_BACK_PLAYERS; ++i) { -+ int rad = RAD_BIG; -+ int posX = random.nextInt(-rad, rad + 1); -+ int posZ = random.nextInt(-rad, rad + 1); -+ -+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) { -+ @Override -+ protected void addCallback(Void parameter, int chunkX, int chunkZ) { -+ int src = 45 - 31 + 1; -+ ref.setSource(chunkX, chunkZ, src); -+ propagator.setSource(chunkX, chunkZ, src); -+ } -+ -+ @Override -+ protected void removeCallback(Void parameter, int chunkX, int chunkZ) { -+ ref.removeSource(chunkX, chunkZ); -+ propagator.removeSource(chunkX, chunkZ); -+ } -+ }; -+ -+ map.add(posX, posZ, random.nextInt(1, 63)); -+ -+ tpBack.add(map); -+ } -+ } else { -+ for (int i = 0; i < PLAYERS; ++i) { -+ if (random.nextDouble() > WALK_CHANCE) { -+ continue; -+ } -+ -+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = walkers.get(i); -+ -+ int updateX = random.nextInt(-1, 2); -+ int updateZ = random.nextInt(-1, 2); -+ -+ map.update(map.lastChunkX + updateX, map.lastChunkZ + updateZ, VD); -+ } -+ -+ for (int i = 0; i < PLAYERS; ++i) { -+ if (random.nextDouble() > TP_CHANCE) { -+ continue; -+ } -+ -+ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD; -+ int posX = random.nextInt(-rad, rad + 1); -+ int posZ = random.nextInt(-rad, rad + 1); -+ -+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = walkers.get(i); -+ -+ map.update(posX, posZ, VD); -+ } -+ -+ for (int i = 0; i < TP_BACK_PLAYERS; ++i) { -+ if (random.nextDouble() > TP_BACK_CHANCE) { -+ continue; -+ } -+ -+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = tpBack.get(i); -+ -+ map.update(-map.lastChunkX, -map.lastChunkZ, random.nextInt(1, 63)); -+ -+ if (random.nextDouble() > TP_STEAL_CHANCE) { -+ propagator.performUpdate( -+ map.lastChunkX >> SECTION_SHIFT, map.lastChunkZ >> SECTION_SHIFT, schedulingLock, null, null -+ ); -+ propagator.performUpdate( -+ (-map.lastChunkX >> SECTION_SHIFT), (-map.lastChunkZ >> SECTION_SHIFT), schedulingLock, null, null -+ ); -+ } -+ } -+ } -+ -+ ref.propagateUpdates(); -+ propagator.performUpdates(ticketLock, schedulingLock, null, null); -+ -+ if (!refMap.equals(levelMap)) { -+ throw new IllegalStateException("Error!"); -+ } -+ } -+ } -+ */ -+} -diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/queue/RadiusAwarePrioritisedExecutor.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/queue/RadiusAwarePrioritisedExecutor.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f7b0e2564ac4bd2db1d2b2bdc230c9f52f8a21b7 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/queue/RadiusAwarePrioritisedExecutor.java -@@ -0,0 +1,667 @@ -+package io.papermc.paper.chunk.system.scheduling.queue; -+ -+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; -+import io.papermc.paper.util.CoordinateUtils; -+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; -+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; -+import java.util.ArrayList; -+import java.util.Comparator; -+import java.util.List; -+import java.util.PriorityQueue; -+ -+public class RadiusAwarePrioritisedExecutor { -+ -+ private static final Comparator DEPENDENCY_NODE_COMPARATOR = (final DependencyNode t1, final DependencyNode t2) -> { -+ return Long.compare(t1.id, t2.id); -+ }; -+ -+ private final DependencyTree[] queues = new DependencyTree[PrioritisedExecutor.Priority.TOTAL_SCHEDULABLE_PRIORITIES]; -+ private static final int NO_TASKS_QUEUED = -1; -+ private int selectedQueue = NO_TASKS_QUEUED; -+ private boolean canQueueTasks = true; -+ -+ public RadiusAwarePrioritisedExecutor(final PrioritisedExecutor executor, final int maxToSchedule) { -+ for (int i = 0; i < this.queues.length; ++i) { -+ this.queues[i] = new DependencyTree(this, executor, maxToSchedule, i); -+ } -+ } -+ -+ private boolean canQueueTasks() { -+ return this.canQueueTasks; -+ } -+ -+ private List treeFinished() { -+ this.canQueueTasks = true; -+ for (int priority = 0; priority < this.queues.length; ++priority) { -+ final DependencyTree queue = this.queues[priority]; -+ if (queue.hasWaitingTasks()) { -+ final List ret = queue.tryPushTasks(); -+ -+ if (ret == null || ret.isEmpty()) { -+ // this happens when the tasks in the wait queue were purged -+ // in this case, the queue was actually empty, we just had to purge it -+ // if we set the selected queue without scheduling any tasks, the queue will never be unselected -+ // as that requires a scheduled task completing... -+ continue; -+ } -+ -+ this.selectedQueue = priority; -+ return ret; -+ } -+ } -+ -+ this.selectedQueue = NO_TASKS_QUEUED; -+ -+ return null; -+ } -+ -+ private List queue(final Task task, final PrioritisedExecutor.Priority priority) { -+ final int priorityId = priority.priority; -+ final DependencyTree queue = this.queues[priorityId]; -+ -+ final DependencyNode node = new DependencyNode(task, queue); -+ -+ if (task.dependencyNode != null) { -+ throw new IllegalStateException(); -+ } -+ task.dependencyNode = node; -+ -+ queue.pushNode(node); -+ -+ if (this.selectedQueue == NO_TASKS_QUEUED) { -+ this.canQueueTasks = true; -+ this.selectedQueue = priorityId; -+ return queue.tryPushTasks(); -+ } -+ -+ if (!this.canQueueTasks) { -+ return null; -+ } -+ -+ if (PrioritisedExecutor.Priority.isHigherPriority(priorityId, this.selectedQueue)) { -+ // prevent the lower priority tree from queueing more tasks -+ this.canQueueTasks = false; -+ return null; -+ } -+ -+ // priorityId != selectedQueue: lower priority, don't care - treeFinished will pick it up -+ return priorityId == this.selectedQueue ? queue.tryPushTasks() : null; -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius, -+ final Runnable run, final PrioritisedExecutor.Priority priority) { -+ if (radius < 0) { -+ throw new IllegalArgumentException("Radius must be > 0: " + radius); -+ } -+ return new Task(this, chunkX, chunkZ, radius, run, priority); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius, -+ final Runnable run) { -+ return this.createTask(chunkX, chunkZ, radius, run, PrioritisedExecutor.Priority.NORMAL); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask queueTask(final int chunkX, final int chunkZ, final int radius, -+ final Runnable run, final PrioritisedExecutor.Priority priority) { -+ final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run, priority); -+ -+ ret.queue(); -+ -+ return ret; -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask queueTask(final int chunkX, final int chunkZ, final int radius, -+ final Runnable run) { -+ final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run); -+ -+ ret.queue(); -+ -+ return ret; -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run, final PrioritisedExecutor.Priority priority) { -+ return new Task(this, 0, 0, -1, run, priority); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run) { -+ return this.createInfiniteRadiusTask(run, PrioritisedExecutor.Priority.NORMAL); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run, final PrioritisedExecutor.Priority priority) { -+ final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, priority); -+ -+ ret.queue(); -+ -+ return ret; -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run) { -+ final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, PrioritisedExecutor.Priority.NORMAL); -+ -+ ret.queue(); -+ -+ return ret; -+ } -+ -+ // all accesses must be synchronised by the radius aware object -+ private static final class DependencyTree { -+ -+ private final RadiusAwarePrioritisedExecutor scheduler; -+ private final PrioritisedExecutor executor; -+ private final int maxToSchedule; -+ private final int treeIndex; -+ -+ private int currentlyExecuting; -+ private long idGenerator; -+ -+ private final PriorityQueue awaiting = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR); -+ -+ private final PriorityQueue infiniteRadius = new PriorityQueue<>(DEPENDENCY_NODE_COMPARATOR); -+ private boolean isInfiniteRadiusScheduled; -+ -+ private final Long2ReferenceOpenHashMap nodeByPosition = new Long2ReferenceOpenHashMap<>(); -+ -+ public DependencyTree(final RadiusAwarePrioritisedExecutor scheduler, final PrioritisedExecutor executor, -+ final int maxToSchedule, final int treeIndex) { -+ this.scheduler = scheduler; -+ this.executor = executor; -+ this.maxToSchedule = maxToSchedule; -+ this.treeIndex = treeIndex; -+ } -+ -+ public boolean hasWaitingTasks() { -+ return !this.awaiting.isEmpty() || !this.infiniteRadius.isEmpty(); -+ } -+ -+ private long nextId() { -+ return this.idGenerator++; -+ } -+ -+ private boolean isExecutingAnyTasks() { -+ return this.currentlyExecuting != 0; -+ } -+ -+ private void pushNode(final DependencyNode node) { -+ if (!node.task.isFiniteRadius()) { -+ this.infiniteRadius.add(node); -+ return; -+ } -+ -+ // set up dependency for node -+ final Task task = node.task; -+ -+ final int centerX = task.chunkX; -+ final int centerZ = task.chunkZ; -+ final int radius = task.radius; -+ -+ final int minX = centerX - radius; -+ final int maxX = centerX + radius; -+ -+ final int minZ = centerZ - radius; -+ final int maxZ = centerZ + radius; -+ -+ ReferenceOpenHashSet parents = null; -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ final DependencyNode dependency = this.nodeByPosition.put(CoordinateUtils.getChunkKey(currX, currZ), node); -+ if (dependency != null) { -+ if (parents == null) { -+ parents = new ReferenceOpenHashSet<>(); -+ } -+ if (parents.add(dependency)) { -+ // added a dependency, so we need to add as a child to the dependency -+ if (dependency.children == null) { -+ dependency.children = new ArrayList<>(); -+ } -+ dependency.children.add(node); -+ } -+ } -+ } -+ } -+ -+ if (parents == null) { -+ // no dependencies, add straight to awaiting -+ this.awaiting.add(node); -+ } else { -+ node.parents = parents.size(); -+ // we will be added to awaiting once we have no parents -+ } -+ } -+ -+ // called only when a node is returned after being executed -+ private List returnNode(final DependencyNode node) { -+ final Task task = node.task; -+ -+ // now that the task is completed, we can push its children to the awaiting queue -+ this.pushChildren(node); -+ -+ if (task.isFiniteRadius()) { -+ // remove from dependency map -+ this.removeNodeFromMap(node); -+ } else { -+ // mark as no longer executing infinite radius -+ if (!this.isInfiniteRadiusScheduled) { -+ throw new IllegalStateException(); -+ } -+ this.isInfiniteRadiusScheduled = false; -+ } -+ -+ // decrement executing count, we are done executing this task -+ --this.currentlyExecuting; -+ -+ if (this.currentlyExecuting == 0) { -+ return this.scheduler.treeFinished(); -+ } -+ -+ return this.scheduler.canQueueTasks() ? this.tryPushTasks() : null; -+ } -+ -+ private List tryPushTasks() { -+ // tasks are not queued, but only created here - we do hold the lock for the map -+ List ret = null; -+ PrioritisedExecutor.PrioritisedTask pushedTask; -+ while ((pushedTask = this.tryPushTask()) != null) { -+ if (ret == null) { -+ ret = new ArrayList<>(); -+ } -+ ret.add(pushedTask); -+ } -+ -+ return ret; -+ } -+ -+ private void removeNodeFromMap(final DependencyNode node) { -+ final Task task = node.task; -+ -+ final int centerX = task.chunkX; -+ final int centerZ = task.chunkZ; -+ final int radius = task.radius; -+ -+ final int minX = centerX - radius; -+ final int maxX = centerX + radius; -+ -+ final int minZ = centerZ - radius; -+ final int maxZ = centerZ + radius; -+ -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ for (int currX = minX; currX <= maxX; ++currX) { -+ this.nodeByPosition.remove(CoordinateUtils.getChunkKey(currX, currZ), node); -+ } -+ } -+ } -+ -+ private void pushChildren(final DependencyNode node) { -+ // add all the children that we can into awaiting -+ final List children = node.children; -+ if (children != null) { -+ for (int i = 0, len = children.size(); i < len; ++i) { -+ final DependencyNode child = children.get(i); -+ int newParents = --child.parents; -+ if (newParents == 0) { -+ // no more dependents, we can push to awaiting -+ // even if the child is purged, we need to push it so that its children will be pushed -+ this.awaiting.add(child); -+ } else if (newParents < 0) { -+ throw new IllegalStateException(); -+ } -+ } -+ } -+ } -+ -+ private DependencyNode pollAwaiting() { -+ final DependencyNode ret = this.awaiting.poll(); -+ if (ret == null) { -+ return ret; -+ } -+ -+ if (ret.parents != 0) { -+ throw new IllegalStateException(); -+ } -+ -+ if (ret.purged) { -+ // need to manually remove from state here -+ this.pushChildren(ret); -+ this.removeNodeFromMap(ret); -+ } // else: delay children push until the task has finished -+ -+ return ret; -+ } -+ -+ private DependencyNode pollInfinite() { -+ return this.infiniteRadius.poll(); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask tryPushTask() { -+ if (this.currentlyExecuting >= this.maxToSchedule || this.isInfiniteRadiusScheduled) { -+ return null; -+ } -+ -+ DependencyNode firstInfinite; -+ while ((firstInfinite = this.infiniteRadius.peek()) != null && firstInfinite.purged) { -+ this.pollInfinite(); -+ } -+ -+ DependencyNode firstAwaiting; -+ while ((firstAwaiting = this.awaiting.peek()) != null && firstAwaiting.purged) { -+ this.pollAwaiting(); -+ } -+ -+ if (firstInfinite == null && firstAwaiting == null) { -+ return null; -+ } -+ -+ // firstAwaiting compared to firstInfinite -+ final int compare; -+ -+ if (firstAwaiting == null) { -+ // we choose first infinite, or infinite < awaiting -+ compare = 1; -+ } else if (firstInfinite == null) { -+ // we choose first awaiting, or awaiting < infinite -+ compare = -1; -+ } else { -+ compare = DEPENDENCY_NODE_COMPARATOR.compare(firstAwaiting, firstInfinite); -+ } -+ -+ if (compare >= 0) { -+ if (this.currentlyExecuting != 0) { -+ // don't queue infinite task while other tasks are executing in parallel -+ return null; -+ } -+ ++this.currentlyExecuting; -+ this.pollInfinite(); -+ this.isInfiniteRadiusScheduled = true; -+ return firstInfinite.task.pushTask(this.executor); -+ } else { -+ ++this.currentlyExecuting; -+ this.pollAwaiting(); -+ return firstAwaiting.task.pushTask(this.executor); -+ } -+ } -+ } -+ -+ private static final class DependencyNode { -+ -+ private final Task task; -+ private final DependencyTree tree; -+ -+ // dependency tree fields -+ // (must hold lock on the scheduler to use) -+ // null is the same as empty, we just use it so that we don't allocate the set unless we need to -+ private List children; -+ // 0 indicates that this task is considered "awaiting" -+ private int parents; -+ // false -> scheduled and not cancelled -+ // true -> scheduled but cancelled -+ private boolean purged; -+ private final long id; -+ -+ public DependencyNode(final Task task, final DependencyTree tree) { -+ this.task = task; -+ this.id = tree.nextId(); -+ this.tree = tree; -+ } -+ } -+ -+ private static final class Task implements PrioritisedExecutor.PrioritisedTask, Runnable { -+ -+ // task specific fields -+ private final RadiusAwarePrioritisedExecutor scheduler; -+ private final int chunkX; -+ private final int chunkZ; -+ private final int radius; -+ private Runnable run; -+ private PrioritisedExecutor.Priority priority; -+ -+ private DependencyNode dependencyNode; -+ private PrioritisedExecutor.PrioritisedTask queuedTask; -+ -+ private Task(final RadiusAwarePrioritisedExecutor scheduler, final int chunkX, final int chunkZ, final int radius, -+ final Runnable run, final PrioritisedExecutor.Priority priority) { -+ this.scheduler = scheduler; -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ this.radius = radius; -+ this.run = run; -+ this.priority = priority; -+ } -+ -+ private boolean isFiniteRadius() { -+ return this.radius >= 0; -+ } -+ -+ private PrioritisedExecutor.PrioritisedTask pushTask(final PrioritisedExecutor executor) { -+ return this.queuedTask = executor.createTask(this, this.priority); -+ } -+ -+ private void executeTask() { -+ final Runnable run = this.run; -+ this.run = null; -+ run.run(); -+ } -+ -+ private static void scheduleTasks(final List toSchedule) { -+ if (toSchedule != null) { -+ for (int i = 0, len = toSchedule.size(); i < len; ++i) { -+ toSchedule.get(i).queue(); -+ } -+ } -+ } -+ -+ private void returnNode() { -+ final List toSchedule; -+ synchronized (this.scheduler) { -+ final DependencyNode node = this.dependencyNode; -+ this.dependencyNode = null; -+ toSchedule = node.tree.returnNode(node); -+ } -+ -+ scheduleTasks(toSchedule); -+ } -+ -+ @Override -+ public void run() { -+ final Runnable run = this.run; -+ this.run = null; -+ try { -+ run.run(); -+ } finally { -+ this.returnNode(); -+ } -+ } -+ -+ @Override -+ public boolean queue() { -+ final List toSchedule; -+ synchronized (this.scheduler) { -+ if (this.queuedTask != null || this.dependencyNode != null || this.priority == PrioritisedExecutor.Priority.COMPLETING) { -+ return false; -+ } -+ -+ toSchedule = this.scheduler.queue(this, this.priority); -+ } -+ -+ scheduleTasks(toSchedule); -+ return true; -+ } -+ -+ @Override -+ public boolean cancel() { -+ final PrioritisedExecutor.PrioritisedTask task; -+ synchronized (this.scheduler) { -+ if ((task = this.queuedTask) == null) { -+ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { -+ return false; -+ } -+ -+ this.priority = PrioritisedExecutor.Priority.COMPLETING; -+ if (this.dependencyNode != null) { -+ this.dependencyNode.purged = true; -+ this.dependencyNode = null; -+ } -+ -+ return true; -+ } -+ } -+ -+ if (task.cancel()) { -+ // must manually return the node -+ this.run = null; -+ this.returnNode(); -+ return true; -+ } -+ return false; -+ } -+ -+ @Override -+ public boolean execute() { -+ final PrioritisedExecutor.PrioritisedTask task; -+ synchronized (this.scheduler) { -+ if ((task = this.queuedTask) == null) { -+ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { -+ return false; -+ } -+ -+ this.priority = PrioritisedExecutor.Priority.COMPLETING; -+ if (this.dependencyNode != null) { -+ this.dependencyNode.purged = true; -+ this.dependencyNode = null; -+ } -+ // fall through to execution logic -+ } -+ } -+ -+ if (task != null) { -+ // will run the return node logic automatically -+ return task.execute(); -+ } else { -+ // don't run node removal/insertion logic, we aren't actually removed from the dependency tree -+ this.executeTask(); -+ return true; -+ } -+ } -+ -+ @Override -+ public PrioritisedExecutor.Priority getPriority() { -+ final PrioritisedExecutor.PrioritisedTask task; -+ synchronized (this.scheduler) { -+ if ((task = this.queuedTask) == null) { -+ return this.priority; -+ } -+ } -+ -+ return task.getPriority(); -+ } -+ -+ @Override -+ public boolean setPriority(final PrioritisedExecutor.Priority priority) { -+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ -+ final PrioritisedExecutor.PrioritisedTask task; -+ List toSchedule = null; -+ synchronized (this.scheduler) { -+ if ((task = this.queuedTask) == null) { -+ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { -+ return false; -+ } -+ -+ if (this.priority == priority) { -+ return true; -+ } -+ -+ this.priority = priority; -+ if (this.dependencyNode != null) { -+ // need to re-insert node -+ this.dependencyNode.purged = true; -+ this.dependencyNode = null; -+ toSchedule = this.scheduler.queue(this, priority); -+ } -+ } -+ } -+ -+ if (task != null) { -+ return task.setPriority(priority); -+ } -+ -+ scheduleTasks(toSchedule); -+ -+ return true; -+ } -+ -+ @Override -+ public boolean raisePriority(final PrioritisedExecutor.Priority priority) { -+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ -+ final PrioritisedExecutor.PrioritisedTask task; -+ List toSchedule = null; -+ synchronized (this.scheduler) { -+ if ((task = this.queuedTask) == null) { -+ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { -+ return false; -+ } -+ -+ if (this.priority.isHigherOrEqualPriority(priority)) { -+ return true; -+ } -+ -+ this.priority = priority; -+ if (this.dependencyNode != null) { -+ // need to re-insert node -+ this.dependencyNode.purged = true; -+ this.dependencyNode = null; -+ toSchedule = this.scheduler.queue(this, priority); -+ } -+ } -+ } -+ -+ if (task != null) { -+ return task.raisePriority(priority); -+ } -+ -+ scheduleTasks(toSchedule); -+ -+ return true; -+ } -+ -+ @Override -+ public boolean lowerPriority(final PrioritisedExecutor.Priority priority) { -+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { -+ throw new IllegalArgumentException("Invalid priority " + priority); -+ } -+ -+ final PrioritisedExecutor.PrioritisedTask task; -+ List toSchedule = null; -+ synchronized (this.scheduler) { -+ if ((task = this.queuedTask) == null) { -+ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) { -+ return false; -+ } -+ -+ if (this.priority.isLowerOrEqualPriority(priority)) { -+ return true; -+ } -+ -+ this.priority = priority; -+ if (this.dependencyNode != null) { -+ // need to re-insert node -+ this.dependencyNode.purged = true; -+ this.dependencyNode = null; -+ toSchedule = this.scheduler.queue(this, priority); -+ } -+ } -+ } -+ -+ if (task != null) { -+ return task.lowerPriority(priority); -+ } -+ -+ scheduleTasks(toSchedule); -+ -+ return true; -+ } -+ } -+} -diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java -index e47fb2aa5e885162cae5cbfc9f33ff7864bf538e..b68b37274f22c2a89d723aec4d1c6be813eef73c 100644 ---- a/src/main/java/io/papermc/paper/command/PaperCommand.java -+++ b/src/main/java/io/papermc/paper/command/PaperCommand.java -@@ -43,6 +43,7 @@ public final class PaperCommand extends Command { - commands.put(Set.of("mobcaps", "playermobcaps"), new MobcapsCommand()); - commands.put(Set.of("dumplisteners"), new DumpListenersCommand()); - commands.put(Set.of("fixlight"), new FixLightCommand()); -+ commands.put(Set.of("debug", "chunkinfo", "holderinfo"), new ChunkDebugCommand()); - - return commands.entrySet().stream() - .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue()))) -diff --git a/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java b/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java -new file mode 100644 -index 0000000000000000000000000000000000000000..962d3cae6340fc11607b59355e291629618f289c ---- /dev/null -+++ b/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java -@@ -0,0 +1,265 @@ -+package io.papermc.paper.command.subcommands; -+ -+import io.papermc.paper.command.CommandUtil; -+import io.papermc.paper.command.PaperSubcommand; -+import java.io.File; -+import java.time.LocalDateTime; -+import java.time.format.DateTimeFormatter; -+import java.util.ArrayList; -+import java.util.Collections; -+import java.util.List; -+import java.util.Locale; -+import io.papermc.paper.util.MCUtil; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ChunkHolder; -+import net.minecraft.server.level.FullChunkStatus; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.level.chunk.ChunkAccess; -+import net.minecraft.world.level.chunk.ImposterProtoChunk; -+import net.minecraft.world.level.chunk.LevelChunk; -+import net.minecraft.world.level.chunk.ProtoChunk; -+import org.bukkit.Bukkit; -+import org.bukkit.command.CommandSender; -+import org.bukkit.craftbukkit.CraftWorld; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.checker.nullness.qual.Nullable; -+import org.checkerframework.framework.qual.DefaultQualifier; -+ -+import static net.kyori.adventure.text.Component.text; -+import static net.kyori.adventure.text.format.NamedTextColor.BLUE; -+import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA; -+import static net.kyori.adventure.text.format.NamedTextColor.GREEN; -+import static net.kyori.adventure.text.format.NamedTextColor.RED; -+ -+@DefaultQualifier(NonNull.class) -+public final class ChunkDebugCommand implements PaperSubcommand { -+ @Override -+ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { -+ switch (subCommand) { -+ case "debug" -> this.doDebug(sender, args); -+ case "chunkinfo" -> this.doChunkInfo(sender, args); -+ case "holderinfo" -> this.doHolderInfo(sender, args); -+ } -+ return true; -+ } -+ -+ @Override -+ public List tabComplete(final CommandSender sender, final String subCommand, final String[] args) { -+ switch (subCommand) { -+ case "debug" -> { -+ if (args.length == 1) { -+ return CommandUtil.getListMatchingLast(sender, args, "help", "chunks"); -+ } -+ } -+ case "holderinfo" -> { -+ List worldNames = new ArrayList<>(); -+ worldNames.add("*"); -+ for (org.bukkit.World world : Bukkit.getWorlds()) { -+ worldNames.add(world.getName()); -+ } -+ if (args.length == 1) { -+ return CommandUtil.getListMatchingLast(sender, args, worldNames); -+ } -+ } -+ case "chunkinfo" -> { -+ List worldNames = new ArrayList<>(); -+ worldNames.add("*"); -+ for (org.bukkit.World world : Bukkit.getWorlds()) { -+ worldNames.add(world.getName()); -+ } -+ if (args.length == 1) { -+ return CommandUtil.getListMatchingLast(sender, args, worldNames); -+ } -+ } -+ } -+ return Collections.emptyList(); -+ } -+ -+ private void doChunkInfo(final CommandSender sender, final String[] args) { -+ List worlds; -+ if (args.length < 1 || args[0].equals("*")) { -+ worlds = Bukkit.getWorlds(); -+ } else { -+ worlds = new ArrayList<>(args.length); -+ for (final String arg : args) { -+ org.bukkit.@Nullable World world = Bukkit.getWorld(arg); -+ if (world == null) { -+ sender.sendMessage(text("World '" + arg + "' is invalid", RED)); -+ return; -+ } -+ worlds.add(world); -+ } -+ } -+ -+ int accumulatedTotal = 0; -+ int accumulatedInactive = 0; -+ int accumulatedBorder = 0; -+ int accumulatedTicking = 0; -+ int accumulatedEntityTicking = 0; -+ -+ for (final org.bukkit.World bukkitWorld : worlds) { -+ final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle(); -+ -+ int total = 0; -+ int inactive = 0; -+ int full = 0; -+ int blockTicking = 0; -+ int entityTicking = 0; -+ -+ for (final ChunkHolder chunk : io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(world)) { -+ if (chunk.getFullChunkNowUnchecked() == null) { -+ continue; -+ } -+ -+ ++total; -+ -+ FullChunkStatus state = chunk.getFullStatus(); -+ -+ switch (state) { -+ case INACCESSIBLE -> ++inactive; -+ case FULL -> ++full; -+ case BLOCK_TICKING -> ++blockTicking; -+ case ENTITY_TICKING -> ++entityTicking; -+ } -+ } -+ -+ accumulatedTotal += total; -+ accumulatedInactive += inactive; -+ accumulatedBorder += full; -+ accumulatedTicking += blockTicking; -+ accumulatedEntityTicking += entityTicking; -+ -+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":"))); -+ sender.sendMessage(text().color(DARK_AQUA).append( -+ text("Total: ", BLUE), text(total), -+ text(" Inactive: ", BLUE), text(inactive), -+ text(" Full: ", BLUE), text(full), -+ text(" Block Ticking: ", BLUE), text(blockTicking), -+ text(" Entity Ticking: ", BLUE), text(entityTicking) -+ )); -+ } -+ if (worlds.size() > 1) { -+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text("all listed worlds", GREEN), text(":", DARK_AQUA))); -+ sender.sendMessage(text().color(DARK_AQUA).append( -+ text("Total: ", BLUE), text(accumulatedTotal), -+ text(" Inactive: ", BLUE), text(accumulatedInactive), -+ text(" Full: ", BLUE), text(accumulatedBorder), -+ text(" Block Ticking: ", BLUE), text(accumulatedTicking), -+ text(" Entity Ticking: ", BLUE), text(accumulatedEntityTicking) -+ )); -+ } -+ } -+ -+ private void doHolderInfo(final CommandSender sender, final String[] args) { -+ List worlds; -+ if (args.length < 1 || args[0].equals("*")) { -+ worlds = Bukkit.getWorlds(); -+ } else { -+ worlds = new ArrayList<>(args.length); -+ for (final String arg : args) { -+ org.bukkit.@Nullable World world = Bukkit.getWorld(arg); -+ if (world == null) { -+ sender.sendMessage(text("World '" + arg + "' is invalid", RED)); -+ return; -+ } -+ worlds.add(world); -+ } -+ } -+ -+ int accumulatedTotal = 0; -+ int accumulatedCanUnload = 0; -+ int accumulatedNull = 0; -+ int accumulatedReadOnly = 0; -+ int accumulatedProtoChunk = 0; -+ int accumulatedFullChunk = 0; -+ -+ for (final org.bukkit.World bukkitWorld : worlds) { -+ final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle(); -+ -+ int total = 0; -+ int canUnload = 0; -+ int nullChunks = 0; -+ int readOnly = 0; -+ int protoChunk = 0; -+ int fullChunk = 0; -+ -+ for (final ChunkHolder chunk : world.chunkTaskScheduler.chunkHolderManager.getOldChunkHolders()) { // Paper - change updating chunks map -+ final ChunkAccess lastChunk = chunk.getAvailableChunkNow(); -+ -+ ++total; -+ -+ if (lastChunk == null) { -+ ++nullChunks; -+ } else if (lastChunk instanceof ImposterProtoChunk) { -+ ++readOnly; -+ } else if (lastChunk instanceof ProtoChunk) { -+ ++protoChunk; -+ } else if (lastChunk instanceof LevelChunk) { -+ ++fullChunk; -+ } -+ -+ if (chunk.newChunkHolder.isSafeToUnload() == null) { -+ ++canUnload; -+ } -+ } -+ -+ accumulatedTotal += total; -+ accumulatedCanUnload += canUnload; -+ accumulatedNull += nullChunks; -+ accumulatedReadOnly += readOnly; -+ accumulatedProtoChunk += protoChunk; -+ accumulatedFullChunk += fullChunk; -+ -+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":"))); -+ sender.sendMessage(text().color(DARK_AQUA).append( -+ text("Total: ", BLUE), text(total), -+ text(" Unloadable: ", BLUE), text(canUnload), -+ text(" Null: ", BLUE), text(nullChunks), -+ text(" ReadOnly: ", BLUE), text(readOnly), -+ text(" Proto: ", BLUE), text(protoChunk), -+ text(" Full: ", BLUE), text(fullChunk) -+ )); -+ } -+ if (worlds.size() > 1) { -+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text("all listed worlds", GREEN), text(":", DARK_AQUA))); -+ sender.sendMessage(text().color(DARK_AQUA).append( -+ text("Total: ", BLUE), text(accumulatedTotal), -+ text(" Unloadable: ", BLUE), text(accumulatedCanUnload), -+ text(" Null: ", BLUE), text(accumulatedNull), -+ text(" ReadOnly: ", BLUE), text(accumulatedReadOnly), -+ text(" Proto: ", BLUE), text(accumulatedProtoChunk), -+ text(" Full: ", BLUE), text(accumulatedFullChunk) -+ )); -+ } -+ } -+ -+ private void doDebug(final CommandSender sender, final String[] args) { -+ if (args.length < 1) { -+ sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED)); -+ return; -+ } -+ -+ final String debugType = args[0].toLowerCase(Locale.ENGLISH); -+ switch (debugType) { -+ case "chunks" -> { -+ if (args.length >= 2 && args[1].toLowerCase(Locale.ENGLISH).equals("help")) { -+ sender.sendMessage(text("Use /paper debug chunks [world] to dump loaded chunk information to a file", RED)); -+ break; -+ } -+ File file = new File(new File(new File("."), "debug"), -+ "chunks-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + ".txt"); -+ sender.sendMessage(text("Writing chunk information dump to " + file, GREEN)); -+ try { -+ MCUtil.dumpChunks(file, false); -+ sender.sendMessage(text("Successfully written chunk information!", GREEN)); -+ } catch (Throwable thr) { -+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr); -+ sender.sendMessage(text("Failed to dump chunk information, see console", RED)); -+ } -+ } -+ // "help" & default -+ default -> sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED)); -+ } -+ } -+ -+} -diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java -index 481cfe3a291196c0391c2ec8560e566e0e1e2fb8..30fe1c0645a07d663b08c0f988a1ab3a750bf7c4 100644 ---- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java -+++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java -@@ -29,6 +29,45 @@ public class GlobalConfiguration extends ConfigurationPart { - public static GlobalConfiguration get() { - return instance; - } -+ -+ public ChunkLoadingBasic chunkLoadingBasic; -+ -+ public class ChunkLoadingBasic extends ConfigurationPart { -+ @Comment("The maximum rate in chunks per second that the server will send to any individual player. Set to -1 to disable this limit.") -+ public double playerMaxChunkSendRate = 75.0; -+ -+ @Comment( -+ "The maximum rate at which chunks will load for any individual player. " + -+ "Note that this setting also affects chunk generations, since a chunk load is always first issued to test if a" + -+ "chunk is already generated. Set to -1 to disable this limit." -+ ) -+ public double playerMaxChunkLoadRate = 100.0; -+ -+ @Comment("The maximum rate at which chunks will generate for any individual player. Set to -1 to disable this limit.") -+ public double playerMaxChunkGenerateRate = -1.0; -+ } -+ -+ public ChunkLoadingAdvanced chunkLoadingAdvanced; -+ -+ public class ChunkLoadingAdvanced extends ConfigurationPart { -+ @Comment( -+ "Set to true if the server will match the chunk send radius that clients have configured" + -+ "in their view distance settings if the client is less-than the server's send distance." -+ ) -+ public boolean autoConfigSendDistance = true; -+ -+ @Comment( -+ "Specifies the maximum amount of concurrent chunk loads that an individual player can have." + -+ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit." -+ ) -+ public int playerMaxConcurrentChunkLoads = 0; -+ -+ @Comment( -+ "Specifies the maximum amount of concurrent chunk generations that an individual player can have." + -+ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit." -+ ) -+ public int playerMaxConcurrentChunkGenerates = 0; -+ } - static void set(GlobalConfiguration instance) { - GlobalConfiguration.instance = instance; - } -@@ -130,21 +169,6 @@ public class GlobalConfiguration extends ConfigurationPart { - public int incomingPacketThreshold = 300; - } - -- public ChunkLoading chunkLoading; -- -- public class ChunkLoading extends ConfigurationPart { -- public int minLoadRadius = 2; -- public int maxConcurrentSends = 2; -- public boolean autoconfigSendDistance = true; -- public double targetPlayerChunkSendRate = 100.0; -- public double globalMaxChunkSendRate = -1.0; -- public boolean enableFrustumPriority = false; -- public double globalMaxChunkLoadRate = -1.0; -- public double playerMaxConcurrentLoads = 20.0; -- public double globalMaxConcurrentLoads = 500.0; -- public double playerMaxChunkLoadRate = -1.0; -- } -- - public UnsupportedSettings unsupportedSettings; - - public class UnsupportedSettings extends ConfigurationPart { -@@ -203,7 +227,7 @@ public class GlobalConfiguration extends ConfigurationPart { - - @PostProcess - private void postProcess() { -- //io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.init(this); -+ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.init(this); - } - } - -diff --git a/src/main/java/io/papermc/paper/threadedregions/TickRegions.java b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d5d39e9c1f326e91010237b0db80d527ac52f4d6 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java -@@ -0,0 +1,9 @@ -+package io.papermc.paper.threadedregions; -+ -+// placeholder class for Folia -+public class TickRegions { -+ -+ public static int getRegionChunkShift() { -+ return 4; -+ } -+} -diff --git a/src/main/java/io/papermc/paper/util/IntervalledCounter.java b/src/main/java/io/papermc/paper/util/IntervalledCounter.java -index cea9c098ade00ee87b8efc8164ab72f5279758f0..197224e31175252d8438a8df585bbb65f2288d7f 100644 ---- a/src/main/java/io/papermc/paper/util/IntervalledCounter.java -+++ b/src/main/java/io/papermc/paper/util/IntervalledCounter.java -@@ -2,6 +2,8 @@ package io.papermc.paper.util; - - public final class IntervalledCounter { - -+ private static final int INITIAL_SIZE = 8; -+ - protected long[] times; - protected long[] counts; - protected final long interval; -@@ -11,8 +13,8 @@ public final class IntervalledCounter { - protected int tail; // exclusive - - public IntervalledCounter(final long interval) { -- this.times = new long[8]; -- this.counts = new long[8]; -+ this.times = new long[INITIAL_SIZE]; -+ this.counts = new long[INITIAL_SIZE]; - this.interval = interval; - } - -@@ -67,13 +69,13 @@ public final class IntervalledCounter { - this.tail = nextTail; - } - -- public void updateAndAdd(final int count) { -+ public void updateAndAdd(final long count) { - final long currTime = System.nanoTime(); - this.updateCurrentTime(currTime); - this.addTime(currTime, count); - } - -- public void updateAndAdd(final int count, final long currTime) { -+ public void updateAndAdd(final long count, final long currTime) { - this.updateCurrentTime(currTime); - this.addTime(currTime, count); - } -@@ -93,9 +95,13 @@ public final class IntervalledCounter { - this.tail = size; - - if (tail >= head) { -+ // sequentially ordered from [head, tail) - System.arraycopy(oldElements, head, newElements, 0, size); - System.arraycopy(oldCounts, head, newCounts, 0, size); - } else { -+ // ordered from [head, length) -+ // then followed by [0, tail) -+ - System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head); - System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail); - -@@ -106,10 +112,18 @@ public final class IntervalledCounter { - - // returns in units per second - public double getRate() { -- return this.size() / (this.interval * 1.0e-9); -+ return (double)this.sum / ((double)this.interval * 1.0E-9); -+ } -+ -+ public long getInterval() { -+ return this.interval; - } - -- public long size() { -+ public long getSum() { - return this.sum; - } -+ -+ public int totalDataPoints() { -+ return this.tail >= this.head ? (this.tail - this.head) : (this.tail + (this.counts.length - this.head)); -+ } - } -diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java -index 5c401c7e30ed5f6f32b1ad6523537aa46173cff5..e5c2e53388ff61e4aeae22a74b525d9a26ea0200 100644 ---- a/src/main/java/io/papermc/paper/util/MCUtil.java -+++ b/src/main/java/io/papermc/paper/util/MCUtil.java -@@ -7,17 +7,30 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; - import io.papermc.paper.math.BlockPosition; - import io.papermc.paper.math.FinePosition; - import io.papermc.paper.math.Position; -+import com.google.gson.JsonArray; -+import com.google.gson.JsonObject; -+import com.google.gson.internal.Streams; -+import com.google.gson.stream.JsonWriter; -+import com.mojang.datafixers.util.Either; - import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet; - import java.lang.ref.Cleaner; -+import it.unimi.dsi.fastutil.objects.ReferenceArrayList; - import net.minecraft.core.BlockPos; - import net.minecraft.core.Direction; - import net.minecraft.core.Vec3i; - import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ChunkHolder; -+import net.minecraft.server.level.ChunkMap; -+import net.minecraft.server.level.DistanceManager; - import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.server.level.Ticket; - import net.minecraft.world.entity.Entity; - import net.minecraft.world.level.ChunkPos; - import net.minecraft.world.level.ClipContext; - import net.minecraft.world.level.Level; -+import net.minecraft.world.level.chunk.ChunkAccess; -+import net.minecraft.world.level.chunk.status.ChunkStatus; - import net.minecraft.world.phys.Vec3; - import org.apache.commons.lang.exception.ExceptionUtils; - import com.mojang.authlib.GameProfile; -@@ -30,8 +43,11 @@ import org.spigotmc.AsyncCatcher; - - import javax.annotation.Nonnull; - import javax.annotation.Nullable; -+import java.io.*; -+import java.nio.charset.StandardCharsets; - import java.util.List; - import java.util.Queue; -+import java.util.Set; - import java.util.concurrent.CompletableFuture; - import java.util.concurrent.ExecutionException; - import java.util.concurrent.LinkedBlockingQueue; -@@ -532,6 +548,98 @@ public final class MCUtil { - } - } - -+ public static ChunkStatus getChunkStatus(ChunkHolder chunk) { -+ return chunk.getChunkHolderStatus(); -+ } -+ -+ public static void dumpChunks(File file, boolean watchdog) throws IOException { -+ file.getParentFile().mkdirs(); -+ file.createNewFile(); -+ ReferenceArrayList worlds = new ReferenceArrayList<>(org.bukkit.Bukkit.getWorlds()); -+ ReferenceArrayList loadedWorlds = new ReferenceArrayList<>(worlds); -+ JsonObject data = new JsonObject(); -+ -+ data.addProperty("server-version", org.bukkit.Bukkit.getVersion()); -+ data.addProperty("data-version", 1); -+ -+ { -+ JsonArray players = new JsonArray(); -+ data.add("all-players", players); -+ List playerList = MinecraftServer.getServer().getPlayerList().players; -+ for (ServerPlayer player : playerList) { -+ JsonObject playerData = new JsonObject(); -+ players.add(playerData); -+ -+ Level playerWorld = player.level(); -+ org.bukkit.World craftWorld = playerWorld.getWorld(); -+ Entity.RemovalReason removalReason = player.getRemovalReason(); -+ -+ playerData.addProperty("name", player.getScoreboardName()); -+ playerData.addProperty("x", player.getX()); -+ playerData.addProperty("y", player.getY()); -+ playerData.addProperty("z", player.getZ()); -+ playerData.addProperty("world", playerWorld == null ? "null world" : craftWorld.getName()); -+ playerData.addProperty("removalReason", removalReason == null ? "null" : removalReason.name()); -+ -+ if (!worlds.contains(craftWorld)) { -+ worlds.add(craftWorld); -+ } -+ } -+ } -+ -+ JsonArray chunkWaitInformation = new JsonArray(); -+ data.add("chunk-wait-infos", chunkWaitInformation); -+ -+ for (io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.ChunkInfo chunkInfo : io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.getChunkInfos()) { -+ chunkWaitInformation.add(chunkInfo.toString()); -+ } -+ -+ JsonArray worldsData = new JsonArray(); -+ -+ for (org.bukkit.World bukkitWorld : worlds) { -+ JsonObject worldData = new JsonObject(); -+ -+ ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)bukkitWorld).getHandle(); -+ List players = world.players(); -+ -+ worldData.addProperty("is-loaded", loadedWorlds.contains(bukkitWorld)); -+ worldData.addProperty("name", world.getWorld().getName()); -+ worldData.addProperty("view-distance", world.getWorld().getViewDistance()); // Paper - replace chunk loader system -+ worldData.addProperty("tick-view-distance", world.getWorld().getSimulationDistance()); // Paper - replace chunk loader system -+ -+ JsonArray playersData = new JsonArray(); -+ -+ for (ServerPlayer player : players) { -+ JsonObject playerData = new JsonObject(); -+ -+ playerData.addProperty("name", player.getScoreboardName()); -+ playerData.addProperty("x", player.getX()); -+ playerData.addProperty("y", player.getY()); -+ playerData.addProperty("z", player.getZ()); -+ -+ playersData.add(playerData); -+ } -+ -+ worldData.add("players", playersData); -+ worldData.add("chunk-data", watchdog ? world.chunkTaskScheduler.chunkHolderManager.getDebugJsonForWatchdog() : world.chunkTaskScheduler.chunkHolderManager.getDebugJson()); -+ worldsData.add(worldData); -+ } -+ -+ data.add("worlds", worldsData); -+ -+ StringWriter stringWriter = new StringWriter(); -+ JsonWriter jsonWriter = new JsonWriter(stringWriter); -+ jsonWriter.setIndent(" "); -+ jsonWriter.setLenient(false); -+ Streams.write(data, jsonWriter); -+ -+ String fileData = stringWriter.toString(); -+ -+ try (PrintStream out = new PrintStream(new FileOutputStream(file), false, StandardCharsets.UTF_8)) { -+ out.print(fileData); -+ } -+ } -+ - public static int getTicketLevelFor(net.minecraft.world.level.chunk.status.ChunkStatus status) { - return net.minecraft.server.level.ChunkMap.MAX_VIEW_DISTANCE + net.minecraft.world.level.chunk.status.ChunkStatus.getDistance(status); - } -diff --git a/src/main/java/io/papermc/paper/util/TickThread.java b/src/main/java/io/papermc/paper/util/TickThread.java -index 73e83d56a340f0c7dcb8ff737d621003e72c6de4..bdaf062f9b66ceab303a0807eca301342886a8ea 100644 ---- a/src/main/java/io/papermc/paper/util/TickThread.java -+++ b/src/main/java/io/papermc/paper/util/TickThread.java -@@ -1,12 +1,20 @@ - package io.papermc.paper.util; - -+import net.minecraft.core.BlockPos; - import net.minecraft.server.MinecraftServer; - import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.server.network.ServerGamePacketListenerImpl; -+import net.minecraft.util.Mth; - import net.minecraft.world.entity.Entity; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.phys.AABB; -+import net.minecraft.world.phys.Vec3; - import org.bukkit.Bukkit; - import java.util.concurrent.atomic.AtomicInteger; - --public final class TickThread extends Thread { -+public class TickThread extends Thread { - - public static final boolean STRICT_THREAD_CHECKS = Boolean.getBoolean("paper.strict-thread-checks"); - -@@ -16,6 +24,10 @@ public final class TickThread extends Thread { - } - } - -+ /** -+ * @deprecated -+ */ -+ @Deprecated - public static void softEnsureTickThread(final String reason) { - if (!STRICT_THREAD_CHECKS) { - return; -@@ -23,6 +35,10 @@ public final class TickThread extends Thread { - ensureTickThread(reason); - } - -+ /** -+ * @deprecated -+ */ -+ @Deprecated - public static void ensureTickThread(final String reason) { - if (!isTickThread()) { - MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); -@@ -30,6 +46,20 @@ public final class TickThread extends Thread { - } - } - -+ public static void ensureTickThread(final ServerLevel world, final BlockPos pos, final String reason) { -+ if (!isTickThreadFor(world, pos)) { -+ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); -+ throw new IllegalStateException(reason); -+ } -+ } -+ -+ public static void ensureTickThread(final ServerLevel world, final ChunkPos pos, final String reason) { -+ if (!isTickThreadFor(world, pos)) { -+ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); -+ throw new IllegalStateException(reason); -+ } -+ } -+ - public static void ensureTickThread(final ServerLevel world, final int chunkX, final int chunkZ, final String reason) { - if (!isTickThreadFor(world, chunkX, chunkZ)) { - MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); -@@ -44,6 +74,20 @@ public final class TickThread extends Thread { - } - } - -+ public static void ensureTickThread(final ServerLevel world, final AABB aabb, final String reason) { -+ if (!isTickThreadFor(world, aabb)) { -+ MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); -+ throw new IllegalStateException(reason); -+ } -+ } -+ -+ public static void ensureTickThread(final ServerLevel world, final double blockX, final double blockZ, final String reason) { -+ if (!isTickThreadFor(world, blockX, blockZ)) { -+ MinecraftServer.LOGGER.error("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) */ - - private static final AtomicInteger ID_GENERATOR = new AtomicInteger(); -@@ -66,13 +110,45 @@ public final class TickThread extends Thread { - } - - public static boolean isTickThread() { -- return Bukkit.isPrimaryThread(); -+ return Thread.currentThread() instanceof TickThread; -+ } -+ -+ public static boolean isShutdownThread() { -+ return false; -+ } -+ -+ public static boolean isTickThreadFor(final ServerLevel world, final BlockPos pos) { -+ return isTickThread(); -+ } -+ -+ public static boolean isTickThreadFor(final ServerLevel world, final ChunkPos pos) { -+ return isTickThread(); -+ } -+ -+ public static boolean isTickThreadFor(final ServerLevel world, final Vec3 pos) { -+ return isTickThread(); - } - - public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ) { - return isTickThread(); - } - -+ public static boolean isTickThreadFor(final ServerLevel world, final AABB aabb) { -+ return isTickThread(); -+ } -+ -+ public static boolean isTickThreadFor(final ServerLevel world, final double blockX, final double blockZ) { -+ return isTickThread(); -+ } -+ -+ public static boolean isTickThreadFor(final ServerLevel world, final Vec3 position, final Vec3 deltaMovement, final int buffer) { -+ return isTickThread(); -+ } -+ -+ public static boolean isTickThreadFor(final ServerLevel world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) { -+ return isTickThread(); -+ } -+ - public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ, final int radius) { - return isTickThread(); - } -diff --git a/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java b/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c78cbec447032de9fe69748591bef6be300160ed ---- /dev/null -+++ b/src/main/java/io/papermc/paper/world/ChunkEntitySlices.java -@@ -0,0 +1,607 @@ -+package io.papermc.paper.world; -+ -+import com.destroystokyo.paper.util.maplist.EntityList; -+import io.papermc.paper.chunk.system.entity.EntityLookup; -+import io.papermc.paper.util.TickThread; -+import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; -+import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.server.level.ChunkHolder; -+import net.minecraft.server.level.FullChunkStatus; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.util.Mth; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.entity.EntityType; -+import net.minecraft.world.entity.boss.EnderDragonPart; -+import net.minecraft.world.entity.boss.enderdragon.EnderDragon; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.chunk.storage.EntityStorage; -+import net.minecraft.world.level.entity.Visibility; -+import net.minecraft.world.phys.AABB; -+import org.bukkit.craftbukkit.event.CraftEventFactory; -+import java.util.ArrayList; -+import java.util.Arrays; -+import java.util.Iterator; -+import java.util.List; -+import java.util.function.Predicate; -+import org.bukkit.event.entity.EntityRemoveEvent; -+ -+public final class ChunkEntitySlices { -+ -+ protected final int minSection; -+ protected final int maxSection; -+ public final int chunkX; -+ public final int chunkZ; -+ protected final ServerLevel world; -+ -+ protected final EntityCollectionBySection allEntities; -+ protected final EntityCollectionBySection hardCollidingEntities; -+ protected final Reference2ObjectOpenHashMap, EntityCollectionBySection> entitiesByClass; -+ protected final EntityList entities = new EntityList(); -+ -+ public FullChunkStatus status; -+ -+ protected boolean isTransient; -+ -+ public boolean isTransient() { -+ return this.isTransient; -+ } -+ -+ public void setTransient(final boolean value) { -+ this.isTransient = value; -+ } -+ -+ // TODO implement container search optimisations -+ -+ public ChunkEntitySlices(final ServerLevel world, final int chunkX, final int chunkZ, final FullChunkStatus status, -+ final int minSection, final int maxSection) { // inclusive, inclusive -+ this.minSection = minSection; -+ this.maxSection = maxSection; -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ this.world = world; -+ -+ this.allEntities = new EntityCollectionBySection(this); -+ this.hardCollidingEntities = new EntityCollectionBySection(this); -+ this.entitiesByClass = new Reference2ObjectOpenHashMap<>(); -+ -+ this.status = status; -+ } -+ -+ // Paper start - optimise CraftChunk#getEntities -+ public org.bukkit.entity.Entity[] getChunkEntities() { -+ List ret = new java.util.ArrayList<>(); -+ final Entity[] entities = this.entities.getRawData(); -+ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) { -+ final Entity entity = entities[i]; -+ if (entity == null) { -+ continue; -+ } -+ final org.bukkit.entity.Entity bukkit = entity.getBukkitEntity(); -+ if (bukkit != null && bukkit.isValid()) { -+ ret.add(bukkit); -+ } -+ } -+ -+ return ret.toArray(new org.bukkit.entity.Entity[0]); -+ } -+ -+ public CompoundTag save() { -+ final int len = this.entities.size(); -+ if (len == 0) { -+ return null; -+ } -+ -+ final Entity[] rawData = this.entities.getRawData(); -+ final List collectedEntities = new ArrayList<>(len); -+ for (int i = 0; i < len; ++i) { -+ final Entity entity = rawData[i]; -+ if (entity.shouldBeSaved()) { -+ collectedEntities.add(entity); -+ } -+ } -+ -+ if (collectedEntities.isEmpty()) { -+ return null; -+ } -+ -+ return EntityStorage.saveEntityChunk(collectedEntities, new ChunkPos(this.chunkX, this.chunkZ), this.world); -+ } -+ -+ // returns true if this chunk has transient entities remaining -+ public boolean unload() { -+ final int len = this.entities.size(); -+ final Entity[] collectedEntities = Arrays.copyOf(this.entities.getRawData(), len); -+ -+ for (int i = 0; i < len; ++i) { -+ final Entity entity = collectedEntities[i]; -+ if (entity.isRemoved()) { -+ // removed by us below -+ continue; -+ } -+ if (entity.shouldBeSaved()) { -+ entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, EntityRemoveEvent.Cause.UNLOAD); -+ if (entity.isVehicle()) { -+ // we cannot assume that these entities are contained within this chunk, because entities can -+ // desync - so we need to remove them all -+ for (final Entity passenger : entity.getIndirectPassengers()) { -+ passenger.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, EntityRemoveEvent.Cause.UNLOAD); -+ } -+ } -+ } -+ } -+ -+ return this.entities.size() != 0; -+ } -+ -+ private List getAllEntities() { -+ final int len = this.entities.size(); -+ if (len == 0) { -+ return new ArrayList<>(); -+ } -+ -+ final Entity[] rawData = this.entities.getRawData(); -+ final List collectedEntities = new ArrayList<>(len); -+ for (int i = 0; i < len; ++i) { -+ collectedEntities.add(rawData[i]); -+ } -+ -+ return collectedEntities; -+ } -+ -+ public void callEntitiesLoadEvent() { -+ CraftEventFactory.callEntitiesLoadEvent(this.world, new ChunkPos(this.chunkX, this.chunkZ), this.getAllEntities()); -+ } -+ -+ public void callEntitiesUnloadEvent() { -+ CraftEventFactory.callEntitiesUnloadEvent(this.world, new ChunkPos(this.chunkX, this.chunkZ), this.getAllEntities()); -+ } -+ // Paper end - optimise CraftChunk#getEntities -+ -+ public boolean isEmpty() { -+ return this.entities.size() == 0; -+ } -+ -+ public void mergeInto(final ChunkEntitySlices slices) { -+ final Entity[] entities = this.entities.getRawData(); -+ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) { -+ final Entity entity = entities[i]; -+ slices.addEntity(entity, entity.sectionY); -+ } -+ } -+ -+ private boolean preventStatusUpdates; -+ public boolean startPreventingStatusUpdates() { -+ final boolean ret = this.preventStatusUpdates; -+ this.preventStatusUpdates = true; -+ return ret; -+ } -+ -+ public boolean isPreventingStatusUpdates() { -+ return this.preventStatusUpdates; -+ } -+ -+ public void stopPreventingStatusUpdates(final boolean prev) { -+ this.preventStatusUpdates = prev; -+ } -+ -+ public void updateStatus(final FullChunkStatus status, final EntityLookup lookup) { -+ this.status = status; -+ -+ final Entity[] entities = this.entities.getRawData(); -+ -+ for (int i = 0, size = this.entities.size(); i < size; ++i) { -+ final Entity entity = entities[i]; -+ -+ final Visibility oldVisibility = EntityLookup.getEntityStatus(entity); -+ entity.chunkStatus = status; -+ final Visibility newVisibility = EntityLookup.getEntityStatus(entity); -+ -+ lookup.entityStatusChange(entity, this, oldVisibility, newVisibility, false, false, false); -+ } -+ } -+ -+ public boolean addEntity(final Entity entity, final int chunkSection) { -+ if (!this.entities.add(entity)) { -+ return false; -+ } -+ entity.chunkStatus = this.status; -+ final int sectionIndex = chunkSection - this.minSection; -+ -+ this.allEntities.addEntity(entity, sectionIndex); -+ -+ if (entity.hardCollides()) { -+ this.hardCollidingEntities.addEntity(entity, sectionIndex); -+ } -+ -+ for (final Iterator, EntityCollectionBySection>> iterator = -+ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { -+ final Reference2ObjectMap.Entry, EntityCollectionBySection> entry = iterator.next(); -+ -+ if (entry.getKey().isInstance(entity)) { -+ entry.getValue().addEntity(entity, sectionIndex); -+ } -+ } -+ -+ return true; -+ } -+ -+ public boolean removeEntity(final Entity entity, final int chunkSection) { -+ if (!this.entities.remove(entity)) { -+ return false; -+ } -+ entity.chunkStatus = null; -+ final int sectionIndex = chunkSection - this.minSection; -+ -+ this.allEntities.removeEntity(entity, sectionIndex); -+ -+ if (entity.hardCollides()) { -+ this.hardCollidingEntities.removeEntity(entity, sectionIndex); -+ } -+ -+ for (final Iterator, EntityCollectionBySection>> iterator = -+ this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) { -+ final Reference2ObjectMap.Entry, EntityCollectionBySection> entry = iterator.next(); -+ -+ if (entry.getKey().isInstance(entity)) { -+ entry.getValue().removeEntity(entity, sectionIndex); -+ } -+ } -+ -+ return true; -+ } -+ -+ public void getHardCollidingEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { -+ this.hardCollidingEntities.getEntities(except, box, into, predicate); -+ } -+ -+ public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { -+ this.allEntities.getEntitiesWithEnderDragonParts(except, box, into, predicate); -+ } -+ -+ public void getEntitiesWithoutDragonParts(final Entity except, final AABB box, final List into, final Predicate predicate) { -+ this.allEntities.getEntities(except, box, into, predicate); -+ } -+ -+ public void getEntities(final EntityType type, final AABB box, final List into, -+ final Predicate predicate) { -+ this.allEntities.getEntities(type, box, (List)into, (Predicate)predicate); -+ } -+ -+ protected EntityCollectionBySection initClass(final Class clazz) { -+ final EntityCollectionBySection ret = new EntityCollectionBySection(this); -+ -+ for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) { -+ final BasicEntityList sectionEntities = this.allEntities.entitiesBySection[sectionIndex]; -+ if (sectionEntities == null) { -+ continue; -+ } -+ -+ final Entity[] storage = sectionEntities.storage; -+ -+ for (int i = 0, len = Math.min(storage.length, sectionEntities.size()); i < len; ++i) { -+ final Entity entity = storage[i]; -+ -+ if (clazz.isInstance(entity)) { -+ ret.addEntity(entity, sectionIndex); -+ } -+ } -+ } -+ -+ return ret; -+ } -+ -+ public void getEntities(final Class clazz, final Entity except, final AABB box, final List into, -+ final Predicate predicate) { -+ EntityCollectionBySection collection = this.entitiesByClass.get(clazz); -+ if (collection != null) { -+ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate); -+ } else { -+ this.entitiesByClass.putIfAbsent(clazz, collection = this.initClass(clazz)); -+ collection.getEntitiesWithEnderDragonParts(except, clazz, box, (List)into, (Predicate)predicate); -+ } -+ } -+ -+ protected static final class BasicEntityList { -+ -+ protected static final Entity[] EMPTY = new Entity[0]; -+ protected static final int DEFAULT_CAPACITY = 4; -+ -+ protected E[] storage; -+ protected int size; -+ -+ public BasicEntityList() { -+ this(0); -+ } -+ -+ public BasicEntityList(final int cap) { -+ this.storage = (E[])(cap <= 0 ? EMPTY : new Entity[cap]); -+ } -+ -+ public boolean isEmpty() { -+ return this.size == 0; -+ } -+ -+ public int size() { -+ return this.size; -+ } -+ -+ private void resize() { -+ if (this.storage == EMPTY) { -+ this.storage = (E[])new Entity[DEFAULT_CAPACITY]; -+ } else { -+ this.storage = Arrays.copyOf(this.storage, this.storage.length * 2); -+ } -+ } -+ -+ public void add(final E entity) { -+ final int idx = this.size++; -+ if (idx >= this.storage.length) { -+ this.resize(); -+ this.storage[idx] = entity; -+ } else { -+ this.storage[idx] = entity; -+ } -+ } -+ -+ public int indexOf(final E entity) { -+ final E[] storage = this.storage; -+ -+ for (int i = 0, len = Math.min(this.storage.length, this.size); i < len; ++i) { -+ if (storage[i] == entity) { -+ return i; -+ } -+ } -+ -+ return -1; -+ } -+ -+ public boolean remove(final E entity) { -+ final int idx = this.indexOf(entity); -+ if (idx == -1) { -+ return false; -+ } -+ -+ final int size = --this.size; -+ final E[] storage = this.storage; -+ if (idx != size) { -+ System.arraycopy(storage, idx + 1, storage, idx, size - idx); -+ } -+ -+ storage[size] = null; -+ -+ return true; -+ } -+ -+ public boolean has(final E entity) { -+ return this.indexOf(entity) != -1; -+ } -+ } -+ -+ protected static final class EntityCollectionBySection { -+ -+ protected final ChunkEntitySlices manager; -+ protected final long[] nonEmptyBitset; -+ protected final BasicEntityList[] entitiesBySection; -+ protected int count; -+ -+ public EntityCollectionBySection(final ChunkEntitySlices manager) { -+ this.manager = manager; -+ -+ final int sectionCount = manager.maxSection - manager.minSection + 1; -+ -+ this.nonEmptyBitset = new long[(sectionCount + (Long.SIZE - 1)) >>> 6]; // (sectionCount + (Long.SIZE - 1)) / Long.SIZE -+ this.entitiesBySection = new BasicEntityList[sectionCount]; -+ } -+ -+ public void addEntity(final Entity entity, final int sectionIndex) { -+ BasicEntityList list = this.entitiesBySection[sectionIndex]; -+ -+ if (list != null && list.has(entity)) { -+ return; -+ } -+ -+ if (list == null) { -+ this.entitiesBySection[sectionIndex] = list = new BasicEntityList<>(); -+ this.nonEmptyBitset[sectionIndex >>> 6] |= (1L << (sectionIndex & (Long.SIZE - 1))); -+ } -+ -+ list.add(entity); -+ ++this.count; -+ } -+ -+ public void removeEntity(final Entity entity, final int sectionIndex) { -+ final BasicEntityList list = this.entitiesBySection[sectionIndex]; -+ -+ if (list == null || !list.remove(entity)) { -+ return; -+ } -+ -+ --this.count; -+ -+ if (list.isEmpty()) { -+ this.entitiesBySection[sectionIndex] = null; -+ this.nonEmptyBitset[sectionIndex >>> 6] ^= (1L << (sectionIndex & (Long.SIZE - 1))); -+ } -+ } -+ -+ public void getEntities(final Entity except, final AABB box, final List into, final Predicate predicate) { -+ if (this.count == 0) { -+ return; -+ } -+ -+ final int minSection = this.manager.minSection; -+ final int maxSection = this.manager.maxSection; -+ -+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); -+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); -+ -+ final BasicEntityList[] entitiesBySection = this.entitiesBySection; -+ -+ for (int section = min; section <= max; ++section) { -+ final BasicEntityList list = entitiesBySection[section - minSection]; -+ -+ if (list == null) { -+ continue; -+ } -+ -+ final Entity[] storage = list.storage; -+ -+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { -+ final Entity entity = storage[i]; -+ -+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { -+ continue; -+ } -+ -+ if (predicate != null && !predicate.test(entity)) { -+ continue; -+ } -+ -+ into.add(entity); -+ } -+ } -+ } -+ -+ public void getEntitiesWithEnderDragonParts(final Entity except, final AABB box, final List into, -+ final Predicate predicate) { -+ if (this.count == 0) { -+ return; -+ } -+ -+ final int minSection = this.manager.minSection; -+ final int maxSection = this.manager.maxSection; -+ -+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); -+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); -+ -+ final BasicEntityList[] entitiesBySection = this.entitiesBySection; -+ -+ for (int section = min; section <= max; ++section) { -+ final BasicEntityList list = entitiesBySection[section - minSection]; -+ -+ if (list == null) { -+ continue; -+ } -+ -+ final Entity[] storage = list.storage; -+ -+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { -+ final Entity entity = storage[i]; -+ -+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { -+ continue; -+ } -+ -+ if (predicate == null || predicate.test(entity)) { -+ into.add(entity); -+ } // else: continue to test the ender dragon parts -+ -+ if (entity instanceof EnderDragon) { -+ for (final EnderDragonPart part : ((EnderDragon)entity).subEntities) { -+ if (part == except || !part.getBoundingBox().intersects(box)) { -+ continue; -+ } -+ -+ if (predicate != null && !predicate.test(part)) { -+ continue; -+ } -+ -+ into.add(part); -+ } -+ } -+ } -+ } -+ } -+ -+ public void getEntitiesWithEnderDragonParts(final Entity except, final Class clazz, final AABB box, final List into, -+ final Predicate predicate) { -+ if (this.count == 0) { -+ return; -+ } -+ -+ final int minSection = this.manager.minSection; -+ final int maxSection = this.manager.maxSection; -+ -+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); -+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); -+ -+ final BasicEntityList[] entitiesBySection = this.entitiesBySection; -+ -+ for (int section = min; section <= max; ++section) { -+ final BasicEntityList list = entitiesBySection[section - minSection]; -+ -+ if (list == null) { -+ continue; -+ } -+ -+ final Entity[] storage = list.storage; -+ -+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { -+ final Entity entity = storage[i]; -+ -+ if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) { -+ continue; -+ } -+ -+ if (predicate == null || predicate.test(entity)) { -+ into.add(entity); -+ } // else: continue to test the ender dragon parts -+ -+ if (entity instanceof EnderDragon) { -+ for (final EnderDragonPart part : ((EnderDragon)entity).subEntities) { -+ if (part == except || !part.getBoundingBox().intersects(box) || !clazz.isInstance(part)) { -+ continue; -+ } -+ -+ if (predicate != null && !predicate.test(part)) { -+ continue; -+ } -+ -+ into.add(part); -+ } -+ } -+ } -+ } -+ } -+ -+ public void getEntities(final EntityType type, final AABB box, final List into, -+ final Predicate predicate) { -+ if (this.count == 0) { -+ return; -+ } -+ -+ final int minSection = this.manager.minSection; -+ final int maxSection = this.manager.maxSection; -+ -+ final int min = Mth.clamp(Mth.floor(box.minY - 2.0) >> 4, minSection, maxSection); -+ final int max = Mth.clamp(Mth.floor(box.maxY + 2.0) >> 4, minSection, maxSection); -+ -+ final BasicEntityList[] entitiesBySection = this.entitiesBySection; -+ -+ for (int section = min; section <= max; ++section) { -+ final BasicEntityList list = entitiesBySection[section - minSection]; -+ -+ if (list == null) { -+ continue; -+ } -+ -+ final Entity[] storage = list.storage; -+ -+ for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) { -+ final Entity entity = storage[i]; -+ -+ if (entity == null || (type != null && entity.getType() != type) || !entity.getBoundingBox().intersects(box)) { -+ continue; -+ } -+ -+ if (predicate != null && !predicate.test((T)entity)) { -+ continue; -+ } -+ -+ into.add((T)entity); -+ } -+ } -+ } -+ } -+} -diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java -index c33f85b570f159ab465b5a10a8044a81f2797f43..244a19ecd0234fa1d7a6ecfea20751595688605d 100644 ---- a/src/main/java/net/minecraft/server/Main.java -+++ b/src/main/java/net/minecraft/server/Main.java -@@ -320,6 +320,7 @@ public class Main { - - convertable_conversionsession.saveDataTag(iregistrycustom_dimension, savedata); - */ -+ Class.forName(net.minecraft.world.entity.npc.VillagerTrades.class.getName()); // Paper - load this sync so it won't fail later async - final DedicatedServer dedicatedserver = (DedicatedServer) MinecraftServer.spin((thread) -> { - DedicatedServer dedicatedserver1 = new DedicatedServer(optionset, worldLoader.get(), thread, convertable_conversionsession, resourcepackrepository, worldstem, dedicatedserversettings, DataFixers.getDataFixer(), services, LoggerChunkProgressListener::createFromGameruleRadius); - -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 881679270a344f1a4d70c3c47871a495af202109..40708dfd84dc21bc5e047d9fb3b092cb854a8041 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -318,7 +318,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { - AtomicReference atomicreference = new AtomicReference(); -- Thread thread = new Thread(() -> { -+ Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system - ((MinecraftServer) atomicreference.get()).runServer(); - }, "Server thread"); - -@@ -654,7 +654,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { -- return worldserver1.getChunkSource().chunkMap.hasWork(); -- })) { -- this.nextTickTimeNanos = Util.getNanos() + TimeUtil.NANOSECONDS_PER_MILLISECOND; -- iterator = this.getAllLevels().iterator(); -- -- while (iterator.hasNext()) { -- worldserver = (ServerLevel) iterator.next(); -- worldserver.getChunkSource().removeTicketsOnClosing(); -- worldserver.getChunkSource().tick(() -> { -- return true; -- }, false); -- } -- -- this.waitUntilNextTick(); -- } -- -- this.saveAllChunks(false, true, false); -- iterator = this.getAllLevels().iterator(); -- -- while (iterator.hasNext()) { -- worldserver = (ServerLevel) iterator.next(); -- if (worldserver != null) { -- try { -- worldserver.close(); -- } catch (IOException ioexception) { -- MinecraftServer.LOGGER.error("Exception closing the level", ioexception); -- } -- } -- } -+ this.saveAllChunks(false, true, false, true); // Paper - rewrite chunk system - move closing into here - - this.isSaving = false; - this.resources.close(); -@@ -1023,6 +1004,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { -- for (final Entity entity : level.getEntities().getAll()) { -+ for (final Entity entity : level.getEntityLookup().getAllCopy()) { // Paper - rewrite chunk system - if (entity.isRemoved()) { - continue; - } -@@ -2625,6 +2620,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { -+ LOGGER.info("Async debug chunks executing"); -+ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(false); -+ CommandSender sender = MinecraftServer.getServer().console; -+ java.io.File file = new java.io.File(new java.io.File(new java.io.File("."), "debug"), -+ "chunks-" + java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(java.time.LocalDateTime.now()) + ".txt"); -+ sender.sendMessage(net.kyori.adventure.text.Component.text("Writing chunk information dump to " + file, net.kyori.adventure.text.format.NamedTextColor.GREEN)); -+ try { -+ io.papermc.paper.util.MCUtil.dumpChunks(file, true); -+ sender.sendMessage(net.kyori.adventure.text.Component.text("Successfully written chunk information!", net.kyori.adventure.text.format.NamedTextColor.GREEN)); -+ } catch (Throwable thr) { -+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr); -+ sender.sendMessage(net.kyori.adventure.text.Component.text("Failed to dump chunk information, see console", net.kyori.adventure.text.format.NamedTextColor.RED)); -+ } -+ }; -+ Thread t = new Thread(run); -+ t.setName("Async debug thread #" + ASYNC_DEBUG_CHUNKS_COUNT.getAndIncrement()); -+ t.setDaemon(true); -+ t.start(); -+ return; -+ } -+ // Paper end - rewrite chunk system - this.serverCommandQueue.add(new ConsoleInput(command, commandSource)); // Paper - Perf: use proper queue - } - -diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java -index 88729d92878f98729eb5669cce5ae5b1418865a1..13d15a135dd0373bef4a5ac9ffb56dbbf53353a0 100644 ---- a/src/main/java/net/minecraft/server/level/ChunkHolder.java -+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java -@@ -46,17 +46,12 @@ public class ChunkHolder { - public static final ChunkResult NOT_DONE_YET = ChunkResult.error("Not done yet"); - private static final CompletableFuture> UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.completedFuture(ChunkHolder.UNLOADED_LEVEL_CHUNK); - private static final List CHUNK_STATUSES = ChunkStatus.getStatusList(); -- private final AtomicReferenceArray>> futures; -+ // Paper - rewrite chunk system - private final LevelHeightAccessor levelHeightAccessor; -- private volatile CompletableFuture> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage -- private volatile CompletableFuture> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage -- private volatile CompletableFuture> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage -- public CompletableFuture chunkToSave; // Paper - public -+ // Paper - rewrite chunk system - @Nullable - private final DebugBuffer chunkToSaveHistory; -- public int oldTicketLevel; -- private int ticketLevel; -- private int queueLevel; -+ // Paper - rewrite chunk system - public final ChunkPos pos; - private boolean hasChangedSections; - private final ShortSet[] changedBlocksPerSection; -@@ -65,11 +60,20 @@ public class ChunkHolder { - private final LevelLightEngine lightEngine; - private final ChunkHolder.LevelChangeListener onLevelChange; - public final ChunkHolder.PlayerProvider playerProvider; -- private boolean wasAccessibleSinceLastSave; -- private CompletableFuture pendingFullStateConfirmation; -- private CompletableFuture sendSync; -+ // Paper - rewrite chunk system - - private final ChunkMap chunkMap; // Paper -+ // Paper start - no-tick view distance -+ public final LevelChunk getSendingChunk() { -+ // it's important that we use getChunkAtIfLoadedImmediately to mirror the chunk sending logic used -+ // in Chunk's neighbour callback -+ LevelChunk ret = this.chunkMap.level.getChunkSource().getChunkAtIfLoadedImmediately(this.pos.x, this.pos.z); -+ if (ret != null && ret.areNeighboursLoaded(1)) { -+ return ret; -+ } -+ return null; -+ } -+ // Paper end - no-tick view distance - - // Paper start - public void onChunkAdd() { -@@ -81,147 +85,131 @@ public class ChunkHolder { - } - // Paper end - -- public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) { -- this.futures = new AtomicReferenceArray(ChunkHolder.CHUNK_STATUSES.size()); -- this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; -- this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; -- this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; -- this.chunkToSave = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error -+ public final io.papermc.paper.chunk.system.scheduling.NewChunkHolder newChunkHolder; // Paper - rewrite chunk system -+ -+ // Paper start - replace player chunk loader -+ private final com.destroystokyo.paper.util.maplist.ReferenceList playersSentChunkTo = new com.destroystokyo.paper.util.maplist.ReferenceList<>(); -+ -+ public void addPlayer(ServerPlayer player) { -+ if (!this.playersSentChunkTo.add(player)) { -+ throw new IllegalStateException("Already sent chunk " + this.pos + " in world '" + this.chunkMap.level.getWorld().getName() + "' to player " + player); -+ } -+ } -+ -+ public void removePlayer(ServerPlayer player) { -+ if (!this.playersSentChunkTo.remove(player)) { -+ throw new IllegalStateException("Have not sent chunk " + this.pos + " in world '" + this.chunkMap.level.getWorld().getName() + "' to player " + player); -+ } -+ } -+ -+ public boolean hasChunkBeenSent() { -+ return this.playersSentChunkTo.size() != 0; -+ } -+ -+ public boolean hasBeenSent(ServerPlayer to) { -+ return this.playersSentChunkTo.contains(to); -+ } -+ // Paper end - replace player chunk loader -+ public ChunkHolder(ChunkPos pos, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.PlayerProvider playersWatchingChunkProvider, io.papermc.paper.chunk.system.scheduling.NewChunkHolder newChunkHolder) { // Paper - rewrite chunk system -+ this.newChunkHolder = newChunkHolder; // Paper - rewrite chunk system - this.chunkToSaveHistory = null; - this.blockChangedLightSectionFilter = new BitSet(); - this.skyChangedLightSectionFilter = new BitSet(); -- this.pendingFullStateConfirmation = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error -- this.sendSync = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error -+ // Paper - rewrite chunk system - this.pos = pos; - this.levelHeightAccessor = world; - this.lightEngine = lightingProvider; -- this.onLevelChange = levelUpdateListener; -+ this.onLevelChange = null; // Paper - rewrite chunk system - this.playerProvider = playersWatchingChunkProvider; -- this.oldTicketLevel = ChunkLevel.MAX_LEVEL + 1; -- this.ticketLevel = this.oldTicketLevel; -- this.queueLevel = this.oldTicketLevel; -- this.setTicketLevel(level); -+ // Paper - rewrite chunk system - this.changedBlocksPerSection = new ShortSet[world.getSectionsCount()]; - this.chunkMap = (ChunkMap)playersWatchingChunkProvider; // Paper - } - - // Paper start - public @Nullable ChunkAccess getAvailableChunkNow() { -- // TODO can we just getStatusFuture(EMPTY)? -- for (ChunkStatus curr = ChunkStatus.FULL, next = curr.getParent(); curr != next; curr = next, next = next.getParent()) { -- CompletableFuture> future = this.getFutureIfPresentUnchecked(curr); -- ChunkResult either = future.getNow(null); -- if (either == null || either.isSuccess()) { -- continue; -- } -- return either.orElseThrow(IllegalStateException::new); -- } -- return null; -+ return this.newChunkHolder.getCurrentChunk(); // Paper - rewrite chunk system - } - // Paper end - // CraftBukkit start - public LevelChunk getFullChunkNow() { -- // Note: We use the oldTicketLevel for isLoaded checks. -- if (!ChunkLevel.fullStatus(this.oldTicketLevel).isOrAfter(FullChunkStatus.FULL)) return null; -- return this.getFullChunkNowUnchecked(); -+ // Paper start - rewrite chunk system -+ if (!this.isFullChunkReady() || !(this.getAvailableChunkNow() instanceof LevelChunk chunk)) return null; // instanceof to avoid a race condition on off-main threads -+ return chunk; -+ // Paper end - rewrite chunk system - } - - public LevelChunk getFullChunkNowUnchecked() { -- CompletableFuture> statusFuture = this.getFutureIfPresentUnchecked(ChunkStatus.FULL); -- ChunkResult either = statusFuture.getNow(null); -- return (either == null) ? null : (LevelChunk) either.orElse(null); -+ // Paper start - rewrite chunk system -+ return this.getAvailableChunkNow() instanceof LevelChunk chunk ? chunk : null; -+ // Paper end - rewrite chunk system - } - // CraftBukkit end - - public CompletableFuture> getFutureIfPresentUnchecked(ChunkStatus leastStatus) { -- CompletableFuture> completablefuture = (CompletableFuture) this.futures.get(leastStatus.getIndex()); -- -- return completablefuture == null ? ChunkHolder.UNLOADED_CHUNK_FUTURE : completablefuture; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public CompletableFuture> getFutureIfPresent(ChunkStatus leastStatus) { -- return ChunkLevel.generationStatus(this.ticketLevel).isOrAfter(leastStatus) ? this.getFutureIfPresentUnchecked(leastStatus) : ChunkHolder.UNLOADED_CHUNK_FUTURE; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public final CompletableFuture> getTickingChunkFuture() { // Paper - final for inline -- return this.tickingChunkFuture; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public final CompletableFuture> getEntityTickingChunkFuture() { // Paper - final for inline -- return this.entityTickingChunkFuture; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk systemv - } - - public final CompletableFuture> getFullChunkFuture() { // Paper - final for inline -- return this.fullChunkFuture; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk systemv - } - - @Nullable - public final LevelChunk getTickingChunk() { // Paper - final for inline -- return (LevelChunk) ((ChunkResult) this.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).orElse(null); // CraftBukkit - decompile error -+ // Paper start - rewrite chunk system -+ if (!this.isTickingReady()) { -+ return null; -+ } -+ return (LevelChunk)this.getAvailableChunkNow(); -+ // Paper end - rewrite chunk system - } - - public CompletableFuture getChunkSendSyncFuture() { -- return this.sendSync; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @Nullable - public LevelChunk getChunkToSend() { -- return !this.sendSync.isDone() ? null : this.getTickingChunk(); -+ return this.getSendingChunk(); // Paper - rewrite chunk system - } - - @Nullable - public ChunkStatus getLastAvailableStatus() { -- for (int i = ChunkHolder.CHUNK_STATUSES.size() - 1; i >= 0; --i) { -- ChunkStatus chunkstatus = (ChunkStatus) ChunkHolder.CHUNK_STATUSES.get(i); -- CompletableFuture> completablefuture = this.getFutureIfPresentUnchecked(chunkstatus); -- -- if (((ChunkResult) completablefuture.getNow(ChunkHolder.UNLOADED_CHUNK)).isSuccess()) { -- return chunkstatus; -- } -- } -- -- return null; -+ return this.newChunkHolder.getCurrentGenStatus(); // Paper - rewrite chunk system - } - - // Paper start - public @Nullable ChunkStatus getChunkHolderStatus() { -- for (ChunkStatus curr = ChunkStatus.FULL, next = curr.getParent(); curr != next; curr = next, next = next.getParent()) { -- CompletableFuture> future = this.getFutureIfPresentUnchecked(curr); -- ChunkResult either = future.getNow(null); -- if (either == null || !either.isSuccess()) { -- continue; -- } -- return curr; -- } -- -- return null; -+ return this.newChunkHolder.getCurrentGenStatus(); // Paper - rewrite chunk system - } - // Paper end - - @Nullable - public ChunkAccess getLastAvailable() { -- for (int i = ChunkHolder.CHUNK_STATUSES.size() - 1; i >= 0; --i) { -- ChunkStatus chunkstatus = (ChunkStatus) ChunkHolder.CHUNK_STATUSES.get(i); -- CompletableFuture> completablefuture = this.getFutureIfPresentUnchecked(chunkstatus); -- -- if (!completablefuture.isCompletedExceptionally()) { -- ChunkAccess ichunkaccess = (ChunkAccess) ((ChunkResult) completablefuture.getNow(ChunkHolder.UNLOADED_CHUNK)).orElse((Object) null); -- -- if (ichunkaccess != null) { -- return ichunkaccess; -- } -- } -- } -- -- return null; -+ return this.newChunkHolder.getCurrentChunk(); // Paper - rewrite chunk system - } - -- public final CompletableFuture getChunkToSave() { // Paper - final for inline -- return this.chunkToSave; -- } -+ // Paper - rewrite chunk system - - public void blockChanged(BlockPos pos) { -- LevelChunk chunk = this.getTickingChunk(); -+ // Paper start - replace player chunk loader -+ if (this.playersSentChunkTo.size() == 0) { -+ return; -+ } -+ // Paper end - replace player chunk loader -+ LevelChunk chunk = this.getSendingChunk(); // Paper - no-tick view distance - - if (chunk != null) { - int i = this.levelHeightAccessor.getSectionIndex(pos.getY()); -@@ -237,13 +225,13 @@ public class ChunkHolder { - } - - public void sectionLightChanged(LightLayer lightType, int y) { -- ChunkAccess ichunkaccess = (ChunkAccess) ((ChunkResult) this.getFutureIfPresent(ChunkStatus.INITIALIZE_LIGHT).getNow(ChunkHolder.UNLOADED_CHUNK)).orElse(null); // CraftBukkit - decompile error -+ ChunkAccess ichunkaccess = this.getAvailableChunkNow(); // Paper - rewrite chunk system - - if (ichunkaccess != null) { - ichunkaccess.setUnsaved(true); -- LevelChunk chunk = this.getTickingChunk(); -+ LevelChunk chunk = this.getSendingChunk(); // Paper - rewrite chunk system - -- if (chunk != null) { -+ if (this.playersSentChunkTo.size() != 0 && chunk != null) { // Paper - replace player chunk loader - int j = this.lightEngine.getMinLightSection(); - int k = this.lightEngine.getMaxLightSection(); - -@@ -263,7 +251,7 @@ public class ChunkHolder { - - // Paper start - starlight - public void broadcast(Packet packet, boolean onChunkViewEdge) { -- this.broadcast(this.playerProvider.getPlayers(this.pos, onChunkViewEdge), packet); -+ this.broadcast(this.getPlayers(onChunkViewEdge), packet); // Paper - rewrite chunk system - } - // Paper end - starlight - -@@ -273,7 +261,7 @@ public class ChunkHolder { - List list; - - if (!this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty()) { -- list = this.playerProvider.getPlayers(this.pos, true); -+ list = this.getPlayers(true); // Paper - rewrite chunk system - if (!list.isEmpty()) { - ClientboundLightUpdatePacket packetplayoutlightupdate = new ClientboundLightUpdatePacket(chunk.getPos(), this.lightEngine, this.skyChangedLightSectionFilter, this.blockChangedLightSectionFilter); - -@@ -285,7 +273,7 @@ public class ChunkHolder { - } - - if (this.hasChangedSections) { -- list = this.playerProvider.getPlayers(this.pos, false); -+ list = this.getPlayers(false); // Paper - rewrite chunk system - - for (int i = 0; i < this.changedBlocksPerSection.length; ++i) { - ShortSet shortset = this.changedBlocksPerSection[i]; -@@ -343,75 +331,33 @@ public class ChunkHolder { - - } - -- private void broadcast(List players, Packet packet) { -- players.forEach((entityplayer) -> { -- entityplayer.connection.send(packet); -- }); -- } -- -- public CompletableFuture> getOrScheduleFuture(ChunkStatus targetStatus, ChunkMap chunkStorage) { -- int i = targetStatus.getIndex(); -- CompletableFuture> completablefuture = (CompletableFuture) this.futures.get(i); -+ // Paper start - rewrite chunk system -+ public List getPlayers(boolean onlyOnWatchDistanceEdge) { -+ List ret = new java.util.ArrayList<>(); - -- if (completablefuture != null) { -- ChunkResult chunkresult = (ChunkResult) completablefuture.getNow(ChunkHolder.NOT_DONE_YET); -- -- if (chunkresult == null) { -- String s = String.valueOf(targetStatus); -- String s1 = "value in future for status: " + s + " was incorrectly set to null at chunk: " + String.valueOf(this.pos); -- -- throw chunkStorage.debugFuturesAndCreateReportedException(new IllegalStateException("null value previously set for chunk status"), s1); -- } -- -- if (chunkresult == ChunkHolder.NOT_DONE_YET || chunkresult.isSuccess()) { -- return completablefuture; -+ for (int i = 0, len = this.playersSentChunkTo.size(); i < len; ++i) { -+ ServerPlayer player = this.playersSentChunkTo.getUnchecked(i); -+ if (onlyOnWatchDistanceEdge && !this.chunkMap.level.playerChunkLoader.isChunkSent(player, this.pos.x, this.pos.z, onlyOnWatchDistanceEdge)) { -+ continue; - } -+ ret.add(player); - } - -- if (ChunkLevel.generationStatus(this.ticketLevel).isOrAfter(targetStatus)) { -- CompletableFuture> completablefuture1 = chunkStorage.schedule(this, targetStatus); -- -- this.updateChunkToSave(completablefuture1, "schedule " + String.valueOf(targetStatus)); -- this.futures.set(i, completablefuture1); -- return completablefuture1; -- } else { -- return completablefuture == null ? ChunkHolder.UNLOADED_CHUNK_FUTURE : completablefuture; -- } -+ return ret; - } -+ // Paper end - rewrite chunk system - -- protected void addSaveDependency(String thenDesc, CompletableFuture then) { -- if (this.chunkToSaveHistory != null) { -- this.chunkToSaveHistory.push(new ChunkHolder.ChunkSaveDebug(Thread.currentThread(), then, thenDesc)); -- } - -- this.chunkToSave = this.chunkToSave.thenCombine(then, (ichunkaccess, object) -> { -- return ichunkaccess; -- }); -- } -- -- private void updateChunkToSave(CompletableFuture> then, String thenDesc) { -- if (this.chunkToSaveHistory != null) { -- this.chunkToSaveHistory.push(new ChunkHolder.ChunkSaveDebug(Thread.currentThread(), then, thenDesc)); -- } -- -- this.chunkToSave = this.chunkToSave.thenCombine(then, (ichunkaccess, chunkresult) -> { -- return (ChunkAccess) ChunkResult.orElse(chunkresult, ichunkaccess); -+ private void broadcast(List players, Packet packet) { -+ players.forEach((entityplayer) -> { -+ entityplayer.connection.send(packet); - }); - } - -- public void addSendDependency(CompletableFuture postProcessingFuture) { -- if (this.sendSync.isDone()) { -- this.sendSync = postProcessingFuture; -- } else { -- this.sendSync = this.sendSync.thenCombine(postProcessingFuture, (object, object1) -> { -- return null; -- }); -- } -- -- } -+ // Paper - rewrite chunk system - - public FullChunkStatus getFullStatus() { -- return ChunkLevel.fullStatus(this.ticketLevel); -+ return this.newChunkHolder.getChunkStatus(); // Paper - rewrite chunk system - } - - public final ChunkPos getPos() { // Paper - final for inline -@@ -419,238 +365,17 @@ public class ChunkHolder { - } - - public final int getTicketLevel() { // Paper - final for inline -- return this.ticketLevel; -- } -- -- public int getQueueLevel() { -- return this.queueLevel; -- } -- -- private void setQueueLevel(int level) { -- this.queueLevel = level; -- } -- -- public void setTicketLevel(int level) { -- this.ticketLevel = level; -- } -- -- private void scheduleFullChunkPromotion(ChunkMap playerchunkmap, CompletableFuture> completablefuture, Executor executor, FullChunkStatus fullchunkstatus) { -- this.pendingFullStateConfirmation.cancel(false); -- CompletableFuture completablefuture1 = new CompletableFuture(); -- -- completablefuture1.thenRunAsync(() -> { -- playerchunkmap.onFullChunkStatusChange(this.pos, fullchunkstatus); -- }, executor); -- this.pendingFullStateConfirmation = completablefuture1; -- completablefuture.thenAccept((chunkresult) -> { -- chunkresult.ifSuccess((chunk) -> { -- completablefuture1.complete(null); // CraftBukkit - decompile error -- }); -- }); -- } -- -- private void demoteFullChunk(ChunkMap playerchunkmap, FullChunkStatus fullchunkstatus) { -- this.pendingFullStateConfirmation.cancel(false); -- playerchunkmap.onFullChunkStatusChange(this.pos, fullchunkstatus); -- } -- -- protected void updateFutures(ChunkMap chunkStorage, Executor executor) { -- ChunkStatus chunkstatus = ChunkLevel.generationStatus(this.oldTicketLevel); -- ChunkStatus chunkstatus1 = ChunkLevel.generationStatus(this.ticketLevel); -- boolean flag = ChunkLevel.isLoaded(this.oldTicketLevel); -- boolean flag1 = ChunkLevel.isLoaded(this.ticketLevel); -- FullChunkStatus fullchunkstatus = ChunkLevel.fullStatus(this.oldTicketLevel); -- FullChunkStatus fullchunkstatus1 = ChunkLevel.fullStatus(this.ticketLevel); -- // CraftBukkit start -- // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins. -- if (fullchunkstatus.isOrAfter(FullChunkStatus.FULL) && !fullchunkstatus1.isOrAfter(FullChunkStatus.FULL)) { -- this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> { -- LevelChunk chunk = (LevelChunk) either.orElse(null); -- if (chunk != null) { -- chunkStorage.callbackExecutor.execute(() -> { -- // Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick -- // lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag. -- // These actions may however happen deferred, so we manually set the needsSaving flag already here. -- chunk.setUnsaved(true); -- chunk.unloadCallback(); -- }); -- } -- }).exceptionally((throwable) -> { -- // ensure exceptions are printed, by default this is not the case -- MinecraftServer.LOGGER.error("Failed to schedule unload callback for chunk " + ChunkHolder.this.pos, throwable); -- return null; -- }); -- -- // Run callback right away if the future was already done -- chunkStorage.callbackExecutor.run(); -- } -- // CraftBukkit end -- -- if (flag) { -- ChunkResult chunkresult = ChunkResult.error(() -> { -- return "Unloaded ticket level " + String.valueOf(this.pos); -- }); -- -- for (int i = flag1 ? chunkstatus1.getIndex() + 1 : 0; i <= chunkstatus.getIndex(); ++i) { -- CompletableFuture> completablefuture = (CompletableFuture) this.futures.get(i); -- -- if (completablefuture == null) { -- this.futures.set(i, CompletableFuture.completedFuture(chunkresult)); -- } -- } -- } -- -- boolean flag2 = fullchunkstatus.isOrAfter(FullChunkStatus.FULL); -- boolean flag3 = fullchunkstatus1.isOrAfter(FullChunkStatus.FULL); -- -- this.wasAccessibleSinceLastSave |= flag3; -- if (!flag2 && flag3) { -- int expectCreateCount = ++this.fullChunkCreateCount; // Paper -- this.fullChunkFuture = chunkStorage.prepareAccessibleChunk(this); -- this.scheduleFullChunkPromotion(chunkStorage, this.fullChunkFuture, executor, FullChunkStatus.FULL); -- // Paper start - cache ticking ready status -- this.fullChunkFuture.thenAccept(chunkResult -> { -- chunkResult.ifSuccess(chunk -> { -- if (ChunkHolder.this.fullChunkCreateCount == expectCreateCount) { -- ChunkHolder.this.isFullChunkReady = true; -- io.papermc.paper.chunk.system.ChunkSystem.onChunkBorder(chunk, this); -- } -- }); -- }); -- this.updateChunkToSave(this.fullChunkFuture, "full"); -- } -- -- if (flag2 && !flag3) { -- // Paper start -- if (this.isFullChunkReady) { -- io.papermc.paper.chunk.system.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper -- } -- // Paper end -- this.fullChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); -- this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; -- ++this.fullChunkCreateCount; // Paper - cache ticking ready status -- this.isFullChunkReady = false; // Paper - cache ticking ready status -- } -- -- boolean flag4 = fullchunkstatus.isOrAfter(FullChunkStatus.BLOCK_TICKING); -- boolean flag5 = fullchunkstatus1.isOrAfter(FullChunkStatus.BLOCK_TICKING); -- -- if (!flag4 && flag5) { -- this.tickingChunkFuture = chunkStorage.prepareTickingChunk(this); -- this.scheduleFullChunkPromotion(chunkStorage, this.tickingChunkFuture, executor, FullChunkStatus.BLOCK_TICKING); -- // Paper start - cache ticking ready status -- this.tickingChunkFuture.thenAccept(chunkResult -> { -- chunkResult.ifSuccess(chunk -> { -- // note: Here is a very good place to add callbacks to logic waiting on this. -- ChunkHolder.this.isTickingReady = true; -- io.papermc.paper.chunk.system.ChunkSystem.onChunkTicking(chunk, this); -- }); -- }); -- // Paper end -- this.updateChunkToSave(this.tickingChunkFuture, "ticking"); -- } -- -- if (flag4 && !flag5) { -- // Paper start -- if (this.isTickingReady) { -- io.papermc.paper.chunk.system.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper -- } -- // Paper end -- this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage -- this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; -- } -- -- boolean flag6 = fullchunkstatus.isOrAfter(FullChunkStatus.ENTITY_TICKING); -- boolean flag7 = fullchunkstatus1.isOrAfter(FullChunkStatus.ENTITY_TICKING); -- -- if (!flag6 && flag7) { -- if (this.entityTickingChunkFuture != ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE) { -- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException()); -- } -- -- this.entityTickingChunkFuture = chunkStorage.prepareEntityTickingChunk(this); -- this.scheduleFullChunkPromotion(chunkStorage, this.entityTickingChunkFuture, executor, FullChunkStatus.ENTITY_TICKING); -- // Paper start - cache ticking ready status -- this.entityTickingChunkFuture.thenAccept(chunkResult -> { -- chunkResult.ifSuccess(chunk -> { -- ChunkHolder.this.isEntityTickingReady = true; -- io.papermc.paper.chunk.system.ChunkSystem.onChunkEntityTicking(chunk, this); -- }); -- }); -- // Paper end -- this.updateChunkToSave(this.entityTickingChunkFuture, "entity ticking"); -- } -- -- if (flag6 && !flag7) { -- // Paper start -- if (this.isEntityTickingReady) { -- io.papermc.paper.chunk.system.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); -- } -- // Paper end -- this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage -- this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; -- } -- -- if (!fullchunkstatus1.isOrAfter(fullchunkstatus)) { -- this.demoteFullChunk(chunkStorage, fullchunkstatus1); -- } -- -- this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel); -- this.oldTicketLevel = this.ticketLevel; -- // CraftBukkit start -- // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins. -- if (!fullchunkstatus.isOrAfter(FullChunkStatus.FULL) && fullchunkstatus1.isOrAfter(FullChunkStatus.FULL)) { -- this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> { -- LevelChunk chunk = (LevelChunk) either.orElse(null); -- if (chunk != null) { -- chunkStorage.callbackExecutor.execute(() -> { -- chunk.loadCallback(); -- }); -- } -- }).exceptionally((throwable) -> { -- // ensure exceptions are printed, by default this is not the case -- MinecraftServer.LOGGER.error("Failed to schedule load callback for chunk " + ChunkHolder.this.pos, throwable); -- return null; -- }); -- -- // Run callback right away if the future was already done -- chunkStorage.callbackExecutor.run(); -- } -- // CraftBukkit end -- } -- -- public boolean wasAccessibleSinceLastSave() { -- return this.wasAccessibleSinceLastSave; -+ return this.newChunkHolder.getTicketLevel(); // Paper - rewrite chunk system - } - -- public void refreshAccessibility() { -- this.wasAccessibleSinceLastSave = ChunkLevel.fullStatus(this.ticketLevel).isOrAfter(FullChunkStatus.FULL); -- } -+ // Paper - rewrite chunk system - - public void replaceProtoChunk(ImposterProtoChunk chunk) { -- for (int i = 0; i < this.futures.length(); ++i) { -- CompletableFuture> completablefuture = (CompletableFuture) this.futures.get(i); -- -- if (completablefuture != null) { -- ChunkAccess ichunkaccess = (ChunkAccess) ((ChunkResult) completablefuture.getNow(ChunkHolder.UNLOADED_CHUNK)).orElse((Object) null); -- -- if (ichunkaccess instanceof ProtoChunk) { -- this.futures.set(i, CompletableFuture.completedFuture(ChunkResult.of(chunk))); -- } -- } -- } -- -- this.updateChunkToSave(CompletableFuture.completedFuture(ChunkResult.of(chunk.getWrapped())), "replaceProto"); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public List>>> getAllFutures() { -- List>>> list = new ArrayList(); -- -- for (int i = 0; i < ChunkHolder.CHUNK_STATUSES.size(); ++i) { -- list.add(Pair.of((ChunkStatus) ChunkHolder.CHUNK_STATUSES.get(i), (CompletableFuture) this.futures.get(i))); -- } -- -- return list; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @FunctionalInterface -@@ -670,15 +395,15 @@ public class ChunkHolder { - - // Paper start - public final boolean isEntityTickingReady() { -- return this.isEntityTickingReady; -+ return this.newChunkHolder.isEntityTickingReady(); // Paper - rewrite chunk system - } - - public final boolean isTickingReady() { -- return this.isTickingReady; -+ return this.newChunkHolder.isTickingReady(); // Paper - rewrite chunk system - } - - public final boolean isFullChunkReady() { -- return this.isFullChunkReady; -+ return this.newChunkHolder.isFullChunkReady(); // Paper - rewrite chunk system - } - // Paper end - } -diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java -index d3f63185edd1db9fab3887ea3f08982435b3a23c..d6ecee1db17cb9eaeffa94b3d8dd150238fdefe5 100644 ---- a/src/main/java/net/minecraft/server/level/ChunkMap.java -+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java -@@ -122,10 +122,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - public static final int MIN_VIEW_DISTANCE = 2; - public static final int MAX_VIEW_DISTANCE = 32; - public static final int FORCED_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING); -- public final Long2ObjectLinkedOpenHashMap updatingChunkMap = new Long2ObjectLinkedOpenHashMap(); -- public volatile Long2ObjectLinkedOpenHashMap visibleChunkMap; -- private final Long2ObjectLinkedOpenHashMap pendingUnloads; -- private final LongSet entitiesInLevel; -+ // Paper - rewrite chunk system - public final ServerLevel level; - private final ThreadedLevelLightEngine lightEngine; - public final BlockableEventLoop mainThreadExecutor; // Paper - public -@@ -134,15 +131,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - private final ChunkGeneratorStructureState chunkGeneratorState; - public final Supplier overworldDataStorage; - private final PoiManager poiManager; -- public final LongSet toDrop; -+ // Paper - rewrite chunk system - private boolean modified; -- private final ChunkTaskPriorityQueueSorter queueSorter; -- private final ProcessorHandle> worldgenMailbox; -- private final ProcessorHandle> mainThreadMailbox; -+ // Paper - rewrite chunk system - public final ChunkProgressListener progressListener; - private final ChunkStatusUpdateListener chunkStatusListener; - public final ChunkMap.ChunkDistanceManager distanceManager; -- private final AtomicInteger tickingGenerated; -+ public final AtomicInteger tickingGenerated; // Paper - public - private final String storageName; - private final PlayerMap playerMap; - public final Int2ObjectMap entityMap; -@@ -150,28 +145,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - private final Long2LongMap chunkSaveCooldowns; - private final Queue unloadQueue; - public int serverViewDistance; -- private WorldGenContext worldGenContext; -- -- // 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 { -- -- private final java.util.Queue queue = new java.util.ArrayDeque<>(); -- -- @Override -- public void execute(Runnable runnable) { -- this.queue.add(runnable); -- } -- -- @Override -- public void run() { -- Runnable task; -- while ((task = this.queue.poll()) != null) { -- task.run(); -- } -- } -- }; -- // CraftBukkit end -+ private WorldGenContext worldGenContext; public final WorldGenContext getWorldGenContext() { return this.worldGenContext; } // Paper - rewrite chunk system - - // Paper start - distance maps - private final com.destroystokyo.paper.util.misc.PooledLinkedHashSets pooledLinkedPlayerHashSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>(); -@@ -181,6 +155,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ()); - // Note: players need to be explicitly added to distance maps before they can be updated - this.nearbyPlayers.addPlayer(player); -+ this.level.playerChunkLoader.addPlayer(player); // Paper - replace chunk loader - } - - void removePlayerFromDistanceMaps(ServerPlayer player) { -@@ -188,6 +163,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ()); - // Note: players need to be explicitly added to distance maps before they can be updated - this.nearbyPlayers.removePlayer(player); -+ this.level.playerChunkLoader.removePlayer(player); // Paper - replace chunk loader - } - - void updateMaps(ServerPlayer player) { -@@ -195,6 +171,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - int chunkZ = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getZ()); - // Note: players need to be explicitly added to distance maps before they can be updated - this.nearbyPlayers.tickPlayer(player); -+ this.level.playerChunkLoader.updatePlayer(player); // Paper - replace chunk loader - } - // Paper end - // Paper start -@@ -224,17 +201,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) { -- return this.pendingUnloads.get(io.papermc.paper.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); -+ return null; // Paper - rewrite chunk system - } - public final io.papermc.paper.util.player.NearbyPlayers nearbyPlayers; - // Paper end - - public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory, int viewDistance, boolean dsync) { - super(new RegionStorageInfo(session.getLevelId(), world.dimension(), "chunk"), session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync); -- this.visibleChunkMap = this.updatingChunkMap.clone(); -- this.pendingUnloads = new Long2ObjectLinkedOpenHashMap(); -- this.entitiesInLevel = new LongOpenHashSet(); -- this.toDrop = new LongOpenHashSet(); -+ // Paper - rewrite chunk system - this.tickingGenerated = new AtomicInteger(); - this.playerMap = new PlayerMap(); - this.entityMap = new Int2ObjectOpenHashMap(); -@@ -263,19 +237,17 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - - this.chunkGeneratorState = chunkGenerator.createState(iregistrycustom.lookupOrThrow(Registries.STRUCTURE_SET), this.randomState, j, world.spigotConfig); // Spigot - this.mainThreadExecutor = mainThreadExecutor; -- ProcessorMailbox threadedmailbox = ProcessorMailbox.create(executor, "worldgen"); -+ // Paper - rewrite chunk system - - Objects.requireNonNull(mainThreadExecutor); -- ProcessorHandle mailbox = ProcessorHandle.of("main", mainThreadExecutor::tell); -+ // Paper - rewrite chunk system - - this.progressListener = worldGenerationProgressListener; - this.chunkStatusListener = chunkStatusChangeListener; -- ProcessorMailbox threadedmailbox1 = ProcessorMailbox.create(executor, "light"); -+ // Paper - rewrite chunk system - -- this.queueSorter = new ChunkTaskPriorityQueueSorter(ImmutableList.of(threadedmailbox, mailbox, threadedmailbox1), executor, Integer.MAX_VALUE); -- this.worldgenMailbox = this.queueSorter.getProcessor(threadedmailbox, false); -- this.mainThreadMailbox = this.queueSorter.getProcessor(mailbox, false); -- this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), threadedmailbox1, this.queueSorter.getProcessor(threadedmailbox1, false)); -+ // Paper - rewrite chunk system -+ this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), null, null); // Paper - rewrite chunk system - this.distanceManager = new ChunkMap.ChunkDistanceManager(executor, mainThreadExecutor); - this.overworldDataStorage = persistentStateManagerFactory; - this.poiManager = new PoiManager(new RegionStorageInfo(session.getLevelId(), world.dimension(), "poi"), path.resolve("poi"), dataFixer, dsync, iregistrycustom, world); -@@ -333,23 +305,15 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - boolean isChunkTracked(ServerPlayer player, int chunkX, int chunkZ) { -- return player.getChunkTrackingView().contains(chunkX, chunkZ) && !player.connection.chunkSender.isPending(ChunkPos.asLong(chunkX, chunkZ)); -+ // Paper start - rewrite player chunk loader -+ return this.level.playerChunkLoader.isChunkSent(player, chunkX, chunkZ); -+ // Paper end - rewrite player chunk loader - } - - private boolean isChunkOnTrackedBorder(ServerPlayer player, int chunkX, int chunkZ) { -- if (!this.isChunkTracked(player, chunkX, chunkZ)) { -- return false; -- } else { -- for (int k = -1; k <= 1; ++k) { -- for (int l = -1; l <= 1; ++l) { -- if ((k != 0 || l != 0) && !this.isChunkTracked(player, chunkX + k, chunkZ + l)) { -- return true; -- } -- } -- } -- -- return false; -- } -+ // Paper start - rewrite player chunk loader -+ return this.level.playerChunkLoader.isChunkSent(player, chunkX, chunkZ, true); -+ // Paper end - rewrite player chunk loader - } - - protected ThreadedLevelLightEngine getLightEngine() { -@@ -358,20 +322,22 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - - @Nullable - protected ChunkHolder getUpdatingChunkIfPresent(long pos) { -- return (ChunkHolder) this.updatingChunkMap.get(pos); -+ // Paper start - rewrite chunk system -+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder holder = this.level.chunkTaskScheduler.chunkHolderManager.getChunkHolder(pos); -+ return holder == null ? null : holder.vanillaChunkHolder; -+ // Paper end - rewrite chunk system - } - - @Nullable - public ChunkHolder getVisibleChunkIfPresent(long pos) { -- return (ChunkHolder) this.visibleChunkMap.get(pos); -+ // Paper start - rewrite chunk system -+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder holder = this.level.chunkTaskScheduler.chunkHolderManager.getChunkHolder(pos); -+ return holder == null ? null : holder.vanillaChunkHolder; -+ // Paper end - rewrite chunk system - } - - protected IntSupplier getChunkQueueLevel(long pos) { -- return () -> { -- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); -- -- return playerchunk == null ? ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1 : Math.min(playerchunk.getQueueLevel(), ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1); -- }; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public String getChunkDebugData(ChunkPos chunkPos) { -@@ -400,80 +366,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - private CompletableFuture>> getChunkRangeFuture(ChunkHolder centerChunk, int margin, IntFunction distanceToStatus) { -- if (margin == 0) { -- ChunkStatus chunkstatus = (ChunkStatus) distanceToStatus.apply(0); -- -- return centerChunk.getOrScheduleFuture(chunkstatus, this).thenApply((chunkresult) -> { -- return chunkresult.map(List::of); -- }); -- } else { -- List>> list = new ArrayList(); -- List list1 = new ArrayList(); -- ChunkPos chunkcoordintpair = centerChunk.getPos(); -- int j = chunkcoordintpair.x; -- int k = chunkcoordintpair.z; -- -- for (int l = -margin; l <= margin; ++l) { -- for (int i1 = -margin; i1 <= margin; ++i1) { -- int j1 = Math.max(Math.abs(i1), Math.abs(l)); -- ChunkPos chunkcoordintpair1 = new ChunkPos(j + i1, k + l); -- long k1 = chunkcoordintpair1.toLong(); -- ChunkHolder playerchunk1 = this.getUpdatingChunkIfPresent(k1); -- -- if (playerchunk1 == null) { -- return CompletableFuture.completedFuture(ChunkResult.error(() -> { -- return "Unloaded " + String.valueOf(chunkcoordintpair1); -- })); -- } -- -- ChunkStatus chunkstatus1 = (ChunkStatus) distanceToStatus.apply(j1); -- CompletableFuture> completablefuture = playerchunk1.getOrScheduleFuture(chunkstatus1, this); -- -- list1.add(playerchunk1); -- list.add(completablefuture); -- } -- } -- -- CompletableFuture>> completablefuture1 = Util.sequence(list); -- CompletableFuture>> completablefuture2 = completablefuture1.thenApply((list2) -> { -- List list3 = Lists.newArrayList(); -- // CraftBukkit start - decompile error -- int cnt = 0; -- -- for (Iterator iterator = list2.iterator(); iterator.hasNext(); ++cnt) { -- final int l1 = cnt; -- // CraftBukkit end -- ChunkResult chunkresult = (ChunkResult) iterator.next(); -- -- if (chunkresult == null) { -- throw this.debugFuturesAndCreateReportedException(new IllegalStateException("At least one of the chunk futures were null"), "n/a"); -- } -- -- ChunkAccess ichunkaccess = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error -- -- if (ichunkaccess == null) { -- return ChunkResult.error(() -> { -- String s = String.valueOf(new ChunkPos(j + l1 % (margin * 2 + 1), k + l1 / (margin * 2 + 1))); -- -- return "Unloaded " + s + " " + chunkresult.getError(); -- }); -- } -- -- list3.add(ichunkaccess); -- } -- -- return ChunkResult.of(list3); -- }); -- Iterator iterator = list1.iterator(); -- -- while (iterator.hasNext()) { -- ChunkHolder playerchunk2 = (ChunkHolder) iterator.next(); -- -- playerchunk2.addSaveDependency("getChunkRangeFuture " + String.valueOf(chunkcoordintpair) + " " + margin, completablefuture2); -- } -- -- return completablefuture2; -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public ReportedException debugFuturesAndCreateReportedException(IllegalStateException exception, String details) { -@@ -503,263 +396,72 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - public CompletableFuture> prepareEntityTickingChunk(ChunkHolder chunk) { -- return this.getChunkRangeFuture(chunk, 2, (i) -> { -- return ChunkStatus.FULL; -- }).thenApplyAsync((chunkresult) -> { -- return chunkresult.map((list) -> { -- return (LevelChunk) list.get(list.size() / 2); -- }); -- }, this.mainThreadExecutor); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @Nullable - ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k) { -- if (!ChunkLevel.isLoaded(k) && !ChunkLevel.isLoaded(level)) { -- return holder; -- } else { -- if (holder != null) { -- holder.setTicketLevel(level); -- } -- -- if (holder != null) { -- if (!ChunkLevel.isLoaded(level)) { -- this.toDrop.add(pos); -- } else { -- this.toDrop.remove(pos); -- } -- } -- -- if (ChunkLevel.isLoaded(level) && holder == null) { -- holder = (ChunkHolder) this.pendingUnloads.remove(pos); -- if (holder != null) { -- holder.setTicketLevel(level); -- } else { -- holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this.queueSorter, this); -- // Paper start -- io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderCreate(this.level, holder); -- // Paper end -- } -- -- // Paper start -- holder.onChunkAdd(); -- // Paper end -- this.updatingChunkMap.put(pos, holder); -- this.modified = true; -- } -- -- return holder; -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @Override - public void close() throws IOException { -- try { -- this.queueSorter.close(); -- this.poiManager.close(); -- } finally { -- super.close(); -- } -+ throw new UnsupportedOperationException("Use ServerChunkCache#close"); // Paper - rewrite chunk system -+ } - -+ // Paper start - rewrite chunk system -+ protected void saveIncrementally() { -+ this.level.chunkTaskScheduler.chunkHolderManager.autoSave(); // Paper - rewrite chunk system - } -+ // Paper end - - rewrite chunk system - - protected void saveAllChunks(boolean flush) { -- if (flush) { -- List list = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList(); // Paper -- MutableBoolean mutableboolean = new MutableBoolean(); -- -- do { -- mutableboolean.setFalse(); -- list.stream().map((playerchunk) -> { -- CompletableFuture completablefuture; -- -- do { -- completablefuture = playerchunk.getChunkToSave(); -- BlockableEventLoop iasynctaskhandler = this.mainThreadExecutor; -- -- Objects.requireNonNull(completablefuture); -- iasynctaskhandler.managedBlock(completablefuture::isDone); -- } while (completablefuture != playerchunk.getChunkToSave()); -- -- return (ChunkAccess) completablefuture.join(); -- }).filter((ichunkaccess) -> { -- return ichunkaccess instanceof ImposterProtoChunk || ichunkaccess instanceof LevelChunk; -- }).filter(this::save).forEach((ichunkaccess) -> { -- mutableboolean.setTrue(); -- }); -- } while (mutableboolean.isTrue()); -- -- this.processUnloads(() -> { -- return true; -- }); -- this.flushWorker(); -- } else { -- io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).forEach(this::saveChunkIfNeeded); -- } -- -+ this.level.chunkTaskScheduler.chunkHolderManager.saveAllChunks(flush, false, false); // Paper - rewrite chunk system - } - - protected void tick(BooleanSupplier shouldKeepTicking) { - ProfilerFiller gameprofilerfiller = this.level.getProfiler(); - -+ try (Timing ignored = this.level.timings.poiUnload.startTiming()) { // Paper - gameprofilerfiller.push("poi"); - this.poiManager.tick(shouldKeepTicking); -+ } // Paper - gameprofilerfiller.popPush("chunk_unload"); - if (!this.level.noSave()) { -+ try (Timing ignored = this.level.timings.chunkUnload.startTiming()) { // Paper - this.processUnloads(shouldKeepTicking); -+ } // Paper - } - - gameprofilerfiller.pop(); - } - - public boolean hasWork() { -- return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || io.papermc.paper.chunk.system.ChunkSystem.hasAnyChunkHolders(this.level) || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets(); // Paper -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private void processUnloads(BooleanSupplier shouldKeepTicking) { -- LongIterator longiterator = this.toDrop.iterator(); -- -- for (int i = 0; longiterator.hasNext() && (shouldKeepTicking.getAsBoolean() || i < 200 || this.toDrop.size() > 2000); longiterator.remove()) { -- long j = longiterator.nextLong(); -- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.remove(j); -- -- if (playerchunk != null) { -- playerchunk.onChunkRemove(); // Paper -- this.pendingUnloads.put(j, playerchunk); -- this.modified = true; -- ++i; -- this.scheduleUnload(j, playerchunk); -- } -- } -- -- int k = Math.max(0, this.unloadQueue.size() - 2000); -- -- Runnable runnable; -- -- while ((shouldKeepTicking.getAsBoolean() || k > 0) && (runnable = (Runnable) this.unloadQueue.poll()) != null) { -- --k; -- runnable.run(); -- } -- -- int l = 0; -- Iterator objectiterator = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper -- -- while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) { -- if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) { -- ++l; -- } -- } -+ this.level.chunkTaskScheduler.chunkHolderManager.processUnloads(); // Paper - rewrite chunk system - - } - - private void scheduleUnload(long pos, ChunkHolder holder) { -- CompletableFuture completablefuture = holder.getChunkToSave(); -- Consumer consumer = (ichunkaccess) -> { // CraftBukkit - decompile error -- CompletableFuture completablefuture1 = holder.getChunkToSave(); -- -- if (completablefuture1 != completablefuture) { -- this.scheduleUnload(pos, holder); -- } else { -- // Paper start -- boolean removed; -- if ((removed = this.pendingUnloads.remove(pos, holder)) && ichunkaccess != null) { -- io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(this.level, holder); -- // Paper end -- if (ichunkaccess instanceof LevelChunk) { -- ((LevelChunk) ichunkaccess).setLoaded(false); -- } -- -- this.save(ichunkaccess); -- if (this.entitiesInLevel.remove(pos) && ichunkaccess instanceof LevelChunk) { -- LevelChunk chunk = (LevelChunk) ichunkaccess; -- -- this.level.unload(chunk); -- } -- -- this.lightEngine.updateChunkStatus(ichunkaccess.getPos()); -- this.lightEngine.tryScheduleUpdate(); -- this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null); -- this.chunkSaveCooldowns.remove(ichunkaccess.getPos().toLong()); -- } else if (removed) { // Paper start -- io.papermc.paper.chunk.system.ChunkSystem.onChunkHolderDelete(this.level, holder); -- } // Paper end -- -- } -- }; -- Queue queue = this.unloadQueue; -- -- Objects.requireNonNull(this.unloadQueue); -- completablefuture.thenAcceptAsync(consumer, queue::add).whenComplete((ovoid, throwable) -> { -- if (throwable != null) { -- ChunkMap.LOGGER.error("Failed to save chunk {}", holder.getPos(), throwable); -- } -- -- }); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - protected boolean promoteChunkMap() { -- if (!this.modified) { -- return false; -- } else { -- this.visibleChunkMap = this.updatingChunkMap.clone(); -- this.modified = false; -- return true; -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public CompletableFuture> schedule(ChunkHolder holder, ChunkStatus requiredStatus) { -- ChunkPos chunkcoordintpair = holder.getPos(); -- -- if (requiredStatus == ChunkStatus.EMPTY) { -- return this.scheduleChunkLoad(chunkcoordintpair).thenApply(ChunkResult::of); -- } else { -- if (requiredStatus == ChunkStatus.LIGHT) { -- this.distanceManager.addTicket(TicketType.LIGHT, chunkcoordintpair, ChunkLevel.byStatus(ChunkStatus.LIGHT), chunkcoordintpair); -- } -- -- if (!requiredStatus.hasLoadDependencies()) { -- ChunkAccess ichunkaccess = (ChunkAccess) ((ChunkResult) holder.getOrScheduleFuture(requiredStatus.getParent(), this).getNow(ChunkHolder.UNLOADED_CHUNK)).orElse((Object) null); -- -- if (ichunkaccess != null && ichunkaccess.getStatus().isOrAfter(requiredStatus)) { -- CompletableFuture completablefuture = requiredStatus.load(this.worldGenContext, (ichunkaccess1) -> { -- return this.protoChunkToFullChunk(holder, ichunkaccess1); -- }, ichunkaccess); -- -- this.progressListener.onStatusChange(chunkcoordintpair, requiredStatus); -- return completablefuture.thenApply(ChunkResult::of); -- } -- } -- -- return this.scheduleChunkGeneration(holder, requiredStatus); -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private CompletableFuture scheduleChunkLoad(ChunkPos pos) { -- return this.readChunk(pos).thenApply((optional) -> { -- return optional.filter((nbttagcompound) -> { -- boolean flag = ChunkMap.isChunkDataValid(nbttagcompound); -- -- if (!flag) { -- ChunkMap.LOGGER.error("Chunk file at {} is missing level data, skipping", pos); -- } -- -- return flag; -- }); -- }).thenApplyAsync((optional) -> { -- this.level.getProfiler().incrementCounter("chunkLoad"); -- if (optional.isPresent()) { -- ProtoChunk protochunk = ChunkSerializer.read(this.level, this.poiManager, pos, (CompoundTag) optional.get()); -- -- this.markPosition(pos, protochunk.getStatus().getChunkType()); -- return protochunk; -- } else { -- return this.createEmptyChunk(pos); -- } -- }, this.mainThreadExecutor).exceptionallyAsync((throwable) -> { -- return this.handleChunkLoadFailure(throwable, pos); -- }, this.mainThreadExecutor); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - -- private static boolean isChunkDataValid(CompoundTag nbt) { -+ public static boolean isChunkDataValid(CompoundTag nbt) { // Paper - async chunk loading - return nbt.contains("Status", 8); - } - -@@ -816,60 +518,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - private CompletableFuture> scheduleChunkGeneration(ChunkHolder holder, ChunkStatus requiredStatus) { -- ChunkPos chunkcoordintpair = holder.getPos(); -- CompletableFuture>> completablefuture = this.getChunkRangeFuture(holder, requiredStatus.getRange(), (i) -> { -- return this.getDependencyStatus(requiredStatus, i); -- }); -- -- this.level.getProfiler().incrementCounter(() -> { -- return "chunkGenerate " + String.valueOf(requiredStatus); -- }); -- Executor executor = (runnable) -> { -- this.worldgenMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable)); -- }; -- -- return completablefuture.thenComposeAsync((chunkresult) -> { -- List list = (List) chunkresult.orElse(null); // CraftBukkit - decompile error -- -- if (list == null) { -- this.releaseLightTicket(chunkcoordintpair); -- Objects.requireNonNull(chunkresult); -- return CompletableFuture.completedFuture(ChunkResult.error(chunkresult::getError)); -- } else { -- try { -- ChunkAccess ichunkaccess = (ChunkAccess) list.get(list.size() / 2); -- CompletableFuture completablefuture1; -- -- if (ichunkaccess.getStatus().isOrAfter(requiredStatus)) { -- completablefuture1 = requiredStatus.load(this.worldGenContext, (ichunkaccess1) -> { -- return this.protoChunkToFullChunk(holder, ichunkaccess1); -- }, ichunkaccess); -- } else { -- completablefuture1 = requiredStatus.generate(this.worldGenContext, executor, (ichunkaccess1) -> { -- return this.protoChunkToFullChunk(holder, ichunkaccess1); -- }, list); -- } -- -- this.progressListener.onStatusChange(chunkcoordintpair, requiredStatus); -- return completablefuture1.thenApply(ChunkResult::of); -- } catch (Exception exception) { -- exception.getStackTrace(); -- CrashReport crashreport = CrashReport.forThrowable(exception, "Exception generating new chunk"); -- CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk to be generated"); -- -- crashreportsystemdetails.setDetail("Status being generated", () -> { -- return BuiltInRegistries.CHUNK_STATUS.getKey(requiredStatus).toString(); -- }); -- crashreportsystemdetails.setDetail("Location", (Object) String.format(Locale.ROOT, "%d,%d", chunkcoordintpair.x, chunkcoordintpair.z)); -- crashreportsystemdetails.setDetail("Position hash", (Object) ChunkPos.asLong(chunkcoordintpair.x, chunkcoordintpair.z)); -- crashreportsystemdetails.setDetail("Generator", (Object) this.generator); -- this.mainThreadExecutor.execute(() -> { -- throw new ReportedException(crashreport); -- }); -- throw new ReportedException(crashreport); -- } -- } -- }, executor); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - protected void releaseLightTicket(ChunkPos pos) { -@@ -880,7 +529,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - })); - } - -- private ChunkStatus getDependencyStatus(ChunkStatus centerChunkTargetStatus, int distance) { -+ public static ChunkStatus getDependencyStatus(ChunkStatus centerChunkTargetStatus, int distance) { // Paper -> public, static - ChunkStatus chunkstatus1; - - if (distance == 0) { -@@ -892,7 +541,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - return chunkstatus1; - } - -- private static void postLoadProtoChunk(ServerLevel world, List nbt) { -+ public static void postLoadProtoChunk(ServerLevel world, List nbt, ChunkPos position) { // Paper - public and add chunk position parameter - if (!nbt.isEmpty()) { - // CraftBukkit start - these are spawned serialized (DefinedStructure) and we don't call an add event below at the moment due to ordering complexities - world.addWorldGenChunkEntities(EntityType.loadEntitiesRecursive(nbt, world).filter((entity) -> { -@@ -908,45 +557,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - checkDupeUUID(world, entity); // Paper - duplicate uuid resolving - return !needsRemoval; -- })); -+ }), position); // Paper - rewrite chunk system - // CraftBukkit end - } - - } - - private CompletableFuture protoChunkToFullChunk(ChunkHolder playerchunk, ChunkAccess ichunkaccess) { -- return CompletableFuture.supplyAsync(() -> { -- ChunkPos chunkcoordintpair = playerchunk.getPos(); -- ProtoChunk protochunk = (ProtoChunk) ichunkaccess; -- LevelChunk chunk; -- -- if (protochunk instanceof ImposterProtoChunk) { -- chunk = ((ImposterProtoChunk) protochunk).getWrapped(); -- } else { -- chunk = new LevelChunk(this.level, protochunk, (chunk1) -> { -- ChunkMap.postLoadProtoChunk(this.level, protochunk.getEntities()); -- }); -- playerchunk.replaceProtoChunk(new ImposterProtoChunk(chunk, false)); -- } -- -- chunk.setFullStatus(() -> { -- return ChunkLevel.fullStatus(playerchunk.getTicketLevel()); -- }); -- chunk.runPostLoad(); -- if (this.entitiesInLevel.add(chunkcoordintpair.toLong())) { -- chunk.setLoaded(true); -- chunk.registerAllBlockEntitiesAfterLevelLoad(); -- chunk.registerTickContainerInLevel(this.level); -- } -- -- return chunk; -- }, (runnable) -> { -- ProcessorHandle mailbox = this.mainThreadMailbox; -- long i = playerchunk.getPos().toLong(); -- -- Objects.requireNonNull(playerchunk); -- mailbox.tell(ChunkTaskPriorityQueueSorter.message(runnable, i, playerchunk::getTicketLevel)); -- }); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - // Paper start - duplicate uuid resolving -@@ -990,61 +608,16 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - // Paper end - duplicate uuid resolving - - public CompletableFuture> prepareTickingChunk(ChunkHolder holder) { -- CompletableFuture>> completablefuture = this.getChunkRangeFuture(holder, 1, (i) -> { -- return ChunkStatus.FULL; -- }); -- CompletableFuture> completablefuture1 = completablefuture.thenApplyAsync((chunkresult) -> { -- return chunkresult.map((list) -> { -- return (LevelChunk) list.get(list.size() / 2); -- }); -- }, (runnable) -> { -- this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable)); -- }).thenApplyAsync((chunkresult) -> { -- return chunkresult.ifSuccess((chunk) -> { -- chunk.postProcessGeneration(); -- this.level.startTickingChunk(chunk); -- CompletableFuture completablefuture2 = holder.getChunkSendSyncFuture(); -- -- if (completablefuture2.isDone()) { -- this.onChunkReadyToSend(chunk); -- } else { -- completablefuture2.thenAcceptAsync((object) -> { -- this.onChunkReadyToSend(chunk); -- }, this.mainThreadExecutor); -- } -- -- }); -- }, this.mainThreadExecutor); -- -- completablefuture1.handle((chunkresult, throwable) -> { -- this.tickingGenerated.getAndIncrement(); -- return null; -- }); -- return completablefuture1; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private void onChunkReadyToSend(LevelChunk chunk) { -- ChunkPos chunkcoordintpair = chunk.getPos(); -- Iterator iterator = this.playerMap.getAllPlayers().iterator(); -- -- while (iterator.hasNext()) { -- ServerPlayer entityplayer = (ServerPlayer) iterator.next(); -- -- if (entityplayer.getChunkTrackingView().contains(chunkcoordintpair)) { -- ChunkMap.markChunkPendingToSend(entityplayer, chunk); -- } -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite player chunk loader - - } - - public CompletableFuture> prepareAccessibleChunk(ChunkHolder holder) { -- return this.getChunkRangeFuture(holder, 1, ChunkStatus::getStatusAroundFullChunk).thenApplyAsync((chunkresult) -> { -- return chunkresult.map((list) -> { -- return (LevelChunk) list.get(list.size() / 2); -- }); -- }, (runnable) -> { -- this.mainThreadMailbox.tell(ChunkTaskPriorityQueueSorter.message(holder, runnable)); -- }); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public int getTickingGenerated() { -@@ -1052,96 +625,15 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - private boolean saveChunkIfNeeded(ChunkHolder chunkHolder) { -- if (!chunkHolder.wasAccessibleSinceLastSave()) { -- return false; -- } else { -- ChunkAccess ichunkaccess = (ChunkAccess) chunkHolder.getChunkToSave().getNow(null); // CraftBukkit - decompile error -- -- if (!(ichunkaccess instanceof ImposterProtoChunk) && !(ichunkaccess instanceof LevelChunk)) { -- return false; -- } else { -- long i = ichunkaccess.getPos().toLong(); -- long j = this.chunkSaveCooldowns.getOrDefault(i, -1L); -- long k = System.currentTimeMillis(); -- -- if (k < j) { -- return false; -- } else { -- boolean flag = this.save(ichunkaccess); -- -- chunkHolder.refreshAccessibility(); -- if (flag) { -- this.chunkSaveCooldowns.put(i, k + 10000L); -- } -- -- return flag; -- } -- } -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public boolean save(ChunkAccess chunk) { -- this.poiManager.flush(chunk.getPos()); -- if (!chunk.isUnsaved()) { -- return false; -- } else { -- chunk.setUnsaved(false); -- ChunkPos chunkcoordintpair = chunk.getPos(); -- -- try { -- ChunkStatus chunkstatus = chunk.getStatus(); -- -- if (chunkstatus.getChunkType() != ChunkType.LEVELCHUNK) { -- if (this.isExistingChunkFull(chunkcoordintpair)) { -- return false; -- } -- -- if (chunkstatus == ChunkStatus.EMPTY && chunk.getAllStarts().values().stream().noneMatch(StructureStart::isValid)) { -- return false; -- } -- } -- -- this.level.getProfiler().incrementCounter("chunkSave"); -- CompoundTag nbttagcompound = ChunkSerializer.write(this.level, chunk); -- -- this.write(chunkcoordintpair, nbttagcompound).exceptionallyAsync((throwable) -> { -- this.level.getServer().reportChunkSaveFailure(chunkcoordintpair); -- return null; -- }, this.mainThreadExecutor); -- this.markPosition(chunkcoordintpair, chunkstatus.getChunkType()); -- return true; -- } catch (Exception exception) { -- ChunkMap.LOGGER.error("Failed to save chunk {},{}", new Object[]{chunkcoordintpair.x, chunkcoordintpair.z, exception}); -- this.level.getServer().reportChunkSaveFailure(chunkcoordintpair); -- return false; -- } -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private boolean isExistingChunkFull(ChunkPos pos) { -- byte b0 = this.chunkTypeCache.get(pos.toLong()); -- -- if (b0 != 0) { -- return b0 == 1; -- } else { -- CompoundTag nbttagcompound; -- -- try { -- nbttagcompound = (CompoundTag) ((Optional) this.readChunk(pos).join()).orElse((Object) null); -- if (nbttagcompound == null) { -- this.markPositionReplaceable(pos); -- return false; -- } -- } catch (Exception exception) { -- ChunkMap.LOGGER.error("Failed to read chunk {}", pos, exception); -- this.markPositionReplaceable(pos); -- return false; -- } -- -- ChunkType chunktype = ChunkSerializer.getChunkTypeFromTag(nbttagcompound); -- -- return this.markPosition(pos, chunktype) == 1; -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public void setServerViewDistance(int watchDistance) { // Paper - public -@@ -1149,37 +641,36 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - - if (j != this.serverViewDistance) { - this.serverViewDistance = j; -- this.distanceManager.updatePlayerTickets(this.serverViewDistance); -- Iterator iterator = this.playerMap.getAllPlayers().iterator(); -+ this.level.playerChunkLoader.setLoadDistance(this.serverViewDistance + 1); // Paper - replace player loader system -+ } - -- while (iterator.hasNext()) { -- ServerPlayer entityplayer = (ServerPlayer) iterator.next(); -+ } - -- this.updateChunkTracking(entityplayer); -- } -- } -+ // Paper start - replace player loader system -+ public void setTickViewDistance(int distance) { -+ this.level.playerChunkLoader.setTickDistance(distance); -+ } - -+ public void setSendViewDistance(int distance) { -+ this.level.playerChunkLoader.setSendDistance(distance); - } -+ // Paper end - replace player loader system - - public int getPlayerViewDistance(ServerPlayer player) { // Paper - public -- return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance); -+ return io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(player); // Paper - per player view distance - } - - private void markChunkPendingToSend(ServerPlayer player, ChunkPos pos) { -- LevelChunk chunk = this.getChunkToSend(pos.toLong()); -- -- if (chunk != null) { -- ChunkMap.markChunkPendingToSend(player, chunk); -- } -+ throw new UnsupportedOperationException(); // Paper - per player view distance - - } - - private static void markChunkPendingToSend(ServerPlayer player, LevelChunk chunk) { -- player.connection.chunkSender.markChunkPendingToSend(chunk); -+ throw new UnsupportedOperationException(); // Paper - rewrite player chunk loader - } - - private static void dropChunk(ServerPlayer player, ChunkPos pos) { -- player.connection.chunkSender.dropChunk(player, pos); -+ // Paper - rewrite player chunk loader - } - - @Nullable -@@ -1202,30 +693,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - void dumpChunks(Writer writer) throws IOException { -- CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("z").addColumn("level").addColumn("in_memory").addColumn("status").addColumn("full_status").addColumn("accessible_ready").addColumn("ticking_ready").addColumn("entity_ticking_ready").addColumn("ticket").addColumn("spawning").addColumn("block_entity_count").addColumn("ticking_ticket").addColumn("ticking_level").addColumn("block_ticks").addColumn("fluid_ticks").build(writer); -- TickingTracker tickingtracker = this.distanceManager.tickingTracker(); -- Iterator objectbidirectionaliterator = io.papermc.paper.chunk.system.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper -- -- while (objectbidirectionaliterator.hasNext()) { -- ChunkHolder playerchunk = objectbidirectionaliterator.next(); // Paper -- long i = playerchunk.pos.toLong(); // Paper -- ChunkPos chunkcoordintpair = new ChunkPos(i); -- // Paper -- Optional optional = Optional.ofNullable(playerchunk.getLastAvailable()); -- Optional optional1 = optional.flatMap((ichunkaccess) -> { -- return ichunkaccess instanceof LevelChunk ? Optional.of((LevelChunk) ichunkaccess) : Optional.empty(); -- }); -- -- // CraftBukkit - decompile error -- csvwriter.writeRow(chunkcoordintpair.x, chunkcoordintpair.z, playerchunk.getTicketLevel(), optional.isPresent(), optional.map(ChunkAccess::getStatus).orElse(null), optional1.map(LevelChunk::getFullStatus).orElse(null), ChunkMap.printFuture(playerchunk.getFullChunkFuture()), ChunkMap.printFuture(playerchunk.getTickingChunkFuture()), ChunkMap.printFuture(playerchunk.getEntityTickingChunkFuture()), this.distanceManager.getTicketDebugString(i), this.anyPlayerCloseEnoughForSpawning(chunkcoordintpair), optional1.map((chunk) -> { -- return chunk.getBlockEntities().size(); -- }).orElse(0), tickingtracker.getTicketDebugString(i), tickingtracker.getLevel(i), optional1.map((chunk) -> { -- return chunk.getBlockTicks().count(); -- }).orElse(0), optional1.map((chunk) -> { -- return chunk.getFluidTicks().count(); -- }).orElse(0)); -- } -- -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private static String printFuture(CompletableFuture> future) { -@@ -1240,6 +708,32 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - } - -+ // Paper start - Asynchronous chunk io -+ @Nullable -+ @Override -+ public CompoundTag readSync(ChunkPos chunkcoordintpair) throws IOException { -+ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) { -+ return io.papermc.paper.chunk.system.io.RegionFileIOThread.loadData( -+ this.level, chunkcoordintpair.x, chunkcoordintpair.z, io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA, -+ io.papermc.paper.chunk.system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread() -+ ); -+ } -+ return super.readSync(chunkcoordintpair); -+ } -+ -+ @Override -+ public CompletableFuture write(ChunkPos chunkcoordintpair, CompoundTag nbttagcompound) throws IOException { -+ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) { -+ io.papermc.paper.chunk.system.io.RegionFileIOThread.scheduleSave( -+ this.level, chunkcoordintpair.x, chunkcoordintpair.z, nbttagcompound, -+ io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA); -+ return null; -+ } -+ super.write(chunkcoordintpair, nbttagcompound); -+ return null; -+ } -+ // Paper end -+ - private CompletableFuture> readChunk(ChunkPos chunkPos) { - return this.read(chunkPos).thenApplyAsync((optional) -> { - return optional.map((nbttagcompound) -> this.upgradeChunkTag(nbttagcompound, chunkPos)); // CraftBukkit -@@ -1340,8 +834,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - this.distanceManager.addPlayer(SectionPos.of((EntityAccess) player), player); - } - -- player.setChunkTrackingView(ChunkTrackingView.EMPTY); -- this.updateChunkTracking(player); -+ // Paper - handled by player chunk loader - this.addPlayerToDistanceMaps(player); // Paper - distance maps - } else { - SectionPos sectionposition = player.getLastSectionPos(); -@@ -1352,7 +845,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - this.removePlayerFromDistanceMaps(player); // Paper - distance maps -- this.applyChunkTrackingView(player, ChunkTrackingView.EMPTY); -+ // Paper - handled by player chunk loader - } - - } -@@ -1400,71 +893,30 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - this.playerMap.unIgnorePlayer(player); - } - -- this.updateChunkTracking(player); -+ // Paper - replaced by PlayerChunkLoader - } - - this.updateMaps(player); // Paper - distance maps - } - - private void updateChunkTracking(ServerPlayer player) { -- ChunkPos chunkcoordintpair = player.chunkPosition(); -- int i = this.getPlayerViewDistance(player); -- ChunkTrackingView chunktrackingview = player.getChunkTrackingView(); -- -- if (chunktrackingview instanceof ChunkTrackingView.Positioned chunktrackingview_a) { -- if (chunktrackingview_a.center().equals(chunkcoordintpair) && chunktrackingview_a.viewDistance() == i) { -- return; -- } -- } -- -- this.applyChunkTrackingView(player, ChunkTrackingView.of(chunkcoordintpair, i)); -+ throw new UnsupportedOperationException(); // Paper - replaced by PlayerChunkLoader - } - - private void applyChunkTrackingView(ServerPlayer player, ChunkTrackingView chunkFilter) { -- if (player.level() == this.level) { -- ChunkTrackingView chunktrackingview1 = player.getChunkTrackingView(); -- -- if (chunkFilter instanceof ChunkTrackingView.Positioned) { -- label15: -- { -- ChunkTrackingView.Positioned chunktrackingview_a = (ChunkTrackingView.Positioned) chunkFilter; -- -- if (chunktrackingview1 instanceof ChunkTrackingView.Positioned) { -- ChunkTrackingView.Positioned chunktrackingview_a1 = (ChunkTrackingView.Positioned) chunktrackingview1; -- -- if (chunktrackingview_a1.center().equals(chunktrackingview_a.center())) { -- break label15; -- } -- } -- -- player.connection.send(new ClientboundSetChunkCacheCenterPacket(chunktrackingview_a.center().x, chunktrackingview_a.center().z)); -- } -- } -- -- ChunkTrackingView.difference(chunktrackingview1, chunkFilter, (chunkcoordintpair) -> { -- this.markChunkPendingToSend(player, chunkcoordintpair); -- }, (chunkcoordintpair) -> { -- ChunkMap.dropChunk(player, chunkcoordintpair); -- }); -- player.setChunkTrackingView(chunkFilter); -- } -+ throw new UnsupportedOperationException(); // Paper - replaced by PlayerChunkLoader - } - - @Override - public List getPlayers(ChunkPos chunkPos, boolean onlyOnWatchDistanceEdge) { -- Set set = this.playerMap.getAllPlayers(); -- Builder builder = ImmutableList.builder(); -- Iterator iterator = set.iterator(); -- -- while (iterator.hasNext()) { -- ServerPlayer entityplayer = (ServerPlayer) iterator.next(); -- -- if (onlyOnWatchDistanceEdge && this.isChunkOnTrackedBorder(entityplayer, chunkPos.x, chunkPos.z) || !onlyOnWatchDistanceEdge && this.isChunkTracked(entityplayer, chunkPos.x, chunkPos.z)) { -- builder.add(entityplayer); -- } -+ // Paper start - per player view distance -+ ChunkHolder holder = this.getVisibleChunkIfPresent(chunkPos.toLong()); -+ if (holder == null) { -+ return new java.util.ArrayList<>(); -+ } else { -+ return holder.getPlayers(onlyOnWatchDistanceEdge); - } -- -- return builder.build(); -+ // Paper end - per player view distance - } - - public void addEntity(Entity entity) { -@@ -1535,13 +987,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - protected void tick() { -- Iterator iterator = this.playerMap.getAllPlayers().iterator(); -- -- while (iterator.hasNext()) { -- ServerPlayer entityplayer = (ServerPlayer) iterator.next(); -- -- this.updateChunkTracking(entityplayer); -- } -+ // Paper - replaced by PlayerChunkLoader - - List list = Lists.newArrayList(); - List list1 = this.level.players(); -@@ -1648,16 +1094,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - public void waitForLightBeforeSending(ChunkPos centerPos, int radius) { -- int j = radius + 1; -- -- ChunkPos.rangeClosed(centerPos, j).forEach((chunkcoordintpair1) -> { -- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(chunkcoordintpair1.toLong()); -- -- if (playerchunk != null) { -- playerchunk.addSendDependency(this.lightEngine.waitForPendingTasks(chunkcoordintpair1.x, chunkcoordintpair1.z)); -- } -- -- }); -+ // Paper - rewrite player chunk loader - } - - public class ChunkDistanceManager extends DistanceManager { // Paper - public -@@ -1668,7 +1105,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - - @Override - protected boolean isChunkToRemove(long pos) { -- return ChunkMap.this.toDrop.contains(pos); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @Nullable -diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java -index 7a48ae2ba962ff56d0abff581b51f28b48bd9aae..ed5154e41ca858f4d6b4d1c276c66831c038d2a6 100644 ---- a/src/main/java/net/minecraft/server/level/DistanceManager.java -+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java -@@ -38,65 +38,28 @@ import org.slf4j.Logger; - - public abstract class DistanceManager { - -+ // Paper start - rewrite chunk system -+ public io.papermc.paper.chunk.system.scheduling.ChunkHolderManager getChunkHolderManager() { -+ return this.chunkMap.level.chunkTaskScheduler.chunkHolderManager; -+ } -+ // Paper end - rewrite chunk system -+ - static final Logger LOGGER = LogUtils.getLogger(); - static final int PLAYER_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING); - private static final int INITIAL_TICKET_LIST_CAPACITY = 4; - final Long2ObjectMap> playersPerChunk = new Long2ObjectOpenHashMap(); -- public final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap(); -- private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker(); -+ // Paper - rewrite chunk system - private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8); -- private final TickingTracker tickingTicketsTracker = new TickingTracker(); -- private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(32); -- final Set chunksToUpdateFutures = Sets.newHashSet(); -- final ChunkTaskPriorityQueueSorter ticketThrottler; -- final ProcessorHandle> ticketThrottlerInput; -- final ProcessorHandle ticketThrottlerReleaser; -- final LongSet ticketsToRelease = new LongOpenHashSet(); -- final Executor mainThreadExecutor; -- private long ticketTickCounter; -- public int simulationDistance = 10; -+ // Paper - rewrite chunk system - private final ChunkMap chunkMap; // Paper - - protected DistanceManager(Executor workerExecutor, Executor mainThreadExecutor, ChunkMap chunkMap) { -- Objects.requireNonNull(mainThreadExecutor); -- ProcessorHandle mailbox = ProcessorHandle.of("player ticket throttler", mainThreadExecutor::execute); -- ChunkTaskPriorityQueueSorter chunktaskqueuesorter = new ChunkTaskPriorityQueueSorter(ImmutableList.of(mailbox), workerExecutor, 4); -- -- this.ticketThrottler = chunktaskqueuesorter; -- this.ticketThrottlerInput = chunktaskqueuesorter.getProcessor(mailbox, true); -- this.ticketThrottlerReleaser = chunktaskqueuesorter.getReleaseProcessor(mailbox); -- this.mainThreadExecutor = mainThreadExecutor; -+ // Paper - rewrite chunk system - this.chunkMap = chunkMap; // Paper - } - - protected void purgeStaleTickets() { -- ++this.ticketTickCounter; -- ObjectIterator>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); -- -- while (objectiterator.hasNext()) { -- Entry>> entry = (Entry) objectiterator.next(); -- Iterator> iterator = ((SortedArraySet) entry.getValue()).iterator(); -- boolean flag = false; -- -- while (iterator.hasNext()) { -- Ticket ticket = (Ticket) iterator.next(); -- -- if (ticket.timedOut(this.ticketTickCounter)) { -- iterator.remove(); -- flag = true; -- this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket); -- } -- } -- -- if (flag) { -- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false); -- } -- -- if (((SortedArraySet) entry.getValue()).isEmpty()) { -- objectiterator.remove(); -- } -- } -- -+ this.getChunkHolderManager().tick(); // Paper - rewrite chunk system - } - - private static int getTicketLevelAt(SortedArraySet> tickets) { -@@ -112,108 +75,25 @@ public abstract class DistanceManager { - protected abstract ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k); - - public boolean runAllUpdates(ChunkMap chunkStorage) { -- this.naturalSpawnChunkCounter.runAllUpdates(); -- this.tickingTicketsTracker.runAllUpdates(); -- this.playerTicketManager.runAllUpdates(); -- int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE); -- boolean flag = i != 0; -- -- if (flag) { -- ; -- } -- -- if (!this.chunksToUpdateFutures.isEmpty()) { -- // CraftBukkit start -- // Iterate pending chunk updates with protection against concurrent modification exceptions -- java.util.Iterator iter = this.chunksToUpdateFutures.iterator(); -- int expectedSize = this.chunksToUpdateFutures.size(); -- do { -- ChunkHolder playerchunk = iter.next(); -- iter.remove(); -- expectedSize--; -- -- playerchunk.updateFutures(chunkStorage, this.mainThreadExecutor); -- -- // Reset iterator if set was modified using add() -- if (this.chunksToUpdateFutures.size() != expectedSize) { -- expectedSize = this.chunksToUpdateFutures.size(); -- iter = this.chunksToUpdateFutures.iterator(); -- } -- } while (iter.hasNext()); -- // CraftBukkit end -- -- return true; -- } else { -- if (!this.ticketsToRelease.isEmpty()) { -- LongIterator longiterator = this.ticketsToRelease.iterator(); -- -- while (longiterator.hasNext()) { -- long j = longiterator.nextLong(); -- -- if (this.getTickets(j).stream().anyMatch((ticket) -> { -- return ticket.getType() == TicketType.PLAYER; -- })) { -- ChunkHolder playerchunk = chunkStorage.getUpdatingChunkIfPresent(j); -- -- if (playerchunk == null) { -- throw new IllegalStateException(); -- } -- -- CompletableFuture> completablefuture = playerchunk.getEntityTickingChunkFuture(); -- -- completablefuture.thenAccept((chunkresult) -> { -- this.mainThreadExecutor.execute(() -> { -- this.ticketThrottlerReleaser.tell(ChunkTaskPriorityQueueSorter.release(() -> { -- }, j, false)); -- }); -- }); -- } -- } -- -- this.ticketsToRelease.clear(); -- } -- -- return flag; -- } -+ return this.getChunkHolderManager().processTicketUpdates(); // Paper - rewrite chunk system - } - - boolean addTicket(long i, Ticket ticket) { // CraftBukkit - void -> boolean -- SortedArraySet> arraysetsorted = this.getTickets(i); -- int j = DistanceManager.getTicketLevelAt(arraysetsorted); -- Ticket ticket1 = (Ticket) arraysetsorted.addOrGet(ticket); -- -- ticket1.setCreatedTick(this.ticketTickCounter); -- if (ticket.getTicketLevel() < j) { -- this.ticketTracker.update(i, ticket.getTicketLevel(), true); -- } -- -- return ticket == ticket1; // CraftBukkit -+ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::addTicket"); // Paper -+ return this.getChunkHolderManager().addTicketAtLevel((TicketType)ticket.getType(), i, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system - } - - boolean removeTicket(long i, Ticket ticket) { // CraftBukkit - void -> boolean -- SortedArraySet> arraysetsorted = this.getTickets(i); -- -- boolean removed = false; // CraftBukkit -- if (arraysetsorted.remove(ticket)) { -- removed = true; // CraftBukkit -- } -- -- if (arraysetsorted.isEmpty()) { -- this.tickets.remove(i); -- } -- -- this.ticketTracker.update(i, DistanceManager.getTicketLevelAt(arraysetsorted), false); -- return removed; // CraftBukkit -+ org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::removeTicket"); // Paper -+ return this.getChunkHolderManager().removeTicketAtLevel((TicketType)ticket.getType(), i, ticket.getTicketLevel(), ticket.key); // Paper - rewrite chunk system - } - - public void addTicket(TicketType type, ChunkPos pos, int level, T argument) { -- this.addTicket(pos.toLong(), new Ticket<>(type, level, argument)); -+ this.getChunkHolderManager().addTicketAtLevel(type, pos, level, argument); // Paper - rewrite chunk system - } - - public void removeTicket(TicketType type, ChunkPos pos, int level, T argument) { -- Ticket ticket = new Ticket<>(type, level, argument); -- -- this.removeTicket(pos.toLong(), ticket); -+ this.getChunkHolderManager().removeTicketAtLevel(type, pos, level, argument); // Paper - rewrite chunk system - } - - public void addRegionTicket(TicketType type, ChunkPos pos, int radius, T argument) { -@@ -222,13 +102,7 @@ public abstract class DistanceManager { - } - - public boolean addRegionTicketAtDistance(TicketType tickettype, ChunkPos chunkcoordintpair, int i, T t0) { -- // CraftBukkit end -- Ticket ticket = new Ticket<>(tickettype, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); -- long j = chunkcoordintpair.toLong(); -- -- boolean added = this.addTicket(j, ticket); // CraftBukkit -- this.tickingTicketsTracker.addTicket(j, ticket); -- return added; // CraftBukkit -+ return this.getChunkHolderManager().addTicketAtLevel(tickettype, chunkcoordintpair, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); // Paper - rewrite chunk system - } - - public void removeRegionTicket(TicketType type, ChunkPos pos, int radius, T argument) { -@@ -237,31 +111,21 @@ public abstract class DistanceManager { - } - - public boolean removeRegionTicketAtDistance(TicketType tickettype, ChunkPos chunkcoordintpair, int i, T t0) { -- // CraftBukkit end -- Ticket ticket = new Ticket<>(tickettype, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); -- long j = chunkcoordintpair.toLong(); -- -- boolean removed = this.removeTicket(j, ticket); // CraftBukkit -- this.tickingTicketsTracker.removeTicket(j, ticket); -- return removed; // CraftBukkit -+ return this.getChunkHolderManager().removeTicketAtLevel(tickettype, chunkcoordintpair, ChunkLevel.byStatus(FullChunkStatus.FULL) - i, t0); // Paper - rewrite chunk system - } - -- private SortedArraySet> getTickets(long position) { -- return (SortedArraySet) this.tickets.computeIfAbsent(position, (j) -> { -- return SortedArraySet.create(4); -- }); -- } -+ // Paper - rewrite chunk system - - protected void updateChunkForced(ChunkPos pos, boolean forced) { -- Ticket ticket = new Ticket<>(TicketType.FORCED, ChunkMap.FORCED_TICKET_LEVEL, pos); -+ Ticket ticket = new Ticket<>(TicketType.FORCED, ChunkMap.FORCED_TICKET_LEVEL, pos, 0L); // Paper - rewrite chunk system - long i = pos.toLong(); - - if (forced) { - this.addTicket(i, ticket); -- this.tickingTicketsTracker.addTicket(i, ticket); -+ //this.tickingTicketsTracker.addTicket(i, ticket); // Paper - no longer used - } else { - this.removeTicket(i, ticket); -- this.tickingTicketsTracker.removeTicket(i, ticket); -+ //this.tickingTicketsTracker.removeTicket(i, ticket); // Paper - no longer used - } - - } -@@ -270,12 +134,10 @@ public abstract class DistanceManager { - ChunkPos chunkcoordintpair = pos.chunk(); - long i = chunkcoordintpair.toLong(); - -- ((ObjectSet) this.playersPerChunk.computeIfAbsent(i, (j) -> { -- return new ObjectOpenHashSet(); -- })).add(player); -+ // Paper - no longer used - this.naturalSpawnChunkCounter.update(i, 0, true); -- this.playerTicketManager.update(i, 0, true); -- this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); -+ //this.playerTicketManager.update(i, 0, true); // Paper - no longer used -+ //this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used - } - - public void removePlayer(SectionPos pos, ServerPlayer player) { -@@ -288,40 +150,44 @@ public abstract class DistanceManager { - if (objectset == null || objectset.isEmpty()) { // Paper - this.playersPerChunk.remove(i); - this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false); -- this.playerTicketManager.update(i, Integer.MAX_VALUE, false); -- this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); -+ //this.playerTicketManager.update(i, Integer.MAX_VALUE, false); // Paper - no longer used -+ //this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used - } - - } - -- private int getPlayerTicketLevel() { -- return Math.max(0, ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING) - this.simulationDistance); -- } -+ // Paper - rewrite chunk system - - public boolean inEntityTickingRange(long chunkPos) { -- return ChunkLevel.isEntityTicking(this.tickingTicketsTracker.getLevel(chunkPos)); -+ // Paper start - replace player chunk loader system -+ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(chunkPos); -+ return holder != null && holder.isEntityTickingReady(); -+ // Paper end - replace player chunk loader system - } - - public boolean inBlockTickingRange(long chunkPos) { -- return ChunkLevel.isBlockTicking(this.tickingTicketsTracker.getLevel(chunkPos)); -+ // Paper start - replace player chunk loader system -+ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(chunkPos); -+ return holder != null && holder.isTickingReady(); -+ // Paper end - replace player chunk loader system - } - - protected String getTicketDebugString(long pos) { -- SortedArraySet> arraysetsorted = (SortedArraySet) this.tickets.get(pos); -- -- return arraysetsorted != null && !arraysetsorted.isEmpty() ? ((Ticket) arraysetsorted.first()).toString() : "no_ticket"; -+ return this.getChunkHolderManager().getTicketDebugString(pos); // Paper - rewrite chunk system - } - - protected void updatePlayerTickets(int viewDistance) { -- this.playerTicketManager.updateViewDistance(viewDistance); -+ this.chunkMap.setServerViewDistance(viewDistance); // Paper - route to player chunk manager - } - -- public void updateSimulationDistance(int simulationDistance) { -- if (simulationDistance != this.simulationDistance) { -- this.simulationDistance = simulationDistance; -- this.tickingTicketsTracker.replacePlayerTicketsLevel(this.getPlayerTicketLevel()); -- } -+ // Paper start -+ public int getSimulationDistance() { -+ return this.chunkMap.level.playerChunkLoader.getAPITickDistance(); -+ } -+ // Paper end - -+ public void updateSimulationDistance(int simulationDistance) { -+ this.chunkMap.level.playerChunkLoader.setTickDistance(simulationDistance); // Paper - route to player chunk manager - } - - public int getNaturalSpawnChunkCount() { -@@ -335,103 +201,26 @@ public abstract class DistanceManager { - } - - public String getDebugStatus() { -- return this.ticketThrottler.getDebugStatus(); -+ return "No DistanceManager stats available"; // Paper - rewrite chunk system - } - -- private void dumpTickets(String path) { -- try { -- FileOutputStream fileoutputstream = new FileOutputStream(new File(path)); -- -- try { -- ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().iterator(); -- -- while (objectiterator.hasNext()) { -- Entry>> entry = (Entry) objectiterator.next(); -- ChunkPos chunkcoordintpair = new ChunkPos(entry.getLongKey()); -- Iterator iterator = ((SortedArraySet) entry.getValue()).iterator(); -- -- while (iterator.hasNext()) { -- Ticket ticket = (Ticket) iterator.next(); -- -- fileoutputstream.write((chunkcoordintpair.x + "\t" + chunkcoordintpair.z + "\t" + String.valueOf(ticket.getType()) + "\t" + ticket.getTicketLevel() + "\t\n").getBytes(StandardCharsets.UTF_8)); -- } -- } -- } catch (Throwable throwable) { -- try { -- fileoutputstream.close(); -- } catch (Throwable throwable1) { -- throwable.addSuppressed(throwable1); -- } -- -- throw throwable; -- } -- -- fileoutputstream.close(); -- } catch (IOException ioexception) { -- DistanceManager.LOGGER.error("Failed to dump tickets to {}", path, ioexception); -- } -- -- } -- -- @VisibleForTesting -- TickingTracker tickingTracker() { -- return this.tickingTicketsTracker; -- } -+ // Paper - rewrite chunk system - - public void removeTicketsOnClosing() { -- ImmutableSet> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.LIGHT, TicketType.FUTURE_AWAIT, TicketType.CHUNK_RELIGHT, ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET); // Paper - add additional tickets to preserve -- ObjectIterator>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); -- -- while (objectiterator.hasNext()) { -- Entry>> entry = (Entry) objectiterator.next(); -- Iterator> iterator = ((SortedArraySet) entry.getValue()).iterator(); -- boolean flag = false; -- -- while (iterator.hasNext()) { -- Ticket ticket = (Ticket) iterator.next(); -- -- if (!immutableset.contains(ticket.getType())) { -- iterator.remove(); -- flag = true; -- this.tickingTicketsTracker.removeTicket(entry.getLongKey(), ticket); -- } -- } -- -- if (flag) { -- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt((SortedArraySet) entry.getValue()), false); -- } -- -- if (((SortedArraySet) entry.getValue()).isEmpty()) { -- objectiterator.remove(); -- } -- } -- -+ // Paper - rewrite chunk system - this stupid hack ain't needed anymore - } - - public boolean hasTickets() { -- return !this.tickets.isEmpty(); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - // CraftBukkit start - public void removeAllTicketsFor(TicketType ticketType, int ticketLevel, T ticketIdentifier) { -- Ticket target = new Ticket<>(ticketType, ticketLevel, ticketIdentifier); -- -- for (java.util.Iterator>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); iterator.hasNext();) { -- Entry>> entry = iterator.next(); -- SortedArraySet> tickets = entry.getValue(); -- if (tickets.remove(target)) { -- // copied from removeTicket -- this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt(tickets), false); -- -- // can't use entry after it's removed -- if (tickets.isEmpty()) { -- iterator.remove(); -- } -- } -- } -+ this.getChunkHolderManager().removeAllTicketsFor(ticketType, ticketLevel, ticketIdentifier); // Paper - rewrite chunk system - } - // CraftBukkit end - -+ /* Paper - rewrite chunk system - private class ChunkTicketTracker extends ChunkTracker { - - private static final int MAX_LEVEL = ChunkLevel.MAX_LEVEL + 1; -@@ -478,6 +267,7 @@ public abstract class DistanceManager { - return this.runUpdates(distance); - } - } -+ */ // Paper - rewrite chunk system - - private class FixedPlayerDistanceChunkTracker extends ChunkTracker { - -@@ -557,6 +347,7 @@ public abstract class DistanceManager { - } - } - -+ /* Paper - rewrite chunk system - private class PlayerTicketTracker extends DistanceManager.FixedPlayerDistanceChunkTracker { - - private int viewDistance = 0; -@@ -652,4 +443,5 @@ public abstract class DistanceManager { - return distance <= this.viewDistance; - } - } -+ */ // Paper - rewrite chunk system - } -diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -index 2d9d4d06b75873f888ef4d8f5779a52706f821a8..f74efe41cd0da2f9749fc96fb9e0f7cf237ad1c6 100644 ---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java -+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -@@ -71,7 +71,7 @@ public class ServerChunkCache extends ChunkSource { - public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet entityTickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true); - final com.destroystokyo.paper.util.concurrent.WeakSeqLock loadedChunkMapSeqLock = new com.destroystokyo.paper.util.concurrent.WeakSeqLock(); - final it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap loadedChunkMap = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(8192, 0.5f); -- long chunkFutureAwaitCounter; -+ final java.util.concurrent.atomic.AtomicLong chunkFutureAwaitCounter = new java.util.concurrent.atomic.AtomicLong(); // Paper - chunk system rewrite - private final LevelChunk[] lastLoadedChunks = new LevelChunk[4 * 4]; - // Paper end - -@@ -195,7 +195,7 @@ public class ServerChunkCache extends ChunkSource { - public LevelChunk getChunkAtIfLoadedImmediately(int x, int z) { - long k = ChunkPos.asLong(x, z); - -- if (Thread.currentThread() == this.mainThread) { -+ if (io.papermc.paper.util.TickThread.isTickThread()) { // Paper - rewrite chunk system - return this.getChunkAtIfLoadedMainThread(x, z); - } - -@@ -247,7 +247,8 @@ public class ServerChunkCache extends ChunkSource { - @Nullable - @Override - public ChunkAccess getChunk(int x, int z, ChunkStatus leastStatus, boolean create) { -- if (Thread.currentThread() != this.mainThread) { -+ final int x1 = x; final int z1 = z; // Paper - conflict on variable change -+ if (!io.papermc.paper.util.TickThread.isTickThread()) { // Paper - rewrite chunk system - return (ChunkAccess) CompletableFuture.supplyAsync(() -> { - return this.getChunk(x, z, leastStatus, create); - }, this.mainThreadProcessor).join(); -@@ -263,15 +264,7 @@ public class ServerChunkCache extends ChunkSource { - gameprofilerfiller.incrementCounter("getChunk"); - long k = ChunkPos.asLong(x, z); - -- for (int l = 0; l < 4; ++l) { -- if (k == this.lastChunkPos[l] && leastStatus == this.lastChunkStatus[l]) { -- ChunkAccess ichunkaccess = this.lastChunk[l]; -- -- if (ichunkaccess != null) { // CraftBukkit - the chunk can become accessible in the meantime TODO for non-null chunks it might also make sense to check that the chunk's state hasn't changed in the meantime -- return ichunkaccess; -- } -- } -- } -+ // Paper - rewrite chunk system - there are no correct callbacks to remove items from cache in the new chunk system - - gameprofilerfiller.incrementCounter("getChunkCacheMiss"); - CompletableFuture> completablefuture = this.getChunkFutureMainThread(x, z, leastStatus, create); -@@ -279,9 +272,11 @@ public class ServerChunkCache extends ChunkSource { - - Objects.requireNonNull(completablefuture); - if (!completablefuture.isDone()) { // Paper -+ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.pushChunkWait(this.level, x1, z1); // Paper - rewrite chunk system - com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.level, x, z); // Paper - Add debug for sync chunk loads - this.level.timings.syncChunkLoad.startTiming(); // Paper - chunkproviderserver_b.managedBlock(completablefuture::isDone); -+ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.popChunkWait(); // Paper - rewrite chunk system - this.level.timings.syncChunkLoad.stopTiming(); // Paper - } // Paper - ChunkResult chunkresult = (ChunkResult) completablefuture.join(); -@@ -299,7 +294,7 @@ public class ServerChunkCache extends ChunkSource { - @Nullable - @Override - public LevelChunk getChunkNow(int chunkX, int chunkZ) { -- if (Thread.currentThread() != this.mainThread) { -+ if (!io.papermc.paper.util.TickThread.isTickThread()) { // Paper - rewrite chunk system - return null; - } else { - return this.getChunkAtIfLoadedMainThread(chunkX, chunkZ); // Paper - Perf: Optimise getChunkAt calls for loaded chunks -@@ -313,7 +308,7 @@ public class ServerChunkCache extends ChunkSource { - } - - public CompletableFuture> getChunkFuture(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) { -- boolean flag1 = Thread.currentThread() == this.mainThread; -+ boolean flag1 = io.papermc.paper.util.TickThread.isTickThread(); // Paper - rewrite chunk system - CompletableFuture completablefuture; - - if (flag1) { -@@ -333,48 +328,54 @@ public class ServerChunkCache extends ChunkSource { - return completablefuture; - } - -+ - private CompletableFuture> getChunkFutureMainThread(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) { -- ChunkPos chunkcoordintpair = new ChunkPos(chunkX, chunkZ); -- long k = chunkcoordintpair.toLong(); -- int l = ChunkLevel.byStatus(leastStatus); -- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k); -+ // Paper start - add isUrgent - old sig left in place for dirty nms plugins -+ return getChunkFutureMainThread(chunkX, chunkZ, leastStatus, create, false); -+ } -+ private CompletableFuture> getChunkFutureMainThread(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create, boolean isUrgent) { -+ // Paper start - rewrite chunk system -+ io.papermc.paper.util.TickThread.ensureTickThread(this.level, chunkX, chunkZ, "Scheduling chunk load off-main"); -+ int minLevel = ChunkLevel.byStatus(leastStatus); -+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.level.chunkTaskScheduler.chunkHolderManager.getChunkHolder(chunkX, chunkZ); - -- // CraftBukkit start - don't add new ticket for currently unloading chunk -- boolean currentlyUnloading = false; -- if (playerchunk != null) { -- FullChunkStatus oldChunkState = ChunkLevel.fullStatus(playerchunk.oldTicketLevel); -- FullChunkStatus currentChunkState = ChunkLevel.fullStatus(playerchunk.getTicketLevel()); -- currentlyUnloading = (oldChunkState.isOrAfter(FullChunkStatus.FULL) && !currentChunkState.isOrAfter(FullChunkStatus.FULL)); -+ boolean needsFullScheduling = leastStatus == ChunkStatus.FULL && (chunkHolder == null || !chunkHolder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)); -+ -+ if ((chunkHolder == null || chunkHolder.getTicketLevel() > minLevel || needsFullScheduling) && !create) { -+ return ChunkHolder.UNLOADED_CHUNK_FUTURE; - } -- if (create && !currentlyUnloading) { -- // CraftBukkit end -- this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair); -- if (this.chunkAbsent(playerchunk, l)) { -- ProfilerFiller gameprofilerfiller = this.level.getProfiler(); -- -- gameprofilerfiller.push("chunkLoad"); -- this.runDistanceManagerUpdates(); -- playerchunk = this.getVisibleChunkIfPresent(k); -- gameprofilerfiller.pop(); -- if (this.chunkAbsent(playerchunk, l)) { -- throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("No chunk holder after ticket has been added")); -+ -+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder.ChunkCompletion chunkCompletion = chunkHolder == null ? null : chunkHolder.getLastChunkCompletion(); -+ if (needsFullScheduling || chunkCompletion == null || !chunkCompletion.genStatus().isOrAfter(leastStatus)) { -+ // schedule -+ CompletableFuture> ret = new CompletableFuture<>(); -+ Consumer complete = (ChunkAccess chunk) -> { -+ if (chunk == null) { -+ ret.complete(ChunkResult.error("Unexpected chunk unload")); -+ } else { -+ ret.complete(ChunkResult.of(chunk)); - } -- } -- } -+ }; - -- return this.chunkAbsent(playerchunk, l) ? ChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.getOrScheduleFuture(leastStatus, this.chunkMap); -- } -+ this.level.chunkTaskScheduler.scheduleChunkLoad( -+ chunkX, chunkZ, leastStatus, true, -+ isUrgent ? ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.BLOCKING : ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, -+ complete -+ ); - -- private boolean chunkAbsent(@Nullable ChunkHolder holder, int maxLevel) { -- return holder == null || holder.oldTicketLevel > maxLevel; // CraftBukkit using oldTicketLevel for isLoaded checks -+ return ret; -+ } else { -+ // can return now -+ return CompletableFuture.completedFuture(ChunkResult.of(chunkCompletion.chunk())); -+ } -+ // Paper end - rewrite chunk system - } - -+ // Paper - rewrite chunk system -+ - @Override - public boolean hasChunk(int x, int z) { -- ChunkHolder playerchunk = this.getVisibleChunkIfPresent((new ChunkPos(x, z)).toLong()); -- int k = ChunkLevel.byStatus(ChunkStatus.FULL); -- -- return !this.chunkAbsent(playerchunk, k); -+ return this.getChunkAtIfLoadedImmediately(x, z) != null; // Paper - rewrite chunk system - } - - @Nullable -@@ -386,22 +387,13 @@ public class ServerChunkCache extends ChunkSource { - if (playerchunk == null) { - return null; - } else { -- int l = ServerChunkCache.CHUNK_STATUSES.size() - 1; -- -- while (true) { -- ChunkStatus chunkstatus = (ChunkStatus) ServerChunkCache.CHUNK_STATUSES.get(l); -- ChunkAccess ichunkaccess = (ChunkAccess) ((ChunkResult) playerchunk.getFutureIfPresentUnchecked(chunkstatus).getNow(ChunkHolder.UNLOADED_CHUNK)).orElse((Object) null); -- -- if (ichunkaccess != null) { -- return ichunkaccess; -- } -- -- if (chunkstatus == ChunkStatus.INITIALIZE_LIGHT.getParent()) { -- return null; -- } -- -- --l; -+ // Paper start - rewrite chunk system -+ ChunkStatus status = playerchunk.getChunkHolderStatus(); -+ if (status != null && !status.isOrAfter(ChunkStatus.LIGHT.getParent())) { -+ return null; - } -+ return playerchunk.getAvailableChunkNow(); -+ // Paper end - rewrite chunk system - } - } - -@@ -415,15 +407,7 @@ public class ServerChunkCache extends ChunkSource { - } - - public boolean runDistanceManagerUpdates() { // Paper - public -- boolean flag = this.distanceManager.runAllUpdates(this.chunkMap); -- boolean flag1 = this.chunkMap.promoteChunkMap(); -- -- if (!flag && !flag1) { -- return false; -- } else { -- this.clearCache(); -- return true; -- } -+ return this.level.chunkTaskScheduler.chunkHolderManager.processTicketUpdates(); // Paper - rewrite chunk system - } - - // Paper start -@@ -433,9 +417,10 @@ public class ServerChunkCache extends ChunkSource { - // Paper end - - public boolean isPositionTicking(long pos) { -- ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); -- -- return playerchunk == null ? false : (!this.level.shouldTickBlocksAt(pos) ? false : ((ChunkResult) playerchunk.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).isSuccess()); -+ // Paper start - replace player chunk loader system -+ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(pos); -+ return holder != null && holder.isTickingReady(); -+ // Paper end - replace player chunk loader system - } - - public void save(boolean flush) { -@@ -451,17 +436,13 @@ public class ServerChunkCache extends ChunkSource { - this.close(true); - } - -- public void close(boolean save) throws IOException { -- if (save) { -- this.save(true); -- } -- // CraftBukkit end -- this.lightEngine.close(); -- this.chunkMap.close(); -+ public void close(boolean save) { // Paper - rewrite chunk system -+ this.level.chunkTaskScheduler.chunkHolderManager.close(save, true); // Paper - rewrite chunk system - } - - // CraftBukkit start - modelled on below - public void purgeUnload() { -+ if (true) return; // Paper - tickets will be removed later, this behavior isn't really well accounted for by the chunk system - this.level.getProfiler().push("purge"); - this.distanceManager.purgeStaleTickets(); - this.runDistanceManagerUpdates(); -@@ -485,6 +466,7 @@ public class ServerChunkCache extends ChunkSource { - this.level.getProfiler().popPush("chunks"); - if (tickChunks) { - this.level.timings.chunks.startTiming(); // Paper - timings -+ this.chunkMap.level.playerChunkLoader.tick(); // Paper - replace player chunk loader - this is mostly required to account for view distance changes - this.tickChunks(); - this.level.timings.chunks.stopTiming(); // Paper - timings - this.chunkMap.tick(); -@@ -587,7 +569,12 @@ public class ServerChunkCache extends ChunkSource { - ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos); - - if (playerchunk != null) { -- ((ChunkResult) playerchunk.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).ifSuccess(chunkConsumer); -+ // Paper start - rewrite chunk system -+ LevelChunk chunk = playerchunk.getFullChunkNow(); -+ if (chunk != null) { -+ chunkConsumer.accept(chunk); -+ } -+ // Paper end - rewrite chunk system - } - - } -@@ -753,17 +740,10 @@ public class ServerChunkCache extends ChunkSource { - @Override - // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task - public boolean pollTask() { -- try { - if (ServerChunkCache.this.runDistanceManagerUpdates()) { - return true; -- } else { -- ServerChunkCache.this.lightEngine.tryScheduleUpdate(); -- return super.pollTask(); - } -- } finally { -- ServerChunkCache.this.chunkMap.callbackExecutor.run(); -- } -- // CraftBukkit end -+ return super.pollTask() | ServerChunkCache.this.level.chunkTaskScheduler.executeMainThreadTask(); // Paper - rewrite chunk system - } - } - -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 7dfbb3dd6100180abd2ebab4d1e9e34928666f9e..7f8793fa8260cb8a1f55d0ba405e6df0ae6f7321 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -199,7 +199,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - public final PrimaryLevelData serverLevelData; // CraftBukkit - type - private int lastSpawnChunkRadius; - final EntityTickList entityTickList; -- public final PersistentEntitySectionManager entityManager; -+ //public final PersistentEntitySectionManager entityManager; // Paper - rewrite chunk system - private final GameEventDispatcher gameEventDispatcher; - public boolean noSave; - private final SleepStatus sleepStatus; -@@ -268,50 +268,65 @@ public class ServerLevel extends Level implements WorldGenLevel { - return true; - } - -- public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, -- java.util.function.Consumer> onLoad) { -- if (Thread.currentThread() != this.thread) { -- this.getChunkSource().mainThreadProcessor.execute(() -> { -- this.loadChunksForMoveAsync(axisalignedbb, priority, onLoad); -- }); -- return; -- } -+ public final void loadChunksAsync(BlockPos pos, int radiusBlocks, -+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, -+ java.util.function.Consumer> onLoad) { -+ loadChunksAsync( -+ (pos.getX() - radiusBlocks) >> 4, -+ (pos.getX() + radiusBlocks) >> 4, -+ (pos.getZ() - radiusBlocks) >> 4, -+ (pos.getZ() + radiusBlocks) >> 4, -+ priority, onLoad -+ ); -+ } -+ -+ public final void loadChunksAsync(BlockPos pos, int radiusBlocks, -+ net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus, -+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, -+ java.util.function.Consumer> onLoad) { -+ loadChunksAsync( -+ (pos.getX() - radiusBlocks) >> 4, -+ (pos.getX() + radiusBlocks) >> 4, -+ (pos.getZ() - radiusBlocks) >> 4, -+ (pos.getZ() + radiusBlocks) >> 4, -+ chunkStatus, priority, onLoad -+ ); -+ } -+ -+ public final void loadChunksAsync(int minChunkX, int maxChunkX, int minChunkZ, int maxChunkZ, -+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, -+ java.util.function.Consumer> onLoad) { -+ this.loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, priority, onLoad); -+ } -+ -+ public final void loadChunksAsync(int minChunkX, int maxChunkX, int minChunkZ, int maxChunkZ, -+ net.minecraft.world.level.chunk.status.ChunkStatus chunkStatus, -+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, -+ java.util.function.Consumer> onLoad) { - List ret = new java.util.ArrayList<>(); -- it.unimi.dsi.fastutil.ints.IntArrayList ticketLevels = new it.unimi.dsi.fastutil.ints.IntArrayList(); -- -- int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3; -- int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3; -- -- int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3; -- int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3; -- -- int minChunkX = minBlockX >> 4; -- int maxChunkX = maxBlockX >> 4; -- -- int minChunkZ = minBlockZ >> 4; -- int maxChunkZ = maxBlockZ >> 4; - - ServerChunkCache chunkProvider = this.getChunkSource(); - - int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1); -- int[] loadedChunks = new int[1]; -+ java.util.concurrent.atomic.AtomicInteger loadedChunks = new java.util.concurrent.atomic.AtomicInteger(); - -- Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++); -+ Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter.getAndIncrement()); -+ -+ int ticketLevel = 33 + net.minecraft.world.level.chunk.status.ChunkStatus.getDistance(chunkStatus); - - java.util.function.Consumer consumer = (net.minecraft.world.level.chunk.ChunkAccess chunk) -> { - if (chunk != null) { -- int ticketLevel = Math.max(33, chunkProvider.chunkMap.getUpdatingChunkIfPresent(chunk.getPos().toLong()).getTicketLevel()); -+ synchronized (ret) { - ret.add(chunk); -- ticketLevels.add(ticketLevel); -+ } - chunkProvider.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunk.getPos(), ticketLevel, holderIdentifier); - } -- if (++loadedChunks[0] == requiredChunks) { -+ if (loadedChunks.incrementAndGet() == requiredChunks) { - try { - onLoad.accept(java.util.Collections.unmodifiableList(ret)); - } finally { - for (int i = 0, len = ret.size(); i < len; ++i) { - ChunkPos 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); -@@ -323,12 +338,228 @@ public class ServerLevel extends Level implements WorldGenLevel { - for (int cx = minChunkX; cx <= maxChunkX; ++cx) { - for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { - io.papermc.paper.chunk.system.ChunkSystem.scheduleChunkLoad( -- this, cx, cz, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true, priority, consumer -+ this, cx, cz, chunkStatus, true, priority, consumer - ); - } - } - } -- // Paper end -+ -+ public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority, -+ java.util.function.Consumer> onLoad) { -+ -+ int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3; -+ int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3; -+ -+ int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3; -+ int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3; -+ -+ int minChunkX = minBlockX >> 4; -+ int maxChunkX = maxBlockX >> 4; -+ -+ int minChunkZ = minBlockZ >> 4; -+ int maxChunkZ = maxBlockZ >> 4; -+ -+ this.loadChunksAsync(minChunkX, maxChunkX, minChunkZ, maxChunkZ, priority, onLoad); -+ } -+ -+ // Paper start - rewrite chunk system -+ public final io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler chunkTaskScheduler; -+ public final io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController chunkDataControllerNew -+ = new io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController(io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.CHUNK_DATA) { -+ -+ @Override -+ public net.minecraft.world.level.chunk.storage.RegionFileStorage getCache() { -+ return ServerLevel.this.getChunkSource().chunkMap.regionFileCache; -+ } -+ -+ @Override -+ public void writeData(int chunkX, int chunkZ, net.minecraft.nbt.CompoundTag compound) throws IOException { -+ ServerLevel.this.getChunkSource().chunkMap.write(new ChunkPos(chunkX, chunkZ), compound); -+ } -+ -+ @Override -+ public net.minecraft.nbt.CompoundTag readData(int chunkX, int chunkZ) throws IOException { -+ return ServerLevel.this.getChunkSource().chunkMap.readSync(new ChunkPos(chunkX, chunkZ)); -+ } -+ }; -+ public final io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController poiDataControllerNew -+ = new io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController(io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.POI_DATA) { -+ -+ @Override -+ public net.minecraft.world.level.chunk.storage.RegionFileStorage getCache() { -+ return ServerLevel.this.getChunkSource().chunkMap.getPoiManager(); -+ } -+ -+ @Override -+ public void writeData(int chunkX, int chunkZ, net.minecraft.nbt.CompoundTag compound) throws IOException { -+ ServerLevel.this.getChunkSource().chunkMap.getPoiManager().write(new ChunkPos(chunkX, chunkZ), compound); -+ } -+ -+ @Override -+ public net.minecraft.nbt.CompoundTag readData(int chunkX, int chunkZ) throws IOException { -+ return ServerLevel.this.getChunkSource().chunkMap.getPoiManager().read(new ChunkPos(chunkX, chunkZ)); -+ } -+ }; -+ public final io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController entityDataControllerNew -+ = new io.papermc.paper.chunk.system.io.RegionFileIOThread.ChunkDataController(io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.ENTITY_DATA) { -+ -+ @Override -+ public net.minecraft.world.level.chunk.storage.RegionFileStorage getCache() { -+ return ServerLevel.this.entityStorage; -+ } -+ -+ @Override -+ public void writeData(int chunkX, int chunkZ, net.minecraft.nbt.CompoundTag compound) throws IOException { -+ ServerLevel.this.writeEntityChunk(chunkX, chunkZ, compound); -+ } -+ -+ @Override -+ public net.minecraft.nbt.CompoundTag readData(int chunkX, int chunkZ) throws IOException { -+ return ServerLevel.this.readEntityChunk(chunkX, chunkZ); -+ } -+ }; -+ private final EntityRegionFileStorage entityStorage; -+ -+ private static final class EntityRegionFileStorage extends net.minecraft.world.level.chunk.storage.RegionFileStorage { -+ -+ public EntityRegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) { -+ super(storageKey, directory, dsync); -+ } -+ -+ protected void write(ChunkPos pos, net.minecraft.nbt.CompoundTag nbt) throws IOException { -+ ChunkPos nbtPos = nbt == null ? null : EntityStorage.readChunkPos(nbt); -+ if (nbtPos != null && !pos.equals(nbtPos)) { -+ throw new IllegalArgumentException( -+ "Entity chunk coordinate and serialized data do not have matching coordinates, trying to serialize coordinate " + pos.toString() -+ + " but compound says coordinate is " + nbtPos + " for world: " + this -+ ); -+ } -+ super.write(pos, nbt); -+ } -+ } -+ -+ private void writeEntityChunk(int chunkX, int chunkZ, net.minecraft.nbt.CompoundTag compound) throws IOException { -+ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) { -+ io.papermc.paper.chunk.system.io.RegionFileIOThread.scheduleSave( -+ this, chunkX, chunkZ, compound, -+ io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.ENTITY_DATA); -+ return; -+ } -+ this.entityStorage.write(new ChunkPos(chunkX, chunkZ), compound); -+ } -+ -+ private net.minecraft.nbt.CompoundTag readEntityChunk(int chunkX, int chunkZ) throws IOException { -+ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) { -+ return io.papermc.paper.chunk.system.io.RegionFileIOThread.loadData( -+ this, chunkX, chunkZ, io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.ENTITY_DATA, -+ io.papermc.paper.chunk.system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread() -+ ); -+ } -+ return this.entityStorage.read(new ChunkPos(chunkX, chunkZ)); -+ } -+ -+ private final io.papermc.paper.chunk.system.entity.EntityLookup entityLookup; -+ public final io.papermc.paper.chunk.system.entity.EntityLookup getEntityLookup() { -+ return this.entityLookup; -+ } -+ -+ private final java.util.concurrent.atomic.AtomicLong nonFullSyncLoadIdGenerator = new java.util.concurrent.atomic.AtomicLong(); -+ -+ private ChunkAccess getIfAboveStatus(int chunkX, int chunkZ, net.minecraft.world.level.chunk.status.ChunkStatus status) { -+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder loaded = -+ this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(chunkX, chunkZ); -+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder.ChunkCompletion loadedCompletion; -+ if (loaded != null && (loadedCompletion = loaded.getLastChunkCompletion()) != null && loadedCompletion.genStatus().isOrAfter(status)) { -+ return loadedCompletion.chunk(); -+ } -+ -+ return null; -+ } -+ -+ @Override -+ public ChunkAccess syncLoadNonFull(int chunkX, int chunkZ, net.minecraft.world.level.chunk.status.ChunkStatus status) { -+ if (status == null || status.isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.FULL)) { -+ throw new IllegalArgumentException("Status: " + status); -+ } -+ ChunkAccess loaded = this.getIfAboveStatus(chunkX, chunkZ, status); -+ if (loaded != null) { -+ return loaded; -+ } -+ -+ Long ticketId = Long.valueOf(this.nonFullSyncLoadIdGenerator.getAndIncrement()); -+ int ticketLevel = 33 + net.minecraft.world.level.chunk.status.ChunkStatus.getDistance(status); -+ this.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel( -+ TicketType.NON_FULL_SYNC_LOAD, chunkX, chunkZ, ticketLevel, ticketId -+ ); -+ this.chunkTaskScheduler.chunkHolderManager.processTicketUpdates(); -+ -+ this.chunkTaskScheduler.beginChunkLoadForNonFullSync(chunkX, chunkZ, status, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.BLOCKING); -+ -+ // we could do a simple spinwait here, since we do not need to process tasks while performing this load -+ // but we process tasks only because it's a better use of the time spent -+ this.chunkSource.mainThreadProcessor.managedBlock(() -> { -+ return ServerLevel.this.getIfAboveStatus(chunkX, chunkZ, status) != null; -+ }); -+ -+ loaded = ServerLevel.this.getIfAboveStatus(chunkX, chunkZ, status); -+ if (loaded == null) { -+ throw new IllegalStateException("Expected chunk to be loaded for status " + status); -+ } -+ -+ this.chunkTaskScheduler.chunkHolderManager.removeTicketAtLevel( -+ TicketType.NON_FULL_SYNC_LOAD, chunkX, chunkZ, ticketLevel, ticketId -+ ); -+ -+ return loaded; -+ } -+ -+ public final int getRegionChunkShift() { -+ // placeholder for folia -+ return io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift(); -+ } -+ // Paper end - rewrite chunk system -+ -+ public final io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader playerChunkLoader = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader(this); -+ private final java.util.concurrent.atomic.AtomicReference viewDistances = new java.util.concurrent.atomic.AtomicReference<>(new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances(-1, -1, -1)); -+ -+ public io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances getViewDistances() { -+ return this.viewDistances.get(); -+ } -+ -+ private void updateViewDistance(final java.util.function.Function update) { -+ for (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances curr = this.viewDistances.get();;) { -+ if (this.viewDistances.compareAndSet(curr, update.apply(curr))) { -+ return; -+ } -+ } -+ } -+ -+ public void setTickViewDistance(final int distance) { -+ if ((distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE)) { -+ throw new IllegalArgumentException("Tick view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE) + ", got: " + distance); -+ } -+ this.updateViewDistance((input) -> { -+ return input.setTickViewDistance(distance); -+ }); -+ } -+ -+ public void setLoadViewDistance(final int distance) { -+ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) { -+ throw new IllegalArgumentException("Load view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance); -+ } -+ this.updateViewDistance((input) -> { -+ return input.setLoadViewDistance(distance); -+ }); -+ } -+ -+ public void setSendViewDistance(final int distance) { -+ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) { -+ throw new IllegalArgumentException("Send view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance); -+ } -+ this.updateViewDistance((input) -> { -+ return input.setSendViewDistance(distance); -+ }); -+ } - - // Paper start - optimise getPlayerByUUID - @Nullable -@@ -382,16 +613,16 @@ public class ServerLevel extends Level implements WorldGenLevel { - // CraftBukkit end - boolean flag2 = minecraftserver.forceSynchronousWrites(); - DataFixer datafixer = minecraftserver.getFixerUpper(); -- EntityPersistentStorage entitypersistentstorage = new EntityStorage(new SimpleRegionStorage(new RegionStorageInfo(convertable_conversionsession.getLevelId(), resourcekey, "entities"), convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), datafixer, flag2, DataFixTypes.ENTITY_CHUNK), this, minecraftserver); -+ this.entityStorage = new EntityRegionFileStorage(new RegionStorageInfo(convertable_conversionsession.getLevelId(), resourcekey, "entities"), convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), flag2); // Paper - rewrite chunk system - -- this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage); -+ // this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage, this.entitySliceManager); // Paper // Paper - rewrite chunk system - StructureTemplateManager structuretemplatemanager = minecraftserver.getStructureManager(); - int j = this.spigotConfig.viewDistance; // Spigot - int k = this.spigotConfig.simulationDistance; // Spigot -- PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; -+ //PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; // Paper - rewrite chunk system - -- Objects.requireNonNull(this.entityManager); -- this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, persistententitysectionmanager::updateChunkStatus, () -> { -+ //Objects.requireNonNull(this.entityManager); // Paper - rewrite chunk system -+ this.chunkSource = new ServerChunkCache(this, convertable_conversionsession, datafixer, structuretemplatemanager, executor, chunkgenerator, j, k, flag2, worldloadlistener, null, () -> { // Paper - rewrite chunk system - return minecraftserver.overworld().getDataStorage(); - }); - this.chunkSource.getGeneratorState().ensureStructuresGenerated(); -@@ -426,6 +657,9 @@ public class ServerLevel extends Level implements WorldGenLevel { - return (RandomSequences) this.getDataStorage().computeIfAbsent(RandomSequences.factory(l), "random_sequences"); - }); - this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit -+ -+ this.chunkTaskScheduler = new io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler(this, io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.workerThreads); // Paper - rewrite chunk system -+ this.entityLookup = new io.papermc.paper.chunk.system.entity.EntityLookup(this, new EntityCallbacks()); // Paper - rewrite chunk system - } - - // Paper start -@@ -558,7 +792,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - gameprofilerfiller.push("checkDespawn"); - entity.checkDespawn(); - gameprofilerfiller.pop(); -- if (this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { -+ if (true || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { // Paper - now always true if in the ticking list - Entity entity1 = entity.getVehicle(); - - if (entity1 != null) { -@@ -583,13 +817,16 @@ public class ServerLevel extends Level implements WorldGenLevel { - } - - gameprofilerfiller.push("entityManagement"); -- this.entityManager.tick(); -+ //this.entityManager.tick(); // Paper - rewrite chunk system - gameprofilerfiller.pop(); - } - - @Override - public boolean shouldTickBlocksAt(long chunkPos) { -- return this.chunkSource.chunkMap.getDistanceManager().inBlockTickingRange(chunkPos); -+ // Paper start - replace player chunk loader system -+ ChunkHolder holder = this.chunkSource.chunkMap.getVisibleChunkIfPresent(chunkPos); -+ return holder != null && holder.isTickingReady(); -+ // Paper end - replace player chunk loader system - } - - protected void tickTime() { -@@ -1066,6 +1303,11 @@ public class ServerLevel extends Level implements WorldGenLevel { - } - - public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled) { -+ // Paper start - rewrite chunk system - add close param -+ this.save(progressListener, flush, savingDisabled, false); -+ } -+ public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled, boolean close) { -+ // Paper end - rewrite chunk system - add close param - ServerChunkCache chunkproviderserver = this.getChunkSource(); - - if (!savingDisabled) { -@@ -1081,16 +1323,13 @@ public class ServerLevel extends Level implements WorldGenLevel { - } - - timings.worldSaveChunks.startTiming(); // Paper -- chunkproviderserver.save(flush); -+ if (!close) chunkproviderserver.save(flush); // Paper - rewrite chunk system -+ if (close) chunkproviderserver.close(true); // Paper - rewrite chunk system - timings.worldSaveChunks.stopTiming(); // Paper - }// Paper -- if (flush) { -- this.entityManager.saveAll(); -- } else { -- this.entityManager.autoSave(); -- } -+ // Paper - rewrite chunk system - entity saving moved into ChunkHolder - -- } -+ } else if (close) { chunkproviderserver.close(false); } // Paper - rewrite chunk system - - // CraftBukkit start - moved from MinecraftServer.saveChunks - ServerLevel worldserver1 = this; -@@ -1226,7 +1465,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - this.removePlayerImmediately((ServerPlayer) entity, Entity.RemovalReason.DISCARDED); - } - -- this.entityManager.addNewEntity(player); -+ this.entityLookup.addNewEntity(player); // Paper - rewite chunk system - } - - // CraftBukkit start -@@ -1257,7 +1496,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - } - // CraftBukkit end - -- return this.entityManager.addNewEntity(entity); -+ return this.entityLookup.addNewEntity(entity); // Paper - rewrite chunk system - } - } - -@@ -1269,10 +1508,10 @@ public class ServerLevel extends Level implements WorldGenLevel { - public boolean tryAddFreshEntityWithPassengers(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) { - // CraftBukkit end - Stream stream = entity.getSelfAndPassengers().map(Entity::getUUID); // CraftBukkit - decompile error -- PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; -+ //PersistentEntitySectionManager persistententitysectionmanager = this.entityManager; // Paper - rewrite chunk system - -- Objects.requireNonNull(this.entityManager); -- if (stream.anyMatch(persistententitysectionmanager::isLoaded)) { -+ //Objects.requireNonNull(this.entityManager); // Paper - rewrite chunk system -+ if (stream.anyMatch(this.entityLookup::hasEntity)) { // Paper - rewrite chunk system - return false; - } else { - this.addFreshEntityWithPassengers(entity, reason); // CraftBukkit -@@ -1858,7 +2097,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - } - } - -- bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.entityManager.gatherStats())); -+ bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.entityLookup.getDebugInfo())); // Paper - rewrite chunk system - bufferedwriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size())); - bufferedwriter.write(String.format(Locale.ROOT, "block_ticks: %d\n", this.getBlockTicks().count())); - bufferedwriter.write(String.format(Locale.ROOT, "fluid_ticks: %d\n", this.getFluidTicks().count())); -@@ -1907,7 +2146,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - BufferedWriter bufferedwriter2 = Files.newBufferedWriter(path1); - - try { -- playerchunkmap.dumpChunks(bufferedwriter2); -+ //playerchunkmap.dumpChunks(bufferedwriter2); // Paper - rewrite chunk system - } catch (Throwable throwable4) { - if (bufferedwriter2 != null) { - try { -@@ -1928,7 +2167,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - BufferedWriter bufferedwriter3 = Files.newBufferedWriter(path2); - - try { -- this.entityManager.dumpSections(bufferedwriter3); -+ //this.entityManager.dumpSections(bufferedwriter3); // Paper - rewrite chunk system - } catch (Throwable throwable6) { - if (bufferedwriter3 != null) { - try { -@@ -2070,7 +2309,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - - @VisibleForTesting - public String getWatchdogStats() { -- return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.entityManager.gatherStats(), ServerLevel.getTypeCount(this.entityManager.getEntityGetter().getAll(), (entity) -> { -+ return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.entityLookup.getDebugInfo(), ServerLevel.getTypeCount(this.entityLookup.getAll(), (entity) -> { // Paper - rewrite chunk system - return BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString(); - }), this.blockEntityTickers.size(), ServerLevel.getTypeCount(this.blockEntityTickers, TickingBlockEntity::getType), this.getBlockTicks().count(), this.getFluidTicks().count(), this.gatherChunkSourceStats()); - } -@@ -2130,15 +2369,15 @@ public class ServerLevel extends Level implements WorldGenLevel { - @Override - public LevelEntityGetter getEntities() { - org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot -- return this.entityManager.getEntityGetter(); -+ return this.entityLookup; // Paper - rewrite chunk system - } - -- public void addLegacyChunkEntities(Stream entities) { -- this.entityManager.addLegacyChunkEntities(entities); -+ public void addLegacyChunkEntities(Stream entities, ChunkPos forChunk) { // Paper - rewrite chunk system -+ this.entityLookup.addLegacyChunkEntities(entities.toList(), forChunk); // Paper - rewrite chunk system - } - -- public void addWorldGenChunkEntities(Stream entities) { -- this.entityManager.addWorldGenChunkEntities(entities); -+ public void addWorldGenChunkEntities(Stream entities, ChunkPos forChunk) { // Paper - rewrite chunk system -+ this.entityLookup.addWorldGenChunkEntities(entities.toList(), forChunk); // Paper - rewrite chunk system - } - - public void startTickingChunk(LevelChunk chunk) { -@@ -2158,34 +2397,49 @@ public class ServerLevel extends Level implements WorldGenLevel { - @Override - public void close() throws IOException { - super.close(); -- this.entityManager.close(); -+ //this.entityManager.close(); // Paper - rewrite chunk system - } - - @Override - public String gatherChunkSourceStats() { - String s = this.chunkSource.gatherStats(); - -- return "Chunks[S] W: " + s + " E: " + this.entityManager.gatherStats(); -+ return "Chunks[S] W: " + s + " E: " + this.entityLookup.getDebugInfo(); // Paper - rewrite chunk system - } - - public boolean areEntitiesLoaded(long chunkPos) { -- return this.entityManager.areEntitiesLoaded(chunkPos); -+ // Paper start - rewrite chunk system -+ return this.getChunkIfLoadedImmediately(ChunkPos.getX(chunkPos), ChunkPos.getZ(chunkPos)) != null; -+ // Paper end - rewrite chunk system - } - - private boolean isPositionTickingWithEntitiesLoaded(long chunkPos) { -- return this.areEntitiesLoaded(chunkPos) && this.chunkSource.isPositionTicking(chunkPos); -+ // Paper start - optimize is ticking ready type functions -+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(chunkPos); -+ // isTicking implies the chunk is loaded, and the chunk is loaded now implies the entities are loaded -+ return chunkHolder != null && chunkHolder.isTickingReady(); -+ // Paper end - } - - public boolean isPositionEntityTicking(BlockPos pos) { -- return this.entityManager.canPositionTick(pos) && this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(ChunkPos.asLong(pos)); -+ // Paper start - rewrite chunk system -+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(io.papermc.paper.util.CoordinateUtils.getChunkKey(pos)); -+ return chunkHolder != null && chunkHolder.isEntityTickingReady(); -+ // Paper end - rewrite chunk system - } - - public boolean isNaturalSpawningAllowed(BlockPos pos) { -- return this.entityManager.canPositionTick(pos); -+ // Paper start - rewrite chunk system -+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(io.papermc.paper.util.CoordinateUtils.getChunkKey(pos)); -+ return chunkHolder != null && chunkHolder.isEntityTickingReady(); -+ // Paper end - rewrite chunk system - } - - public boolean isNaturalSpawningAllowed(ChunkPos pos) { -- return this.entityManager.canPositionTick(pos); -+ // Paper start - rewrite chunk system -+ io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder = this.chunkTaskScheduler.chunkHolderManager.getChunkHolder(io.papermc.paper.util.CoordinateUtils.getChunkKey(pos)); -+ return chunkHolder != null && chunkHolder.isEntityTickingReady(); -+ // Paper end - rewrite chunk system - } - - @Override -@@ -2211,7 +2465,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - CrashReportCategory crashreportsystemdetails = super.fillReportDetails(report); - - crashreportsystemdetails.setDetail("Loaded entity count", () -> { -- return String.valueOf(this.entityManager.count()); -+ return String.valueOf(this.entityLookup.getAllCopy().length); // Paper - }); - return crashreportsystemdetails; - } -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index bee8cc19d7912d2ab1b731c47cf7e31d21d61866..65e6623c2d1a6cf48729f78948fc48e82057114c 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -292,6 +292,50 @@ public class ServerPlayer extends Player { - public @Nullable String clientBrandName = null; // Paper - Brand support - public org.bukkit.event.player.PlayerQuitEvent.QuitReason quitReason = null; // Paper - Add API for quit reason; there are a lot of changes to do if we change all methods leading to the event - -+ // Paper start - replace player chunk loader -+ private final java.util.concurrent.atomic.AtomicReference viewDistances = new java.util.concurrent.atomic.AtomicReference<>(new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances(-1, -1, -1)); -+ public io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.PlayerChunkLoaderData chunkLoader; -+ -+ public io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances getViewDistances() { -+ return this.viewDistances.get(); -+ } -+ -+ private void updateViewDistance(final java.util.function.Function update) { -+ for (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.ViewDistances curr = this.viewDistances.get();;) { -+ if (this.viewDistances.compareAndSet(curr, update.apply(curr))) { -+ return; -+ } -+ } -+ } -+ -+ public void setTickViewDistance(final int distance) { -+ if ((distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE)) { -+ throw new IllegalArgumentException("Tick view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE) + ", got: " + distance); -+ } -+ this.updateViewDistance((input) -> { -+ return input.setTickViewDistance(distance); -+ }); -+ } -+ -+ public void setLoadViewDistance(final int distance) { -+ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) { -+ throw new IllegalArgumentException("Load view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance); -+ } -+ this.updateViewDistance((input) -> { -+ return input.setLoadViewDistance(distance); -+ }); -+ } -+ -+ public void setSendViewDistance(final int distance) { -+ if (distance != -1 && (distance < io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE || distance > io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1)) { -+ throw new IllegalArgumentException("Send view distance must be a number between " + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MIN_VIEW_DISTANCE + " and " + (io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.MAX_VIEW_DISTANCE + 1) + " or -1, got: " + distance); -+ } -+ this.updateViewDistance((input) -> { -+ return input.setSendViewDistance(distance); -+ }); -+ } -+ // Paper end - replace player chunk loader -+ - public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) { - super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile); - this.chatVisibility = ChatVisiblity.FULL; -diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java -index f206df06a7d8895175db31d4a840d7467ffe826f..8ef22f8f0d6da49247a90152e5cfa9ffc7f596a4 100644 ---- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java -+++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java -@@ -37,15 +37,12 @@ import net.minecraft.world.level.chunk.status.ChunkStatus; - public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable { - public static final int DEFAULT_BATCH_SIZE = 1000; - private static final Logger LOGGER = LogUtils.getLogger(); -- private final ProcessorMailbox taskMailbox; -- private final ObjectList> lightTasks = new ObjectArrayList<>(); -+ // Paper - rewrite chunk system - private final ChunkMap chunkMap; -- private final ProcessorHandle> sorterMailbox; -- private final int taskPerBatch = 1000; -- private final AtomicBoolean scheduled = new AtomicBoolean(); -+ // Paper - rewrite chunk system - - // Paper start - replace light engine impl -- protected final ca.spottedleaf.starlight.common.light.StarLightInterface theLightEngine; -+ public final ca.spottedleaf.starlight.common.light.StarLightInterface theLightEngine; - public final boolean hasBlockLight; - public final boolean hasSkyLight; - // Paper end - replace light engine impl -@@ -59,8 +56,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - ) { - super(chunkProvider, false, false); // Paper - destroy vanilla light engine state - this.chunkMap = chunkStorage; -- this.sorterMailbox = executor; -- this.taskMailbox = processor; -+ // Paper - rewrite chunk system - // Paper start - replace light engine impl - this.hasBlockLight = true; - this.hasSkyLight = hasBlockLight; // Nice variable name. -@@ -104,7 +100,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - ++totalChunks; - } - -- this.taskMailbox.tell(() -> { -+ this.chunkMap.level.chunkTaskScheduler.radiusAwareScheduler.queueInfiniteRadiusTask(() -> { // Paper - rewrite chunk system - this.theLightEngine.relightChunks(chunks, (ChunkPos chunkPos) -> { - chunkLightCallback.accept(chunkPos); - ((java.util.concurrent.Executor)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().mainThreadProcessor).execute(() -> { -@@ -121,7 +117,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap(); - - private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ, -- final Supplier runnable) { -+ final Supplier runnable) { // Paper - rewrite chunk system - final ServerLevel world = (ServerLevel)this.theLightEngine.getWorld(); - - final ChunkAccess center = this.theLightEngine.getAnyChunkNow(chunkX, chunkZ); -@@ -148,7 +144,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - - final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); - -- final ca.spottedleaf.starlight.common.light.StarLightInterface.LightQueue.ChunkTasks updateFuture = runnable.get(); -+ final io.papermc.paper.chunk.system.light.LightQueue.ChunkTasks updateFuture = runnable.get(); // Paper - rewrite chunk system - - if (updateFuture == null) { - // not scheduled -@@ -285,16 +281,11 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - } - - private void addTask(int x, int z, ThreadedLevelLightEngine.TaskType stage, Runnable task) { -- this.addTask(x, z, this.chunkMap.getChunkQueueLevel(ChunkPos.asLong(x, z)), stage, task); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - private void addTask(int x, int z, IntSupplier completedLevelSupplier, ThreadedLevelLightEngine.TaskType stage, Runnable task) { -- this.sorterMailbox.tell(ChunkTaskPriorityQueueSorter.message(() -> { -- this.lightTasks.add(Pair.of(stage, task)); -- if (this.lightTasks.size() >= 1000) { -- this.runUpdate(); -- } -- }, ChunkPos.asLong(x, z), completedLevelSupplier)); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @Override -@@ -327,83 +318,15 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl - } - - public CompletableFuture lightChunk(ChunkAccess chunk, boolean excludeBlocks) { -- // Paper start - replace light engine impl -- if (true) { -- boolean lit = excludeBlocks; -- final ChunkPos chunkPos = chunk.getPos(); -- -- return CompletableFuture.supplyAsync(() -> { -- final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(chunk); -- if (!lit) { -- chunk.setLightCorrect(false); -- this.theLightEngine.lightChunk(chunk, emptySections); -- chunk.setLightCorrect(true); -- } else { -- this.theLightEngine.forceLoadInChunk(chunk, 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(chunkPos.x, chunkPos.z); -- } -- -- this.chunkMap.releaseLightTicket(chunkPos); -- return chunk; -- }, (runnable) -> { -- this.theLightEngine.scheduleChunkLight(chunkPos, runnable); -- this.tryScheduleUpdate(); -- }).whenComplete((final ChunkAccess c, final Throwable throwable) -> { -- if (throwable != null) { -- LOGGER.error("Failed to light chunk " + chunkPos, throwable); -- } -- }); -- } -- // Paper end - replace light engine impl -- ChunkPos chunkPos = chunk.getPos(); -- chunk.setLightCorrect(false); -- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { -- if (!excludeBlocks) { -- super.propagateLightSources(chunkPos); -- } -- }, () -> "lightChunk " + chunkPos + " " + excludeBlocks)); -- return CompletableFuture.supplyAsync(() -> { -- chunk.setLightCorrect(true); -- this.chunkMap.releaseLightTicket(chunkPos); -- return chunk; -- }, task -> this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, task)); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public void tryScheduleUpdate() { -- if (this.hasLightWork() && this.scheduled.compareAndSet(false, true)) { // Paper // Paper - rewrite light engine -- this.taskMailbox.tell(() -> { -- this.runUpdate(); -- this.scheduled.set(false); -- }); -- } -+ // Paper - rewrite chunk system - } - - private void runUpdate() { -- int i = Math.min(this.lightTasks.size(), 1000); -- ObjectListIterator> objectListIterator = this.lightTasks.iterator(); -- -- int j; -- for (j = 0; objectListIterator.hasNext() && j < i; j++) { -- Pair pair = objectListIterator.next(); -- if (pair.getFirst() == ThreadedLevelLightEngine.TaskType.PRE_UPDATE) { -- pair.getSecond().run(); -- } -- } -- -- objectListIterator.back(j); -- this.theLightEngine.propagateChanges(); // Paper - rewrite light engine -- -- for (int var5 = 0; objectListIterator.hasNext() && var5 < i; var5++) { -- Pair pair2 = objectListIterator.next(); -- if (pair2.getFirst() == ThreadedLevelLightEngine.TaskType.POST_UPDATE) { -- pair2.getSecond().run(); -- } -- -- objectListIterator.remove(); -- } -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - public CompletableFuture waitForPendingTasks(int x, int z) { -diff --git a/src/main/java/net/minecraft/server/level/Ticket.java b/src/main/java/net/minecraft/server/level/Ticket.java -index eba83b085435150e5954fd5d41dda9ce1d0601ad..e97329f867de2acbdd666925ba5d2aafa7a90574 100644 ---- a/src/main/java/net/minecraft/server/level/Ticket.java -+++ b/src/main/java/net/minecraft/server/level/Ticket.java -@@ -6,9 +6,12 @@ public final class Ticket implements Comparable> { - private final TicketType type; - private final int ticketLevel; - public final T key; -- private long createdTick; -+ // Paper start - rewrite chunk system -+ public long removeDelay; - -- protected Ticket(TicketType type, int level, T argument) { -+ public Ticket(TicketType type, int level, T argument, long removeDelay) { -+ this.removeDelay = removeDelay; -+ // Paper end - rewrite chunk system - this.type = type; - this.ticketLevel = level; - this.key = argument; -@@ -41,7 +44,7 @@ public final class Ticket implements Comparable> { - - @Override - public String toString() { -- return "Ticket[" + this.type + " " + this.ticketLevel + " (" + this.key + ")] at " + this.createdTick; -+ return "Ticket[" + this.type + " " + this.ticketLevel + " (" + this.key + ")] to die in " + this.removeDelay; // Paper - rewrite chunk system - } - - public TicketType getType() { -@@ -53,11 +56,10 @@ public final class Ticket implements Comparable> { - } - - protected void setCreatedTick(long tickCreated) { -- this.createdTick = tickCreated; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - protected boolean timedOut(long currentTick) { -- long l = this.type.timeout(); -- return l != 0L && currentTick - this.createdTick > l; -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - } -diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java -index 6051e5f272838ef23276a90e21c2fc821ca155d1..658e63ebde81dc14c8ab5850fb246dc0aab25dea 100644 ---- a/src/main/java/net/minecraft/server/level/TicketType.java -+++ b/src/main/java/net/minecraft/server/level/TicketType.java -@@ -8,6 +8,7 @@ import net.minecraft.world.level.ChunkPos; - - public class TicketType { - public static final TicketType FUTURE_AWAIT = create("future_await", Long::compareTo); // Paper -+ public static final TicketType ASYNC_LOAD = create("async_load", Long::compareTo); // Paper - - private final String name; - private final Comparator comparator; -@@ -27,6 +28,15 @@ public class TicketType { - public static final TicketType PLUGIN = TicketType.create("plugin", (a, b) -> 0); // CraftBukkit - public static final TicketType PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit - public static final TicketType CHUNK_RELIGHT = create("light_update", Long::compareTo); // Paper - ensure chunks stay loaded for lighting -+ // Paper start - rewrite chunk system -+ public static final TicketType CHUNK_LOAD = create("chunk_load", Long::compareTo); -+ public static final TicketType STATUS_UPGRADE = create("status_upgrade", Long::compareTo); -+ public static final TicketType ENTITY_LOAD = create("entity_load", Long::compareTo); -+ public static final TicketType POI_LOAD = create("poi_load", Long::compareTo); -+ public static final TicketType UNLOAD_COOLDOWN = create("unload_cooldown", (u1, u2) -> 0, 5 * 20); -+ public static final TicketType NON_FULL_SYNC_LOAD = create("non_full_sync_load", Long::compareTo); -+ public static final TicketType DELAY_UNLOAD = create("delay_unload", Comparator.comparingLong(ChunkPos::toLong), 1); -+ // Paper end - rewrite chunk system - - public static TicketType create(String name, Comparator argumentComparator) { - return new TicketType<>(name, argumentComparator, 0L); -diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java -index ca4c8e256047a4af45811c3e772b5a959e2ae941..1351423a12c19a01f602a202832372a399e6a867 100644 ---- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java -+++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java -@@ -544,4 +544,21 @@ public class WorldGenRegion implements WorldGenLevel { - public long nextSubTickCount() { - return this.subTickCount.getAndIncrement(); - } -+ -+ // Paper start -+ // No-op, this class doesn't provide entity access -+ @Override -+ public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { -+ return Collections.emptyList(); -+ } -+ -+ @Override -+ public void getEntities(Entity except, AABB box, Predicate predicate, List into) {} -+ -+ @Override -+ public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) {} -+ -+ @Override -+ public void getEntitiesByClass(Class clazz, Entity except, AABB box, List into, Predicate predicate) {} -+ // Paper end - } -diff --git a/src/main/java/net/minecraft/server/network/PlayerChunkSender.java b/src/main/java/net/minecraft/server/network/PlayerChunkSender.java -index c502d9b85eb68b277ae17dfea34e0475f0156647..27d0f1ed58948039004f8f1eba2f7f9609fdeec0 100644 ---- a/src/main/java/net/minecraft/server/network/PlayerChunkSender.java -+++ b/src/main/java/net/minecraft/server/network/PlayerChunkSender.java -@@ -43,16 +43,23 @@ public class PlayerChunkSender { - - public void dropChunk(ServerPlayer player, ChunkPos pos) { - if (!this.pendingChunks.remove(pos.toLong()) && player.isAlive()) { -+ // Paper start - rewrite player chunk loader -+ dropChunkStatic(player, pos); -+ } -+ } -+ public static void dropChunkStatic(ServerPlayer player, ChunkPos pos) { -+ player.serverLevel().chunkSource.chunkMap.getVisibleChunkIfPresent(pos.toLong()).removePlayer(player); - player.connection.send(new ClientboundForgetLevelChunkPacket(pos)); - // Paper start - PlayerChunkUnloadEvent - if (io.papermc.paper.event.packet.PlayerChunkUnloadEvent.getHandlerList().getRegisteredListeners().length > 0) { - new io.papermc.paper.event.packet.PlayerChunkUnloadEvent(player.getBukkitEntity().getWorld().getChunkAt(pos.longKey), player.getBukkitEntity()).callEvent(); - } - // Paper end - PlayerChunkUnloadEvent -- } - } -+ // Paper end - rewrite player chunk loader - - public void sendNextChunks(ServerPlayer player) { -+ if (true) return; // Paper - rewrite player chunk loader - if (this.unacknowledgedBatches < this.maxUnacknowledgedBatches) { - float f = Math.max(1.0F, this.desiredChunksPerTick); - this.batchQuota = Math.min(this.batchQuota + this.desiredChunksPerTick, f); -@@ -78,7 +85,8 @@ public class PlayerChunkSender { - } - } - -- private static void sendChunk(ServerGamePacketListenerImpl handler, ServerLevel world, LevelChunk chunk) { -+ public static void sendChunk(ServerGamePacketListenerImpl handler, ServerLevel world, LevelChunk chunk) { // Paper - rewrite chunk loader - public -+ handler.player.serverLevel().chunkSource.chunkMap.getVisibleChunkIfPresent(chunk.getPos().toLong()).addPlayer(handler.player); - handler.send(new ClientboundLevelChunkWithLightPacket(chunk, world.getLightEngine(), null, null)); - // Paper start - PlayerChunkLoadEvent - if (io.papermc.paper.event.packet.PlayerChunkLoadEvent.getHandlerList().getRegisteredListeners().length > 0) { -@@ -118,6 +126,7 @@ public class PlayerChunkSender { - } - - public void onChunkBatchReceivedByClient(float desiredBatchSize) { -+ if (true) return; // Paper - rewrite player chunk loader - this.unacknowledgedBatches--; - this.desiredChunksPerTick = Double.isNaN((double)desiredBatchSize) ? 0.01F : Mth.clamp(desiredBatchSize, 0.01F, 64.0F); - if (this.unacknowledgedBatches == 0) { -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 0aa28caa1254137c0bae8e213bd08c9a654f5335..c4b4e5f5c9366b241686e881cda34568a57b4877 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -296,7 +296,7 @@ public abstract class PlayerList { - boolean flag2 = gamerules.getBoolean(GameRules.RULE_LIMITED_CRAFTING); - - // Spigot - view distance -- playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), this.server.levelKeys(), this.getMaxPlayers(), worldserver1.spigotConfig.viewDistance, worldserver1.spigotConfig.simulationDistance, flag1, !flag, flag2, player.createCommonSpawnInfo(worldserver1), this.server.enforceSecureProfile())); -+ playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), this.server.levelKeys(), this.getMaxPlayers(), worldserver1.getWorld().getSendViewDistance(), worldserver1.getWorld().getSimulationDistance(), flag1, !flag, flag2, player.createCommonSpawnInfo(worldserver1), this.server.enforceSecureProfile())); // Paper - replace old player chunk management - player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit - playerconnection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked())); - playerconnection.send(new ClientboundPlayerAbilitiesPacket(player.getAbilities())); -@@ -943,8 +943,8 @@ public abstract class PlayerList { - LevelData worlddata = worldserver2.getLevelData(); - - entityplayer1.connection.send(new ClientboundRespawnPacket(entityplayer1.createCommonSpawnInfo(worldserver2), (byte) i)); -- entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.spigotConfig.viewDistance)); // Spigot -- entityplayer1.connection.send(new ClientboundSetSimulationDistancePacket(worldserver1.spigotConfig.simulationDistance)); // Spigot -+ entityplayer1.connection.send(new ClientboundSetChunkCacheRadiusPacket(worldserver1.getWorld().getSendViewDistance())); // Spigot // Paper - replace old player chunk management -+ entityplayer1.connection.send(new ClientboundSetSimulationDistancePacket(worldserver1.getWorld().getSimulationDistance())); // Spigot // Paper - replace old player chunk management - entityplayer1.connection.teleport(CraftLocation.toBukkit(entityplayer1.position(), worldserver2.getWorld(), entityplayer1.getYRot(), entityplayer1.getXRot())); // CraftBukkit - entityplayer1.connection.send(new ClientboundSetDefaultSpawnPositionPacket(worldserver1.getSharedSpawnPos(), worldserver1.getSharedSpawnAngle())); - entityplayer1.connection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked())); -@@ -1496,7 +1496,7 @@ public abstract class PlayerList { - - public void setViewDistance(int viewDistance) { - this.viewDistance = viewDistance; -- this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); -+ //this.broadcastAll(new ClientboundSetChunkCacheRadiusPacket(viewDistance)); // Paper - move into setViewDistance - Iterator iterator = this.server.getAllLevels().iterator(); - - while (iterator.hasNext()) { -@@ -1511,7 +1511,7 @@ public abstract class PlayerList { - - public void setSimulationDistance(int simulationDistance) { - this.simulationDistance = simulationDistance; -- this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); -+ //this.broadcastAll(new ClientboundSetSimulationDistancePacket(simulationDistance)); // Paper - handled by playerchunkloader - Iterator iterator = this.server.getAllLevels().iterator(); - - while (iterator.hasNext()) { -diff --git a/src/main/java/net/minecraft/util/SortedArraySet.java b/src/main/java/net/minecraft/util/SortedArraySet.java -index ea72dcb064a35bc6245bc5c94d592efedd8faf41..0793dfe47e68a2b48b010aad5b12dcfa1701293a 100644 ---- a/src/main/java/net/minecraft/util/SortedArraySet.java -+++ b/src/main/java/net/minecraft/util/SortedArraySet.java -@@ -14,6 +14,14 @@ public class SortedArraySet extends AbstractSet { - T[] contents; - int size; - -+ // Paper start - rewrite chunk system -+ public SortedArraySet(final SortedArraySet other) { -+ this.comparator = other.comparator; -+ this.size = other.size; -+ this.contents = Arrays.copyOf(other.contents, this.size); -+ } -+ // Paper end - rewrite chunk system -+ - private SortedArraySet(int initialCapacity, Comparator comparator) { - this.comparator = comparator; - if (initialCapacity < 0) { -@@ -22,6 +30,41 @@ public class SortedArraySet extends AbstractSet { - this.contents = (T[])castRawArray(new Object[initialCapacity]); - } - } -+ // Paper start - optimise removeIf -+ @Override -+ public boolean removeIf(java.util.function.Predicate filter) { -+ // prev. impl used an iterator, which could be n^2 and creates garbage -+ int i = 0, len = this.size; -+ T[] backingArray = this.contents; -+ -+ for (;;) { -+ if (i >= len) { -+ return false; -+ } -+ if (!filter.test(backingArray[i])) { -+ ++i; -+ continue; -+ } -+ break; -+ } -+ -+ // we only want to write back to backingArray if we really need to -+ -+ int lastIndex = i; // this is where new elements are shifted to -+ -+ for (; i < len; ++i) { -+ T curr = backingArray[i]; -+ if (!filter.test(curr)) { // if test throws we're screwed -+ backingArray[lastIndex++] = curr; -+ } -+ } -+ -+ // cleanup end -+ Arrays.fill(backingArray, lastIndex, len, null); -+ this.size = lastIndex; -+ return true; -+ } -+ // Paper end - optimise removeIf - - public static > SortedArraySet create() { - return create(10); -@@ -110,6 +153,31 @@ public class SortedArraySet extends AbstractSet { - } - } - -+ // Paper start - rewrite chunk system -+ public T replace(T object) { -+ int i = this.findIndex(object); -+ if (i >= 0) { -+ T old = this.contents[i]; -+ this.contents[i] = object; -+ return old; -+ } else { -+ this.addInternal(object, getInsertionPosition(i)); -+ return object; -+ } -+ } -+ -+ public T removeAndGet(T object) { -+ int i = this.findIndex(object); -+ if (i >= 0) { -+ final T ret = this.contents[i]; -+ this.removeInternal(i); -+ return ret; -+ } else { -+ return null; -+ } -+ } -+ // Paper end - rewrite chunk system -+ - @Override - public boolean remove(Object object) { - int i = this.findIndex((T)object); -diff --git a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java -index 7984f17cd9c4cef8100909b6c33b3144c8096fcf..639f72618a7c22fa94effa9d0406b97fffc64cb5 100644 ---- a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java -+++ b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java -@@ -227,7 +227,13 @@ public class WorldUpgrader { - this.previousWriteFuture.join(); - } - -+ // Paper start - async chunk io -+ try { - this.previousWriteFuture = storage.write(chunkPos, nbttagcompound1); -+ } catch (final IOException e) { -+ com.destroystokyo.paper.util.SneakyThrow.sneaky(e); -+ } -+ // Paper end - async chunk io - return true; - } - } -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 3c1bcf8d1a07b35a8688160c9f05f792451338a3..03840f520624662d4ce3ac9f3065a01c71b5f299 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -482,6 +482,58 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - // Paper end - -+ // Paper 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<>()); -+ { -+ /* // Goodbye, broken on reobf... -+ Boolean hardCollides = cachedOverrides.get(this.getClass()); -+ if (hardCollides == null) { -+ try { -+ java.lang.reflect.Method getHardCollisionBoxEntityMethod = Entity.class.getMethod("canCollideWith", Entity.class); -+ java.lang.reflect.Method hasHardCollisionBoxMethod = Entity.class.getMethod("canBeCollidedWith"); -+ 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 = this instanceof Boat -+ || this instanceof net.minecraft.world.entity.monster.Shulker -+ || this instanceof net.minecraft.world.entity.vehicle.AbstractMinecart -+ || this.shouldHardCollide(); -+ } -+ -+ // plugins can override -+ protected boolean shouldHardCollide() { -+ return false; -+ } -+ -+ public final boolean hardCollides() { -+ return this.hardCollides; -+ } -+ -+ public net.minecraft.server.level.FullChunkStatus chunkStatus; -+ -+ public int sectionX = Integer.MIN_VALUE; -+ public int sectionY = Integer.MIN_VALUE; -+ public int sectionZ = Integer.MIN_VALUE; -+ -+ public boolean updatingSectionStatus = false; -+ // Paper end -+ - public Entity(EntityType type, Level world) { - this.id = Entity.ENTITY_COUNTER.incrementAndGet(); - this.passengers = ImmutableList.of(); -@@ -2608,11 +2660,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - return InteractionResult.PASS; - } - -- public boolean canCollideWith(Entity other) { -+ public boolean canCollideWith(Entity other) { // Paper - diff on change, hard colliding entities override this - TODO CHECK ON UPDATE - AbstractMinecart/Boat override - return other.canBeCollidedWith() && !this.isPassengerOfSameVehicle(other); - } - -- public boolean canBeCollidedWith() { -+ public boolean canBeCollidedWith() { // Paper - diff on change, hard colliding entities override this TODO CHECK ON UPDATE - Boat/Shulker override - return false; - } - -@@ -4037,6 +4089,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - }).count(); - } - -+ // Paper start - rewrite chunk system -+ public boolean hasAnyPlayerPassengers() { -+ // copied from below -+ if (this.passengers.isEmpty()) { return false; } -+ return this.getIndirectPassengersStream().anyMatch((entity) -> entity instanceof Player); -+ } -+ // Paper end - rewrite chunk system - public boolean hasExactlyOnePlayerPassenger() { - if (this.passengers.isEmpty()) { return false; } // Paper - Optimize indirect passenger iteration - return this.countPlayerPassengers() == 1; -@@ -4387,6 +4446,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - return; - } - // Paper end - Block invalid positions and bounding box -+ // Paper start - rewrite chunk system -+ if (this.updatingSectionStatus) { -+ LOGGER.error("Refusing to update position for entity {} to position {} since it is processing a section status update", this, new Vec3(x, y, z), new Throwable()); -+ return; -+ } -+ // Paper end - rewrite chunk system - // Paper start - Fix MC-4 - if (this instanceof ItemEntity) { - if (io.papermc.paper.configuration.GlobalConfiguration.get().misc.fixEntityPositionDesync) { -@@ -4514,6 +4579,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - - @Override - public final void setRemoved(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) { -+ // Paper start - rewrite chunk system -+ io.papermc.paper.util.TickThread.ensureTickThread(this, "Cannot remove entity off-main"); -+ if (!((ServerLevel)this.level).getEntityLookup().canRemoveEntity(this)) { -+ LOGGER.warn("Entity " + this + " is currently prevented from being removed from the world since it is processing section status updates", new Throwable()); -+ return; -+ } -+ // Paper end - rewrite chunk system - CraftEventFactory.callEntityRemoveEvent(this, cause); - // CraftBukkit end - final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers -@@ -4525,7 +4597,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - this.stopRiding(); - } - -- this.getPassengers().forEach(Entity::stopRiding); -+ if (entity_removalreason != RemovalReason.UNLOADED_TO_CHUNK) this.getPassengers().forEach(Entity::stopRiding); // Paper - chunk system - don't adjust passenger state when unloading, it's just not safe (and messes with our logic in entity chunk unload) - this.levelCallback.onRemove(entity_removalreason); - // Paper start - Folia schedulers - if (!(this instanceof ServerPlayer) && entity_removalreason != RemovalReason.CHANGED_DIMENSION && !alreadyRemoved) { -@@ -4556,7 +4628,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - - @Override - public boolean shouldBeSaved() { -- return this.removalReason != null && !this.removalReason.shouldSave() ? false : (this.isPassenger() ? false : !this.isVehicle() || !this.hasExactlyOnePlayerPassenger()); -+ return this.removalReason != null && !this.removalReason.shouldSave() ? false : (this.isPassenger() ? false : !this.isVehicle() || !this.hasAnyPlayerPassengers()); // Paper - rewrite chunk system - it should check if the entity has ANY player passengers - } - - @Override -diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java -index 71d8909f35a22256406a2232d21adfd7d94dc3a5..7b52b0507cbda76aee1db954641f397bef51f94d 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java -+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java -@@ -40,20 +40,40 @@ import net.minecraft.world.level.chunk.storage.SimpleRegionStorage; - public class PoiManager extends SectionStorage { - public static final int MAX_VILLAGE_DISTANCE = 6; - public static final int VILLAGE_SECTION_SIZE = 1; -- private final PoiManager.DistanceTracker distanceTracker; -- private final LongSet loadedChunks = new LongOpenHashSet(); -+ // Paper start - rewrite chunk system -+ // the vanilla tracker needs to be replaced because it does not support level removes -+ public final net.minecraft.server.level.ServerLevel world; -+ private final io.papermc.paper.util.misc.Delayed26WayDistancePropagator3D villageDistanceTracker = new io.papermc.paper.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.isVillageCenter(section)) { -+ this.villageDistanceTracker.setSource(section, POI_DATA_SOURCE); -+ } else { -+ this.villageDistanceTracker.removeSource(section); -+ } -+ } -+ // Paper end - rewrite chunk system - - public PoiManager( - RegionStorageInfo storageKey, Path directory, DataFixer dataFixer, boolean dsync, RegistryAccess registryManager, LevelHeightAccessor world - ) { - super( -+ // Paper start -+ storageKey, -+ directory, -+ dsync, -+ // Paper end - new SimpleRegionStorage(storageKey, directory, dataFixer, dsync, DataFixTypes.POI_CHUNK), - PoiSection::codec, - PoiSection::new, - registryManager, - world - ); -- this.distanceTracker = new PoiManager.DistanceTracker(); -+ this.world = (net.minecraft.server.level.ServerLevel)world; // Paper - rewrite chunk system - } - - public void add(BlockPos pos, Holder type) { -@@ -187,8 +207,8 @@ public class PoiManager extends SectionStorage { - } - - public int sectionsToVillage(SectionPos pos) { -- this.distanceTracker.runAllUpdates(); -- return this.distanceTracker.getLevel(pos.asLong()); -+ this.villageDistanceTracker.propagateUpdates(); // Paper - replace distance tracking util -+ return convertBetweenLevels(this.villageDistanceTracker.getLevel(io.papermc.paper.util.CoordinateUtils.getChunkSectionKey(pos))); // Paper - replace distance tracking util - } - - boolean isVillageCenter(long pos) { -@@ -202,20 +222,117 @@ public class PoiManager extends SectionStorage { - - @Override - public void tick(BooleanSupplier shouldKeepTicking) { -- super.tick(shouldKeepTicking); -- this.distanceTracker.runAllUpdates(); -+ this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system - } - - @Override -- protected void setDirty(long pos) { -- super.setDirty(pos); -- this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false); -+ public void setDirty(long pos) { -+ // Paper start - rewrite chunk system -+ int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkSectionX(pos); -+ int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkSectionZ(pos); -+ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager manager = this.world.chunkTaskScheduler.chunkHolderManager; -+ io.papermc.paper.chunk.system.poi.PoiChunk chunk = manager.getPoiChunkIfLoaded(chunkX, chunkZ, false); -+ if (chunk != null) { -+ chunk.setDirty(true); -+ } -+ this.updateDistanceTracking(pos); -+ // Paper end - rewrite chunk system - } - - @Override - protected void onSectionLoad(long pos) { -- this.distanceTracker.update(pos, this.distanceTracker.getLevelFromSource(pos), false); -+ this.updateDistanceTracking(pos); // Paper - move to new distance tracking util -+ } -+ -+ // Paper start - rewrite chunk system -+ @Override -+ public Optional get(long pos) { -+ int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkSectionX(pos); -+ int chunkY = io.papermc.paper.util.CoordinateUtils.getChunkSectionY(pos); -+ int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkSectionZ(pos); -+ -+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main"); -+ -+ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager manager = this.world.chunkTaskScheduler.chunkHolderManager; -+ io.papermc.paper.chunk.system.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true); -+ -+ return ret == null ? Optional.empty() : ret.getSectionForVanilla(chunkY); -+ } -+ -+ @Override -+ public Optional getOrLoad(long pos) { -+ int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkSectionX(pos); -+ int chunkY = io.papermc.paper.util.CoordinateUtils.getChunkSectionY(pos); -+ int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkSectionZ(pos); -+ -+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main"); -+ -+ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager manager = this.world.chunkTaskScheduler.chunkHolderManager; -+ -+ if (chunkY >= io.papermc.paper.util.WorldUtil.getMinSection(this.world) && -+ chunkY <= io.papermc.paper.util.WorldUtil.getMaxSection(this.world)) { -+ io.papermc.paper.chunk.system.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true); -+ if (ret != null) { -+ return ret.getSectionForVanilla(chunkY); -+ } else { -+ return manager.loadPoiChunk(chunkX, chunkZ).getSectionForVanilla(chunkY); -+ } -+ } -+ // retain vanilla behavior: do not load section if out of bounds! -+ return Optional.empty(); -+ } -+ -+ @Override -+ protected PoiSection getOrCreate(long pos) { -+ int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkSectionX(pos); -+ int chunkY = io.papermc.paper.util.CoordinateUtils.getChunkSectionY(pos); -+ int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkSectionZ(pos); -+ -+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Accessing poi chunk off-main"); -+ -+ io.papermc.paper.chunk.system.scheduling.ChunkHolderManager manager = this.world.chunkTaskScheduler.chunkHolderManager; -+ -+ io.papermc.paper.chunk.system.poi.PoiChunk ret = manager.getPoiChunkIfLoaded(chunkX, chunkZ, true); -+ if (ret != null) { -+ return ret.getOrCreateSection(chunkY); -+ } else { -+ return manager.loadPoiChunk(chunkX, chunkZ).getOrCreateSection(chunkY); -+ } -+ } -+ -+ public void onUnload(long coordinate) { // Paper - rewrite chunk system -+ int chunkX = io.papermc.paper.util.MCUtil.getCoordinateX(coordinate); -+ int chunkZ = io.papermc.paper.util.MCUtil.getCoordinateZ(coordinate); -+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Unloading poi chunk off-main"); -+ for (int section = this.levelHeightAccessor.getMinSection(); section < this.levelHeightAccessor.getMaxSection(); ++section) { -+ long sectionPos = SectionPos.asLong(chunkX, section, chunkZ); -+ this.updateDistanceTracking(sectionPos); -+ } -+ } -+ -+ public void loadInPoiChunk(io.papermc.paper.chunk.system.poi.PoiChunk poiChunk) { -+ int chunkX = poiChunk.chunkX; -+ int chunkZ = poiChunk.chunkZ; -+ io.papermc.paper.util.TickThread.ensureTickThread(this.world, chunkX, chunkZ, "Loading poi chunk off-main"); -+ for (int sectionY = this.levelHeightAccessor.getMinSection(); sectionY < this.levelHeightAccessor.getMaxSection(); ++sectionY) { -+ PoiSection section = poiChunk.getSection(sectionY); -+ if (section != null && !section.isEmpty()) { -+ this.onSectionLoad(SectionPos.asLong(chunkX, sectionY, chunkZ)); -+ } -+ } -+ } -+ -+ public void checkConsistency(net.minecraft.world.level.chunk.ChunkAccess chunk) { -+ int chunkX = chunk.getPos().x; -+ int chunkZ = chunk.getPos().z; -+ int minY = io.papermc.paper.util.WorldUtil.getMinSection(chunk); -+ int maxY = io.papermc.paper.util.WorldUtil.getMaxSection(chunk); -+ LevelChunkSection[] sections = chunk.getSections(); -+ for (int section = minY; section <= maxY; ++section) { -+ this.checkConsistencyWithBlocks(SectionPos.of(chunkX, section, chunkZ), sections[section - minY]); -+ } - } -+ // Paper end - rewrite chunk system - - public void checkConsistencyWithBlocks(SectionPos sectionPos, LevelChunkSection chunkSection) { - Util.ifElse(this.getOrLoad(sectionPos.asLong()), poiSet -> poiSet.refresh(populator -> { -@@ -251,7 +368,7 @@ public class PoiManager extends SectionStorage { - .map(sectionPos -> Pair.of(sectionPos, this.getOrLoad(sectionPos.asLong()))) - .filter(pair -> !pair.getSecond().map(PoiSection::isValid).orElse(false)) - .map(pair -> pair.getFirst().chunk()) -- .filter(chunkPos -> this.loadedChunks.add(chunkPos.toLong())) -+ // Paper - rewrite chunk system - .forEach(chunkPos -> world.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.EMPTY)); - } - -@@ -265,7 +382,7 @@ public class PoiManager extends SectionStorage { - - @Override - protected int getLevelFromSource(long id) { -- return PoiManager.this.isVillageCenter(id) ? 0 : 7; -+ return PoiManager.this.isVillageCenter(id) ? 0 : 7; // Paper - rewrite chunk system - diff on change, this specifies the source level to use for distance tracking - } - - @Override -@@ -287,6 +404,35 @@ public class PoiManager extends SectionStorage { - } - } - -+ // Paper start - Asynchronous chunk io -+ @javax.annotation.Nullable -+ @Override -+ public net.minecraft.nbt.CompoundTag read(ChunkPos chunkcoordintpair) throws java.io.IOException { -+ // Paper start - rewrite chunk system -+ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) { -+ return io.papermc.paper.chunk.system.io.RegionFileIOThread.loadData( -+ this.world, chunkcoordintpair.x, chunkcoordintpair.z, io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.POI_DATA, -+ io.papermc.paper.chunk.system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread() -+ ); -+ } -+ // Paper end - rewrite chunk system -+ return super.read(chunkcoordintpair); -+ } -+ -+ @Override -+ public void write(ChunkPos chunkcoordintpair, net.minecraft.nbt.CompoundTag nbttagcompound) throws java.io.IOException { -+ // Paper start - rewrite chunk system -+ if (!io.papermc.paper.chunk.system.io.RegionFileIOThread.isRegionFileThread()) { -+ io.papermc.paper.chunk.system.io.RegionFileIOThread.scheduleSave( -+ this.world, chunkcoordintpair.x, chunkcoordintpair.z, nbttagcompound, -+ io.papermc.paper.chunk.system.io.RegionFileIOThread.RegionFileType.POI_DATA); -+ return; -+ } -+ // Paper end - rewrite chunk system -+ super.write(chunkcoordintpair, nbttagcompound); -+ } -+ // Paper end -+ - public static enum Occupancy { - HAS_SPACE(PoiRecord::hasSpace), - IS_OCCUPIED(PoiRecord::isOccupied), -diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java -index 971fb29a2c3dc713cb8ab1d2eed054cc16f9c93c..5b7deae326228e482b218aeebd857a59b7434eaf 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java -+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java -@@ -29,6 +29,7 @@ public class PoiSection { - private final Map, Set> byType = Maps.newHashMap(); - private final Runnable setDirty; - private boolean isValid; -+ public final Optional noAllocateOptional = Optional.of(this); // Paper - rewrite chunk system - - public static Codec codec(Runnable updateListener) { - return RecordCodecBuilder.create( -@@ -46,6 +47,12 @@ public class PoiSection { - this(updateListener, true, ImmutableList.of()); - } - -+ // Paper start - isEmpty -+ public boolean isEmpty() { -+ return this.isValid && this.records.isEmpty() && this.byType.isEmpty(); -+ } -+ // Paper end -+ - private PoiSection(Runnable updateListener, boolean valid, List pois) { - this.setDirty = updateListener; - this.isValid = valid; -diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java -index bd20bea7f76a7307f1698fb2dfef37125032d166..9a28912f52824acdc80a62243b136e6f365bf567 100644 ---- a/src/main/java/net/minecraft/world/level/EntityGetter.java -+++ b/src/main/java/net/minecraft/world/level/EntityGetter.java -@@ -19,6 +19,18 @@ import net.minecraft.world.phys.shapes.Shapes; - import net.minecraft.world.phys.shapes.VoxelShape; - - public interface EntityGetter { -+ -+ // Paper start -+ List getHardCollidingEntities(Entity except, AABB box, Predicate predicate); -+ -+ void getEntities(Entity except, AABB box, Predicate predicate, List into); -+ -+ void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into); -+ -+ void getEntitiesByClass(Class clazz, Entity except, final AABB box, List into, -+ Predicate predicate); -+ // Paper end -+ - List getEntities(@Nullable Entity except, AABB box, Predicate predicate); - - List getEntities(EntityTypeTest filter, AABB box, Predicate predicate); -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index 975fcd4b8f93cb8c602ddeb165c485214eac10a4..d3137c9e5cc42ef191ea233b0d37eafeffc6f82c 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -547,6 +547,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - - if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement - this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i); -+ // Paper start - per player view distance - allow block updates for non-ticking chunks in player view distance -+ // if copied from above -+ } else if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0)) { // Paper - replace old player chunk management -+ ((ServerLevel)this).getChunkSource().blockChanged(blockposition); -+ // Paper end - per player view distance - } - - if ((i & 1) != 0) { -@@ -941,7 +946,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - } - // Paper end - Perf: Optimize capturedTileEntities lookup - // CraftBukkit end -- return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && Thread.currentThread() != this.thread ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE)); -+ return this.isOutsideBuildHeight(blockposition) ? null : (!this.isClientSide && !io.papermc.paper.util.TickThread.isTickThread() ? null : this.getChunkAt(blockposition).getBlockEntity(blockposition, LevelChunk.EntityCreationType.IMMEDIATE)); // Paper - rewrite chunk system - } - - public void setBlockEntity(BlockEntity blockEntity) { -@@ -1032,26 +1037,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - public List getEntities(@Nullable Entity except, AABB box, Predicate predicate) { - this.getProfiler().incrementCounter("getEntities"); - List list = Lists.newArrayList(); -- -- this.getEntities().get(box, (entity1) -> { -- if (entity1 != except && predicate.test(entity1)) { -- list.add(entity1); -- } -- -- if (entity1 instanceof EnderDragon) { -- EnderDragonPart[] aentitycomplexpart = ((EnderDragon) entity1).getSubEntities(); -- int i = aentitycomplexpart.length; -- -- for (int j = 0; j < i; ++j) { -- EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; -- -- if (entity1 != except && predicate.test(entitycomplexpart)) { -- list.add(entitycomplexpart); -- } -- } -- } -- -- }); -+ ((ServerLevel)this).getEntityLookup().getEntities(except, box, list, predicate); // Paper - optimise this call - return list; - } - -@@ -1069,33 +1055,23 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - - public void getEntities(EntityTypeTest filter, AABB box, Predicate predicate, List result, int limit) { - this.getProfiler().incrementCounter("getEntities"); -- this.getEntities().get(filter, box, (entity) -> { -- if (predicate.test(entity)) { -- result.add(entity); -- if (result.size() >= limit) { -- return AbortableIterationConsumer.Continuation.ABORT; -- } -- } -- -- if (entity instanceof EnderDragon entityenderdragon) { -- EnderDragonPart[] aentitycomplexpart = entityenderdragon.getSubEntities(); -- int j = aentitycomplexpart.length; -- -- for (int k = 0; k < j; ++k) { -- EnderDragonPart entitycomplexpart = aentitycomplexpart[k]; -- T t0 = filter.tryCast(entitycomplexpart); // CraftBukkit - decompile error -- -- if (t0 != null && predicate.test(t0)) { -- result.add(t0); -- if (result.size() >= limit) { -- return AbortableIterationConsumer.Continuation.ABORT; -- } -- } -- } -+ // Paper start - optimise this call -+ //TODO use limit -+ if (filter instanceof net.minecraft.world.entity.EntityType entityTypeTest) { -+ ((ServerLevel) this).getEntityLookup().getEntities(entityTypeTest, box, result, predicate); -+ } else { -+ Predicate test = (obj) -> { -+ return filter.tryCast(obj) != null; -+ }; -+ predicate = predicate == null ? test : test.and((Predicate) predicate); -+ Class base; -+ if (filter == null || (base = filter.getBaseClass()) == null || base == Entity.class) { -+ ((ServerLevel) this).getEntityLookup().getEntities((Entity) null, box, (List) result, (Predicate)predicate); -+ } else { -+ ((ServerLevel) this).getEntityLookup().getEntities(base, null, box, (List) result, (Predicate)predicate); // Paper - optimise this call - } -- -- return AbortableIterationConsumer.Continuation.CONTINUE; -- }); -+ } -+ // Paper end - optimise this call - } - - @Nullable -@@ -1385,4 +1361,45 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - } - } - // Paper end - notify observers even if grow failed -+ // Paper start -+ //protected final io.papermc.paper.world.EntitySliceManager entitySliceManager; // Paper - rewrite chunk system -+ -+ public org.bukkit.entity.Entity[] getChunkEntities(int chunkX, int chunkZ) { -+ io.papermc.paper.world.ChunkEntitySlices slices = ((ServerLevel)this).getEntityLookup().getChunk(chunkX, chunkZ); -+ if (slices == null) { -+ return new org.bukkit.entity.Entity[0]; -+ } -+ return slices.getChunkEntities(); -+ } -+ -+ @Override -+ public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { -+ List ret = new java.util.ArrayList<>(); -+ ((ServerLevel)this).getEntityLookup().getHardCollidingEntities(except, box, ret, predicate); -+ return ret; -+ } -+ -+ @Override -+ public void getEntities(Entity except, AABB box, Predicate predicate, List into) { -+ ((ServerLevel)this).getEntityLookup().getEntities(except, box, into, predicate); -+ } -+ -+ @Override -+ public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) { -+ ((ServerLevel)this).getEntityLookup().getHardCollidingEntities(except, box, into, predicate); -+ } -+ -+ @Override -+ public void getEntitiesByClass(Class clazz, Entity except, final AABB box, List into, -+ Predicate predicate) { -+ ((ServerLevel)this).getEntityLookup().getEntities((Class)clazz, except, box, (List)into, (Predicate)predicate); -+ } -+ -+ @Override -+ public List getEntitiesOfClass(Class entityClass, AABB box, Predicate predicate) { -+ List ret = new java.util.ArrayList<>(); -+ ((ServerLevel)this).getEntityLookup().getEntities(entityClass, null, box, ret, predicate); -+ return ret; -+ } -+ // Paper end - } -diff --git a/src/main/java/net/minecraft/world/level/LevelReader.java b/src/main/java/net/minecraft/world/level/LevelReader.java -index a0ae26d6197e1069ca09982b4f8b706c55ae8491..32bfeb9aa87b43a9d2ce46dcc99dbd0ff355b412 100644 ---- a/src/main/java/net/minecraft/world/level/LevelReader.java -+++ b/src/main/java/net/minecraft/world/level/LevelReader.java -@@ -26,6 +26,15 @@ public interface LevelReader extends BlockAndTintGetter, CollisionGetter, Signal - @Nullable - ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create); - -+ // Paper start - rewrite chunk system -+ default ChunkAccess syncLoadNonFull(int chunkX, int chunkZ, ChunkStatus status) { -+ if (status == null || status.isOrAfter(ChunkStatus.FULL)) { -+ throw new IllegalArgumentException("Status: " + status.toString()); -+ } -+ return this.getChunk(chunkX, chunkZ, status, true); -+ } -+ // Paper end - rewrite chunk system -+ - @Nullable ChunkAccess getChunkIfLoadedImmediately(int x, int z); // Paper - ifLoaded api (we need this since current impl blocks if the chunk is loading) - @Nullable default ChunkAccess getChunkIfLoadedImmediately(BlockPos pos) { return this.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4);} - -diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java -index c9cd18ce79a6ee7297a8fd14f4dbe712570b3ced..927bdebdb8ae01613f0cea074b3367bd7ffe9ab1 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java -+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java -@@ -120,7 +120,7 @@ public abstract class ChunkGenerator { - return CompletableFuture.supplyAsync(Util.wrapThreadWithTaskName("init_biomes", () -> { - chunk.fillBiomesFromNoise(this.biomeSource, noiseConfig.sampler()); - return chunk; -- }), Util.backgroundExecutor()); -+ }), executor); // Paper - run with supplied executor - } - - public abstract void applyCarvers(WorldGenRegion chunkRegion, long seed, RandomState noiseConfig, BiomeManager biomeAccess, StructureManager structureAccessor, ChunkAccess chunk, GenerationStep.Carving carverStep); -@@ -315,7 +315,7 @@ public abstract class ChunkGenerator { - return Pair.of(placement.getLocatePos(pos), holder); - } - -- ChunkAccess ichunkaccess = world.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS); -+ ChunkAccess ichunkaccess = world.syncLoadNonFull(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS); // Paper - rewrite chunk system - - structurestart = structureAccessor.getStartForStructure(SectionPos.bottomOf(ichunkaccess), (Structure) holder.value(), ichunkaccess); - } while (structurestart == null); -diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -index bac191f92ea3735df19c68d5568c2c7962c8680f..5d94aee1303d9eca5f1fa9a2e033ad0d12909635 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -@@ -86,6 +86,7 @@ public class LevelChunk extends ChunkAccess { - private final Int2ObjectMap gameEventListenerRegistrySections; - private final LevelChunkTicks blockTicks; - private final LevelChunkTicks fluidTicks; -+ public volatile FullChunkStatus chunkStatus = FullChunkStatus.INACCESSIBLE; // Paper - rewrite chunk system - - public LevelChunk(Level world, ChunkPos pos) { - this(world, pos, UpgradeData.EMPTY, new LevelChunkTicks<>(), new LevelChunkTicks<>(), 0L, (LevelChunkSection[]) null, (LevelChunk.PostLoadProcessor) null, (BlendingData) null); -@@ -690,9 +691,26 @@ public class LevelChunk extends ChunkAccess { - - } - -- // CraftBukkit start -- public void loadCallback() { -- // Paper start - neighbour cache -+ // Paper start - new load callbacks -+ private io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder; -+ public io.papermc.paper.chunk.system.scheduling.NewChunkHolder getChunkHolder() { -+ return this.chunkHolder; -+ } -+ -+ public void setChunkHolder(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { -+ if (chunkHolder == null) { -+ throw new NullPointerException("Chunkholder cannot be null"); -+ } -+ if (this.chunkHolder != null) { -+ throw new IllegalStateException("Already have chunkholder: " + this.chunkHolder + ", cannot replace with " + chunkHolder); -+ } -+ this.chunkHolder = chunkHolder; -+ this.playerChunk = chunkHolder.vanillaChunkHolder; -+ } -+ -+ /* Note: We skip the light neighbour chunk loading done for the vanilla full chunk */ -+ /* Starlight does not need these chunks for lighting purposes because of edge checks */ -+ public void pushChunkIntoLoadedMap() { - int chunkX = this.chunkPos.x; - int chunkZ = this.chunkPos.z; - net.minecraft.server.level.ServerChunkCache chunkProvider = this.level.getChunkSource(); -@@ -707,10 +725,55 @@ public class LevelChunk extends ChunkAccess { - } - } - this.setNeighbourLoaded(0, 0, this); -+ this.level.getChunkSource().addLoadedChunk(this); -+ } -+ -+ public void onChunkLoad(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { -+ // figure out how this should interface with: -+ // the entity chunk load event // -> moved to the FULL status -+ // the chunk load event // -> stays here -+ // any entity add to world events // -> in FULL status -+ this.loadCallback(); -+ io.papermc.paper.chunk.system.ChunkSystem.onChunkBorder(this, chunkHolder.vanillaChunkHolder); -+ } -+ -+ public void onChunkUnload(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { -+ // figure out how this should interface with: -+ // the entity chunk load event // -> moved to chunk unload to disk (not written yet) -+ // the chunk load event // -> stays here -+ // any entity add to world events // -> goes into the unload logic, it will completely explode -+ // etc later -+ this.unloadCallback(); -+ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotBorder(this, chunkHolder.vanillaChunkHolder); -+ } -+ -+ public void onChunkTicking(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { -+ this.postProcessGeneration(); -+ this.level.startTickingChunk(this); -+ io.papermc.paper.chunk.system.ChunkSystem.onChunkTicking(this, chunkHolder.vanillaChunkHolder); -+ } -+ -+ public void onChunkNotTicking(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { -+ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotTicking(this, chunkHolder.vanillaChunkHolder); -+ } -+ -+ public void onChunkEntityTicking(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { -+ io.papermc.paper.chunk.system.ChunkSystem.onChunkEntityTicking(this, chunkHolder.vanillaChunkHolder); -+ } -+ -+ public void onChunkNotEntityTicking(io.papermc.paper.chunk.system.scheduling.NewChunkHolder chunkHolder) { -+ io.papermc.paper.chunk.system.ChunkSystem.onChunkNotEntityTicking(this, chunkHolder.vanillaChunkHolder); -+ } -+ // Paper end - new load callbacks -+ -+ // CraftBukkit start -+ public void loadCallback() { -+ if (this.loadedTicketLevel) { LOGGER.error("Double calling chunk load!", new Throwable()); } // Paper -+ // Paper - rewrite chunk system - move into separate callback - this.loadedTicketLevel = true; -- // Paper end - neighbour cache -+ // Paper - rewrite chunk system - move into separate callback - org.bukkit.Server server = this.level.getCraftServer(); -- this.level.getChunkSource().addLoadedChunk(this); // Paper -+ // Paper - rewrite chunk system - move into separate callback - if (server != null) { - /* - * If it's a new world, the first few chunks are generated inside -@@ -719,6 +782,7 @@ public class LevelChunk extends ChunkAccess { - */ - org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this); - server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkLoadEvent(bukkitChunk, this.needsDecoration)); -+ this.chunkHolder.getEntityChunk().callEntitiesLoadEvent(); // Paper - rewrite chunk system - - if (this.needsDecoration) { - try (co.aikar.timings.Timing ignored = this.level.timings.chunkLoadPopulate.startTiming()) { // Paper -@@ -747,9 +811,11 @@ public class LevelChunk extends ChunkAccess { - } - - public void unloadCallback() { -+ if (!this.loadedTicketLevel) { LOGGER.error("Double calling chunk unload!", new Throwable()); } // Paper - org.bukkit.Server server = this.level.getCraftServer(); -+ this.chunkHolder.getEntityChunk().callEntitiesUnloadEvent(); // Paper - rewrite chunk system - org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this); -- org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(bukkitChunk, this.isUnsaved()); -+ org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(bukkitChunk, true); // Paper - rewrite chunk system - force save to true so that mustNotSave is correctly set below - server.getPluginManager().callEvent(unloadEvent); - // note: saving can be prevented, but not forced if no saving is actually required - this.mustNotSave = !unloadEvent.isSaveChunk(); -@@ -771,9 +837,26 @@ public class LevelChunk extends ChunkAccess { - // Paper end - } - -+ // Paper start - add dirty system to tick lists -+ @Override -+ public void setUnsaved(boolean needsSaving) { -+ if (!needsSaving) { -+ this.blockTicks.clearDirty(); -+ this.fluidTicks.clearDirty(); -+ } -+ super.setUnsaved(needsSaving); -+ } -+ // Paper end - add dirty system to tick lists -+ - @Override - public boolean isUnsaved() { -- return super.isUnsaved() && !this.mustNotSave; -+ // Paper start - add dirty system to tick lists -+ long gameTime = this.level.getLevelData().getGameTime(); -+ if (this.blockTicks.isDirty(gameTime) || this.fluidTicks.isDirty(gameTime)) { -+ return true; -+ } -+ // Paper end - add dirty system to tick lists -+ return super.isUnsaved(); // Paper - rewrite chunk system - do NOT clobber the dirty flag - } - // CraftBukkit end - -@@ -842,7 +925,9 @@ public class LevelChunk extends ChunkAccess { - return this.blockEntities; - } - -+ public boolean isPostProcessingDone; // Paper - replace chunk loader system - public void postProcessGeneration() { -+ try { // Paper - replace chunk loader system - ChunkPos chunkcoordintpair = this.getPos(); - - for (int i = 0; i < this.postProcessing.length; ++i) { -@@ -863,6 +948,7 @@ public class LevelChunk extends ChunkAccess { - BlockState iblockdata1 = Block.updateFromNeighbourShapes(iblockdata, this.level, blockposition); - - this.level.setBlock(blockposition, iblockdata1, 20); -+ if (iblockdata1 != iblockdata) this.level.chunkSource.blockChanged(blockposition); // Paper - replace player chunk loader - notify since we send before processing full updates - } - } - -@@ -880,6 +966,10 @@ public class LevelChunk extends ChunkAccess { - - this.pendingBlockEntities.clear(); - this.upgradeData.upgrade(this); -+ } finally { // Paper start - replace chunk loader system -+ this.isPostProcessingDone = true; -+ } -+ // Paper end - replace chunk loader system - } - - @Nullable -@@ -929,7 +1019,7 @@ public class LevelChunk extends ChunkAccess { - } - - public FullChunkStatus getFullStatus() { -- return this.fullStatus == null ? FullChunkStatus.FULL : (FullChunkStatus) this.fullStatus.get(); -+ return this.chunkHolder == null ? FullChunkStatus.INACCESSIBLE : this.chunkHolder.getChunkStatus(); // Paper - rewrite chunk system - } - - public void setFullStatus(Supplier levelTypeProvider) { -diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java -index 95318092f8281d98132d1d3ceb4a5c36cf32eb05..b81c548c0e1ac53784e9c94b34b65db5f123309c 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java -+++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java -@@ -21,13 +21,15 @@ import net.minecraft.world.level.chunk.ProtoChunk; - import net.minecraft.world.level.levelgen.Heightmap; - - public class ChunkStatus { -+ static final ChunkStatus.LoadingTask PASSTHROUGH_LOAD_TASK = (WorldGenContext context, ChunkStatus status, ToFullChunk fullChunkConverter, ChunkAccess chunk) -> CompletableFuture.completedFuture(chunk); // Paper - rewrite chunk system -+ protected static final java.util.List statuses = new java.util.ArrayList<>(); // Paper - rewrite chunk system - public static final int MAX_STRUCTURE_DISTANCE = 8; - private static final EnumSet PRE_FEATURES = EnumSet.of(Heightmap.Types.OCEAN_FLOOR_WG, Heightmap.Types.WORLD_SURFACE_WG); - public static final EnumSet POST_FEATURES = EnumSet.of( - Heightmap.Types.OCEAN_FLOOR, Heightmap.Types.WORLD_SURFACE, Heightmap.Types.MOTION_BLOCKING, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES - ); - public static final ChunkStatus EMPTY = register( -- "empty", null, -1, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateEmpty, ChunkStatusTasks::loadPassThrough -+ "empty", null, -1, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateEmpty, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system - ); - public static final ChunkStatus STRUCTURE_STARTS = register( - "structure_starts", -@@ -47,22 +49,22 @@ public class ChunkStatus { - PRE_FEATURES, - ChunkType.PROTOCHUNK, - ChunkStatusTasks::generateStructureReferences, -- ChunkStatusTasks::loadPassThrough -+ PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system - ); - public static final ChunkStatus BIOMES = register( -- "biomes", STRUCTURE_REFERENCES, 8, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateBiomes, ChunkStatusTasks::loadPassThrough -+ "biomes", STRUCTURE_REFERENCES, 8, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateBiomes, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system - ); - public static final ChunkStatus NOISE = register( -- "noise", BIOMES, 8, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateNoise, ChunkStatusTasks::loadPassThrough -+ "noise", BIOMES, 8, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateNoise, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system - ); - public static final ChunkStatus SURFACE = register( -- "surface", NOISE, 8, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateSurface, ChunkStatusTasks::loadPassThrough -+ "surface", NOISE, 8, false, PRE_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateSurface, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system - ); - public static final ChunkStatus CARVERS = register( -- "carvers", SURFACE, 8, false, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateCarvers, ChunkStatusTasks::loadPassThrough -+ "carvers", SURFACE, 8, false, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateCarvers, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system - ); - public static final ChunkStatus FEATURES = register( -- "features", CARVERS, 8, false, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateFeatures, ChunkStatusTasks::loadPassThrough -+ "features", CARVERS, 8, false, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateFeatures, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system - ); - public static final ChunkStatus INITIALIZE_LIGHT = register( - "initialize_light", -@@ -78,7 +80,7 @@ public class ChunkStatus { - "light", INITIALIZE_LIGHT, 1, true, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateLight, ChunkStatusTasks::loadLight - ); - public static final ChunkStatus SPAWN = register( -- "spawn", LIGHT, 1, false, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateSpawn, ChunkStatusTasks::loadPassThrough -+ "spawn", LIGHT, 1, false, POST_FEATURES, ChunkType.PROTOCHUNK, ChunkStatusTasks::generateSpawn, PASSTHROUGH_LOAD_TASK // Paper - rewrite chunk system - ); - public static final ChunkStatus FULL = register( - "full", SPAWN, 0, false, POST_FEATURES, ChunkType.LEVELCHUNK, ChunkStatusTasks::generateFull, ChunkStatusTasks::loadFull -@@ -128,6 +130,27 @@ public class ChunkStatus { - } - } - // Paper end - starlight -+ // Paper start - rewrite chunk system -+ public boolean isParallelCapable; // Paper -+ public int writeRadius = -1; -+ public int loadRange = 0; -+ -+ private ChunkStatus nextStatus; -+ -+ public final ChunkStatus getNextStatus() { -+ return this.nextStatus; -+ } -+ -+ public final boolean isEmptyLoadStatus() { -+ return this.loadingTask == PASSTHROUGH_LOAD_TASK; -+ } -+ -+ public final boolean isEmptyGenStatus() { -+ return this == ChunkStatus.EMPTY; -+ } -+ -+ public final java.util.concurrent.atomic.AtomicBoolean warnedAboutNoImmediateComplete = new java.util.concurrent.atomic.AtomicBoolean(); -+ // Paper end - rewrite chunk system - - private static ChunkStatus register( - String id, -@@ -190,6 +213,13 @@ public class ChunkStatus { - this.chunkType = chunkType; - this.heightmapsAfter = heightMapTypes; - this.index = previous == null ? 0 : previous.getIndex() + 1; -+ // Paper start -+ this.nextStatus = this; -+ if (statuses.size() > 0) { -+ statuses.get(statuses.size() - 1).nextStatus = this; -+ } -+ statuses.add(this); -+ // Paper end - } - - public int getIndex() { -diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java -index ce7f154b9dad4e78ee0189405cf57dcb3d5301b8..a5e8078b99161272b0f826b8c39e56d17588c264 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java -+++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java -@@ -26,8 +26,9 @@ public class ChunkStatusTasks { - return CompletableFuture.completedFuture(chunk); - } - -- static CompletableFuture loadPassThrough(WorldGenContext context, ChunkStatus status, ToFullChunk fullChunkConverter, ChunkAccess chunk) { -- return CompletableFuture.completedFuture(chunk); -+ @io.papermc.paper.annotation.DoNotUse @Deprecated(forRemoval = true) // Paper - rewrite chunk system - use ChunkStatus.PASSTHROUGH_LOAD_TASK instead -+ static CompletableFuture loadPassThrough(WorldGenContext context, ChunkStatus status, ToFullChunk fullChunkConverter, ChunkAccess chunk) { // Paper - rewrite chunk system - diff on change -+ return CompletableFuture.completedFuture(chunk); // Paper - rewrite chunk system - diff on change - } - - static CompletableFuture generateStructureStarts(WorldGenContext context, ChunkStatus status, Executor executor, ToFullChunk fullChunkConverter, List chunks, ChunkAccess chunk) { -@@ -125,7 +126,7 @@ public class ChunkStatusTasks { - ((ProtoChunk) chunk).setLightEngine(lightingProvider); - boolean flag = ChunkStatusTasks.isLighted(chunk); - -- return lightingProvider.initializeLight(chunk, flag); -+ return CompletableFuture.completedFuture(chunk); // Paper - rewrite chunk system - } - - static CompletableFuture generateLight(WorldGenContext context, ChunkStatus status, Executor executor, ToFullChunk fullChunkConverter, List chunks, ChunkAccess chunk) { -@@ -139,7 +140,7 @@ public class ChunkStatusTasks { - private static CompletableFuture lightChunk(ThreadedLevelLightEngine lightingProvider, ChunkAccess chunk) { - boolean flag = ChunkStatusTasks.isLighted(chunk); - -- return lightingProvider.lightChunk(chunk, flag); -+ return CompletableFuture.completedFuture(chunk); // Paper - rewrite chunk system - } - - static CompletableFuture generateSpawn(WorldGenContext context, ChunkStatus status, Executor executor, ToFullChunk fullChunkConverter, List chunks, ChunkAccess chunk) { -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java -index 01d6b8683a9fa30d05b03ebfef8ee2dca4e83a56..b2e8082dda0d0ca32bfc32cbf9d8ae9822febc30 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java -@@ -112,6 +112,17 @@ public class ChunkSerializer { - } - } - // Paper end - guard against serializing mismatching coordinates -+ // Paper start - rewrite chunk system -+ public static final class InProgressChunkHolder { -+ -+ public final ProtoChunk protoChunk; -+ -+ public CompoundTag poiData; -+ -+ public InProgressChunkHolder(final ProtoChunk protoChunk) { -+ this.protoChunk = protoChunk; -+ } -+ } - public static ProtoChunk read(ServerLevel world, PoiManager poiStorage, ChunkPos chunkPos, CompoundTag nbt) { - // Paper start - Do not let the server load chunks from newer versions - if (nbt.contains("DataVersion", net.minecraft.nbt.Tag.TAG_ANY_NUMERIC)) { -@@ -122,6 +133,12 @@ public class ChunkSerializer { - } - } - // Paper end - Do not let the server load chunks from newer versions -+ InProgressChunkHolder holder = readInProgressChunkHolder(world, poiStorage, chunkPos, nbt); -+ return holder.protoChunk; -+ } -+ -+ public static InProgressChunkHolder readInProgressChunkHolder(ServerLevel world, PoiManager poiStorage, ChunkPos chunkPos, CompoundTag nbt) { -+ // Paper end - rewrite chunk system - ChunkPos chunkcoordintpair1 = new ChunkPos(nbt.getInt("xPos"), nbt.getInt("zPos")); // Paper - guard against serializing mismatching coordinates; diff on change, see ChunkSerializer#getChunkCoordinate - - if (!Objects.equals(chunkPos, chunkcoordintpair1)) { -@@ -178,7 +195,7 @@ public class ChunkSerializer { - achunksection[k] = chunksection; - SectionPos sectionposition = SectionPos.of(chunkPos, b0); - -- poiStorage.checkConsistencyWithBlocks(sectionposition, chunksection); -+ // Paper - rewrite chunk system - moved to final load stage - } - - boolean flag3 = nbttagcompound1.contains("BlockLight", 7); -@@ -325,7 +342,7 @@ public class ChunkSerializer { - } - - if (chunktype == ChunkType.LEVELCHUNK) { -- return new ImposterProtoChunk((LevelChunk) object1, false); -+ return new InProgressChunkHolder(new ImposterProtoChunk((LevelChunk) object1, false)); // Paper - Async chunk loading - } else { - ProtoChunk protochunk1 = (ProtoChunk) object1; - -@@ -360,9 +377,41 @@ public class ChunkSerializer { - protochunk1.setCarvingMask(worldgenstage_features, new CarvingMask(nbttagcompound5.getLongArray(s1), ((ChunkAccess) object1).getMinBuildHeight())); - } - -- return protochunk1; -+ return new InProgressChunkHolder(protochunk1); // Paper - Async chunk loading -+ } -+ } -+ -+ // Paper start - async chunk save for unload -+ public record AsyncSaveData( -+ Tag blockTickList, // non-null if we had to go to the server's tick list -+ Tag fluidTickList, // non-null if we had to go to the server's tick list -+ ListTag blockEntities, -+ long worldTime -+ ) {} -+ -+ // must be called sync -+ public static AsyncSaveData getAsyncSaveData(ServerLevel world, ChunkAccess chunk) { -+ org.spigotmc.AsyncCatcher.catchOp("preparation of chunk data for async save"); -+ -+ final CompoundTag tickLists = new CompoundTag(); -+ ChunkSerializer.saveTicks(world, tickLists, chunk.getTicksForSerialization()); -+ -+ ListTag blockEntitiesSerialized = new ListTag(); -+ for (final BlockPos blockPos : chunk.getBlockEntitiesPos()) { -+ final CompoundTag blockEntityNbt = chunk.getBlockEntityNbtForSaving(blockPos, world.registryAccess()); -+ if (blockEntityNbt != null) { -+ blockEntitiesSerialized.add(blockEntityNbt); -+ } - } -+ -+ return new AsyncSaveData( -+ tickLists.get(BLOCK_TICKS_TAG), -+ tickLists.get(FLUID_TICKS_TAG), -+ blockEntitiesSerialized, -+ world.getGameTime() -+ ); - } -+ // Paper end - - private static void logErrors(ChunkPos chunkPos, int y, String message) { - ChunkSerializer.LOGGER.error("Recoverable errors when loading section [" + chunkPos.x + ", " + y + ", " + chunkPos.z + "]: " + message); -@@ -379,6 +428,11 @@ public class ChunkSerializer { - // CraftBukkit end - - public static CompoundTag write(ServerLevel world, ChunkAccess chunk) { -+ // Paper start -+ return saveChunk(world, chunk, null); -+ } -+ public static CompoundTag saveChunk(ServerLevel world, ChunkAccess chunk, @org.checkerframework.checker.nullness.qual.Nullable AsyncSaveData asyncsavedata) { -+ // Paper end - // Paper start - rewrite light impl - final int minSection = io.papermc.paper.util.WorldUtil.getMinLightSection(world); - final int maxSection = io.papermc.paper.util.WorldUtil.getMaxLightSection(world); -@@ -391,7 +445,7 @@ public class ChunkSerializer { - nbttagcompound.putInt("xPos", chunkcoordintpair.x); - nbttagcompound.putInt("yPos", chunk.getMinSection()); - nbttagcompound.putInt("zPos", chunkcoordintpair.z); -- nbttagcompound.putLong("LastUpdate", world.getGameTime()); -+ nbttagcompound.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : world.getGameTime()); // Paper - async chunk unloading - nbttagcompound.putLong("InhabitedTime", chunk.getInhabitedTime()); - nbttagcompound.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getStatus()).toString()); - BlendingData blendingdata = chunk.getBlendingData(); -@@ -485,8 +539,17 @@ public class ChunkSerializer { - nbttagcompound.putBoolean("isLightOn", false); // Paper - set to false but still store, this allows us to detect --eraseCache (as eraseCache _removes_) - } - -- ListTag nbttaglist1 = new ListTag(); -- Iterator iterator = chunk.getBlockEntitiesPos().iterator(); -+ // Paper start -+ ListTag nbttaglist1; -+ Iterator iterator; -+ if (asyncsavedata != null) { -+ nbttaglist1 = asyncsavedata.blockEntities; -+ iterator = java.util.Collections.emptyIterator(); -+ } else { -+ nbttaglist1 = new ListTag(); -+ iterator = chunk.getBlockEntitiesPos().iterator(); -+ } -+ // Paper end - - CompoundTag nbttagcompound2; - -@@ -522,7 +585,14 @@ public class ChunkSerializer { - nbttagcompound.put("CarvingMasks", nbttagcompound2); - } - -+ // Paper start -+ if (asyncsavedata != null) { -+ nbttagcompound.put(BLOCK_TICKS_TAG, asyncsavedata.blockTickList); -+ nbttagcompound.put(FLUID_TICKS_TAG, asyncsavedata.fluidTickList); -+ } else { - ChunkSerializer.saveTicks(world, nbttagcompound, chunk.getTicksForSerialization()); -+ } -+ // Paper end - nbttagcompound.put("PostProcessing", ChunkSerializer.packOffsets(chunk.getPostProcessing())); - CompoundTag nbttagcompound3 = new CompoundTag(); - Iterator iterator1 = chunk.getHeightmaps().iterator(); -@@ -578,7 +648,7 @@ public class ChunkSerializer { - - return nbttaglist == null && nbttaglist1 == null ? null : (chunk) -> { - if (nbttaglist != null) { -- world.addLegacyChunkEntities(EntityType.loadEntitiesRecursive(nbttaglist, world)); -+ world.addLegacyChunkEntities(EntityType.loadEntitiesRecursive(nbttaglist, world), chunk.getPos()); // Paper - rewrite chunk system - } - - if (nbttaglist1 != null) { -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java -index a62c90e10c0dfa4c6211a05c4071932756d7b218..554dede2ad0e45d3ee4ccc5510b7644f2e9e4250 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java -@@ -31,18 +31,21 @@ import net.minecraft.world.level.storage.DimensionDataStorage; - public class ChunkStorage implements AutoCloseable { - - public static final int LAST_MONOLYTH_STRUCTURE_DATA_VERSION = 1493; -- private final IOWorker worker; -+ // Paper start - rewrite chunk system; async chunk IO -+ private final Object persistentDataLock = new Object(); -+ public final RegionFileStorage regionFileCache; -+ // Paper end - rewrite chunk system - protected final DataFixer fixerUpper; - @Nullable - private volatile LegacyStructureDataHandler legacyStructureHandler; - - public ChunkStorage(RegionStorageInfo storageKey, Path directory, DataFixer dataFixer, boolean dsync) { - this.fixerUpper = dataFixer; -- this.worker = new IOWorker(storageKey, directory, dsync); -+ this.regionFileCache = new RegionFileStorage(storageKey, directory, dsync); // Paper - rewrite chunk system; async chunk IO - } - - public boolean isOldChunkAround(ChunkPos chunkPos, int checkRadius) { -- return this.worker.isOldChunkAround(chunkPos, checkRadius); -+ return true; // Paper - rewrite chunk system - } - - // CraftBukkit start -@@ -50,8 +53,9 @@ public class ChunkStorage implements AutoCloseable { - if (true) return true; // Paper - Perf: this isn't even needed anymore, light is purged updating to 1.14+, why are we holding up the conversion process reading chunk data off disk - return true, we need to set light populated to true so the converter recognizes the chunk as being "full" - ChunkPos pos = new ChunkPos(x, z); - if (cps != null) { -- com.google.common.base.Preconditions.checkState(org.bukkit.Bukkit.isPrimaryThread(), "primary thread"); -- if (cps.hasChunk(x, z)) { -+ // Paper start - rewrite chunk system; async chunk IO -+ if (cps.getChunkAtIfCachedImmediately(x, z) != null) { // isLoaded is a ticket level check, not a chunk loaded check! -+ // Paper end - rewrite chunk system - return true; - } - } -@@ -79,6 +83,7 @@ public class ChunkStorage implements AutoCloseable { - - public CompoundTag upgradeChunkTag(ResourceKey resourcekey, Supplier supplier, CompoundTag nbttagcompound, Optional>> optional, ChunkPos pos, @Nullable LevelAccessor generatoraccess) { - // CraftBukkit end -+ nbttagcompound = nbttagcompound.copy(); // Paper - defensive copy, another thread might modify this - int i = ChunkStorage.getVersion(nbttagcompound); - - try { -@@ -97,9 +102,11 @@ public class ChunkStorage implements AutoCloseable { - if (i < 1493) { - ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.CHUNK, nbttagcompound, i, 1493); // Paper - replace chunk converter - if (nbttagcompound.getCompound("Level").getBoolean("hasLegacyStructureData")) { -+ synchronized (this.persistentDataLock) { // Paper - Async chunk loading - LegacyStructureDataHandler persistentstructurelegacy = this.getLegacyStructureHandler(resourcekey, supplier); - - nbttagcompound = persistentstructurelegacy.updateFromLegacy(nbttagcompound); -+ } // Paper - Async chunk loading - } - } - -@@ -139,7 +146,7 @@ public class ChunkStorage implements AutoCloseable { - LegacyStructureDataHandler persistentstructurelegacy = this.legacyStructureHandler; - - if (persistentstructurelegacy == null) { -- synchronized (this) { -+ synchronized (this.persistentDataLock) { // Paper - async chunk loading - persistentstructurelegacy = this.legacyStructureHandler; - if (persistentstructurelegacy == null) { - this.legacyStructureHandler = persistentstructurelegacy = LegacyStructureDataHandler.getLegacyStructureHandler(worldKey, (DimensionDataStorage) stateManagerGetter.get()); -@@ -165,10 +172,20 @@ public class ChunkStorage implements AutoCloseable { - } - - public CompletableFuture> read(ChunkPos chunkPos) { -- return this.worker.loadAsync(chunkPos); -+ // Paper start - async chunk io -+ try { -+ return CompletableFuture.completedFuture(Optional.ofNullable(this.readSync(chunkPos))); -+ } catch (Throwable thr) { -+ return CompletableFuture.failedFuture(thr); -+ } -+ } -+ @Nullable -+ public CompoundTag readSync(ChunkPos chunkPos) throws IOException { -+ return this.regionFileCache.read(chunkPos); - } -+ // Paper end - async chunk io - -- public CompletableFuture write(ChunkPos chunkPos, CompoundTag nbt) { -+ public CompletableFuture write(ChunkPos chunkPos, CompoundTag nbt) throws IOException { // Paper - rewrite chunk system; async chunk io - // Paper start - guard against serializing mismatching coordinates - if (nbt != null && !chunkPos.equals(ChunkSerializer.getChunkCoordinate(nbt))) { - final String world = (this instanceof net.minecraft.server.level.ChunkMap) ? ((net.minecraft.server.level.ChunkMap) this).level.getWorld().getName() : null; -@@ -176,26 +193,39 @@ public class ChunkStorage implements AutoCloseable { - + " but compound says coordinate is " + ChunkSerializer.getChunkCoordinate(nbt) + (world == null ? " for an unknown world" : (" for world: " + world))); - } - // Paper end - guard against serializing mismatching coordinates -+ this.regionFileCache.write(chunkPos, nbt); // Paper - rewrite chunk system; async chunk io, move above legacy structure index - this.handleLegacyStructureIndex(chunkPos); -- return this.worker.store(chunkPos, nbt); -+ // Paper - rewrite chunk system; async chunk io, move above legacy structure index -+ return null; - } - - protected void handleLegacyStructureIndex(ChunkPos chunkPos) { - if (this.legacyStructureHandler != null) { -+ synchronized (this.persistentDataLock) { // Paper - rewrite chunk system; async chunk io - this.legacyStructureHandler.removeIndex(chunkPos.toLong()); -+ } // Paper - rewrite chunk system; async chunk io - } - - } - - public void flushWorker() { -- this.worker.synchronize(true).join(); -+ io.papermc.paper.chunk.system.io.RegionFileIOThread.flush(); // Paper - rewrite chunk system - } - - public void close() throws IOException { -- this.worker.close(); -+ this.regionFileCache.close(); // Paper - nuke IO worker - } - - public ChunkScanAccess chunkScanner() { -- return this.worker; -+ // Paper start - nuke IO worker -+ return ((chunkPos, streamTagVisitor) -> { -+ try { -+ this.regionFileCache.scanChunk(chunkPos, streamTagVisitor); -+ return java.util.concurrent.CompletableFuture.completedFuture(null); -+ } catch (IOException e) { -+ throw new RuntimeException(e); -+ } -+ }); -+ // Paper end - } - } -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java -index 49d8a62d2b6ca6da4e02b3cec7e42c38b7781b57..9fdf8f857a5f9b231c6d0633eaba498244214f74 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java -@@ -27,43 +27,30 @@ public class EntityStorage implements EntityPersistentStorage { - private static final String ENTITIES_TAG = "Entities"; - private static final String POSITION_TAG = "Position"; - public final ServerLevel level; -- private final SimpleRegionStorage simpleRegionStorage; -+ // Paper - rewrite chunk system - private final LongSet emptyChunks = new LongOpenHashSet(); -- public final ProcessorMailbox entityDeserializerQueue; -+ // Paper - rewrite chunk system - - public EntityStorage(SimpleRegionStorage storage, ServerLevel world, Executor executor) { -- this.simpleRegionStorage = storage; -+ // Paper - rewrite chunk system - this.level = world; -- this.entityDeserializerQueue = ProcessorMailbox.create(executor, "entity-deserializer"); -+ // Paper - rewrite chunk system - } - - @Override - public CompletableFuture> loadEntities(ChunkPos pos) { -- return this.emptyChunks.contains(pos.toLong()) -- ? CompletableFuture.completedFuture(emptyChunk(pos)) -- : this.simpleRegionStorage.read(pos).thenApplyAsync(nbt -> { -- if (nbt.isEmpty()) { -- this.emptyChunks.add(pos.toLong()); -- return emptyChunk(pos); -- } else { -- try { -- ChunkPos chunkPos2 = readChunkPos(nbt.get()); -- if (!Objects.equals(pos, chunkPos2)) { -- LOGGER.error("Chunk file at {} is in the wrong location. (Expected {}, got {})", pos, pos, chunkPos2); -- } -- } catch (Exception var6) { -- LOGGER.warn("Failed to parse chunk {} position info", pos, var6); -- } -- -- CompoundTag compoundTag = this.simpleRegionStorage.upgradeChunkTag(nbt.get(), -1); -- ListTag listTag = compoundTag.getList("Entities", 10); -- List list = EntityType.loadEntitiesRecursive(listTag, this.level).collect(ImmutableList.toImmutableList()); -- return new ChunkEntities<>(pos, list); -- } -- }, this.entityDeserializerQueue::tell); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - copy out read logic into readEntities - } - -- private static ChunkPos readChunkPos(CompoundTag chunkNbt) { -+ // Paper start - rewrite chunk system -+ public static List readEntities(ServerLevel level, CompoundTag compoundTag) { -+ ListTag listTag = compoundTag.getList("Entities", 10); -+ List list = EntityType.loadEntitiesRecursive(listTag, level).collect(ImmutableList.toImmutableList()); -+ return list; -+ } -+ // Paper end - rewrite chunk system -+ -+ public static ChunkPos readChunkPos(CompoundTag chunkNbt) { // Paper - public - int[] is = chunkNbt.getIntArray("Position"); - return new ChunkPos(is[0], is[1]); - } -@@ -78,38 +65,74 @@ public class EntityStorage implements EntityPersistentStorage { - - @Override - public void storeEntities(ChunkEntities dataList) { -+ // Paper start - rewrite chunk system -+ if (true) { -+ throw new UnsupportedOperationException(); -+ } -+ // Paper end - rewrite chunk system - ChunkPos chunkPos = dataList.getPos(); - if (dataList.isEmpty()) { - if (this.emptyChunks.add(chunkPos.toLong())) { -- this.simpleRegionStorage.write(chunkPos, null); -+ // Paper - rewrite chunk system - fix compile for unused field in dead code - } - } else { -- ListTag listTag = new ListTag(); -- dataList.getEntities().forEach(entity -> { -- CompoundTag compoundTagx = new CompoundTag(); -- if (entity.save(compoundTagx)) { -- listTag.add(compoundTagx); -- } -- }); -- CompoundTag compoundTag = NbtUtils.addCurrentDataVersion(new CompoundTag()); -- compoundTag.put("Entities", listTag); -- writeChunkPos(compoundTag, chunkPos); -- this.simpleRegionStorage.write(chunkPos, compoundTag).exceptionally(ex -> { -- LOGGER.error("Failed to store chunk {}", chunkPos, ex); -- return null; -- }); -+ // Paper - move into saveEntityChunk0 - this.emptyChunks.remove(chunkPos.toLong()); - } - } - -+ // Paper start - rewrite chunk system -+ public static void copyEntities(final CompoundTag from, final CompoundTag into) { -+ if (from == null) { -+ return; -+ } -+ final ListTag entitiesFrom = from.getList("Entities", net.minecraft.nbt.Tag.TAG_COMPOUND); -+ if (entitiesFrom == null || entitiesFrom.isEmpty()) { -+ return; -+ } -+ -+ final ListTag entitiesInto = into.getList("Entities", net.minecraft.nbt.Tag.TAG_COMPOUND); -+ into.put("Entities", entitiesInto); // this is in case into doesn't have any entities -+ entitiesInto.addAll(0, entitiesFrom.copy()); // need to copy, this is coming from the save thread -+ } -+ -+ public static CompoundTag saveEntityChunk(List entities, ChunkPos chunkPos, ServerLevel level) { -+ return saveEntityChunk0(entities, chunkPos, level, false); -+ } -+ private static CompoundTag saveEntityChunk0(List entities, ChunkPos chunkPos, ServerLevel level, boolean force) { -+ if (!force && entities.isEmpty()) { -+ return null; -+ } -+ -+ ListTag listTag = new ListTag(); -+ entities.forEach((entity) -> { // diff here: use entities parameter -+ CompoundTag compoundTag = new CompoundTag(); -+ if (entity.save(compoundTag)) { -+ listTag.add(compoundTag); -+ } -+ -+ }); -+ CompoundTag compoundTag = NbtUtils.addCurrentDataVersion(new CompoundTag()); -+ compoundTag.put("Entities", listTag); -+ writeChunkPos(compoundTag, chunkPos); -+ // Paper - remove worker usage -+ -+ return !force && listTag.isEmpty() ? null : compoundTag; -+ } -+ -+ public static CompoundTag upgradeChunkTag(CompoundTag chunkNbt) { -+ int i = NbtUtils.getDataVersion(chunkNbt, -1); -+ return ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.ENTITY_CHUNK, chunkNbt, i, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()); -+ } -+ // Paper end - rewrite chunk system -+ - @Override - public void flush(boolean sync) { -- this.simpleRegionStorage.synchronize(sync).join(); -- this.entityDeserializerQueue.runAll(); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - - @Override - public void close() throws IOException { -- this.simpleRegionStorage.close(); -+ throw new UnsupportedOperationException(); // Paper - rewrite chunk system - } - } -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -index e858436bcf1b234d4bc6e6a117f5224d5c2d9f90..307196b2a58d4f8db3e6e3c3517a8004d4908b13 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -@@ -48,6 +48,7 @@ public class RegionFile implements AutoCloseable { - private final IntBuffer timestamps; - @VisibleForTesting - protected final RegionBitmap usedSectors; -+ public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(); // Paper - - public RegionFile(RegionStorageInfo storageKey, Path directory, Path path, boolean dsync) throws IOException { - this(storageKey, directory, path, RegionFileVersion.getCompressionFormat(), dsync); // Paper - Configurable region compression format -@@ -250,7 +251,7 @@ public class RegionFile implements AutoCloseable { - return (byteCount + 4096 - 1) / 4096; - } - -- public boolean doesChunkExist(ChunkPos pos) { -+ public synchronized boolean doesChunkExist(ChunkPos pos) { // Paper - synchronized - int i = this.getOffset(pos); - - if (i == 0) { -@@ -417,6 +418,11 @@ public class RegionFile implements AutoCloseable { - } - - public void close() throws IOException { -+ // Paper start - Prevent regionfiles from being closed during use -+ this.fileLock.lock(); -+ synchronized (this) { -+ try { -+ // Paper end - try { - this.padToFullSector(); - } finally { -@@ -426,6 +432,10 @@ public class RegionFile implements AutoCloseable { - this.file.close(); - } - } -+ } finally { // Paper start - Prevent regionfiles from being closed during use -+ this.fileLock.unlock(); -+ } -+ } // Paper end - - } - -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -index c4eef3aade889c69cefd873bec2d031cc54103ea..3f6955be976064eb542b5c50a9d6d74457c1833c 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -@@ -26,31 +26,99 @@ public class RegionFileStorage implements AutoCloseable { - private final Path folder; - private final boolean sync; - -- RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) { -+ // Paper start - cache regionfile does not exist state -+ static final int MAX_NON_EXISTING_CACHE = 1024 * 64; -+ private final it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet nonExistingRegionFiles = new it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet(); -+ private synchronized boolean doesRegionFilePossiblyExist(long position) { -+ if (this.nonExistingRegionFiles.contains(position)) { -+ this.nonExistingRegionFiles.addAndMoveToFirst(position); -+ return false; -+ } -+ return true; -+ } -+ -+ private synchronized void createRegionFile(long position) { -+ this.nonExistingRegionFiles.remove(position); -+ } -+ -+ private synchronized void markNonExisting(long position) { -+ if (this.nonExistingRegionFiles.addAndMoveToFirst(position)) { -+ while (this.nonExistingRegionFiles.size() >= MAX_NON_EXISTING_CACHE) { -+ this.nonExistingRegionFiles.removeLastLong(); -+ } -+ } -+ } -+ -+ public synchronized boolean doesRegionFileNotExistNoIO(ChunkPos pos) { -+ long key = ChunkPos.asLong(pos.getRegionX(), pos.getRegionZ()); -+ return !this.doesRegionFilePossiblyExist(key); -+ } -+ // Paper end - cache regionfile does not exist state -+ -+ protected RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) { // Paper - protected constructor - this.folder = directory; - this.sync = dsync; - this.info = storageKey; - } - -- private RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit -- long i = ChunkPos.asLong(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ()); -+ // Paper start -+ public synchronized RegionFile getRegionFileIfLoaded(ChunkPos chunkcoordintpair) { -+ return this.regionCache.getAndMoveToFirst(ChunkPos.asLong(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ())); -+ } -+ -+ public synchronized boolean chunkExists(ChunkPos pos) throws IOException { -+ RegionFile regionfile = getRegionFile(pos, true); -+ -+ return regionfile != null ? regionfile.hasChunk(pos) : false; -+ } -+ -+ public synchronized RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit -+ return this.getRegionFile(chunkcoordintpair, existingOnly, false); -+ } -+ public synchronized RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly, boolean lock) throws IOException { -+ // Paper end -+ long i = ChunkPos.asLong(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ()); final long regionPos = i; // Paper - OBFHELPER - RegionFile regionfile = (RegionFile) this.regionCache.getAndMoveToFirst(i); - - if (regionfile != null) { -+ // Paper start -+ if (lock) { -+ // must be in this synchronized block -+ regionfile.fileLock.lock(); -+ } -+ // Paper end - return regionfile; - } else { -+ // Paper start - cache regionfile does not exist state -+ if (existingOnly && !this.doesRegionFilePossiblyExist(regionPos)) { -+ return null; -+ } -+ // Paper end - cache regionfile does not exist state - if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper - Sanitise RegionFileCache and make configurable - ((RegionFile) this.regionCache.removeLast()).close(); - } - -- FileUtil.createDirectoriesSafe(this.folder); -+ // Paper - only create directory if not existing only - moved down - Path path = this.folder; - int j = chunkcoordintpair.getRegionX(); - Path path1 = path.resolve("r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca"); -- if (existingOnly && !java.nio.file.Files.exists(path1)) return null; // CraftBukkit -+ if (existingOnly && !java.nio.file.Files.exists(path1)) { // Paper start - cache regionfile does not exist state -+ this.markNonExisting(regionPos); -+ return null; // CraftBukkit -+ } else { -+ this.createRegionFile(regionPos); -+ } -+ // Paper end - cache regionfile does not exist state -+ FileUtil.createDirectoriesSafe(this.folder); // Paper - only create directory if not existing only - moved from above - RegionFile regionfile1 = new RegionFile(this.info, path1, this.folder, this.sync); - - this.regionCache.putAndMoveToFirst(i, regionfile1); -+ // Paper start -+ if (lock) { -+ // must be in this synchronized block -+ regionfile1.fileLock.lock(); -+ } -+ // Paper end - return regionfile1; - } - } -@@ -58,11 +126,12 @@ public class RegionFileStorage implements AutoCloseable { - @Nullable - public CompoundTag read(ChunkPos pos) throws IOException { - // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing -- RegionFile regionfile = this.getRegionFile(pos, true); -+ RegionFile regionfile = this.getRegionFile(pos, true, true); // Paper - if (regionfile == null) { - return null; - } - // CraftBukkit end -+ try { // Paper - DataInputStream datainputstream = regionfile.getChunkDataInputStream(pos); - - CompoundTag nbttagcompound; -@@ -99,6 +168,9 @@ public class RegionFileStorage implements AutoCloseable { - } - - return nbttagcompound; -+ } finally { // Paper start -+ regionfile.fileLock.unlock(); -+ } // Paper end - } - - public void scanChunk(ChunkPos chunkPos, StreamTagVisitor scanner) throws IOException { -@@ -133,7 +205,13 @@ public class RegionFileStorage implements AutoCloseable { - } - - protected void write(ChunkPos pos, @Nullable CompoundTag nbt) throws IOException { -- RegionFile regionfile = this.getRegionFile(pos, false); // CraftBukkit -+ // Paper start - rewrite chunk system -+ RegionFile regionfile = this.getRegionFile(pos, nbt == null, true); // CraftBukkit -+ if (nbt == null && regionfile == null) { -+ return; -+ } -+ try { // Try finally to unlock the region file -+ // Paper end - rewrite chunk system - // Paper start - Chunk save reattempt - int attempts = 0; - Exception lastException = null; -@@ -179,9 +257,14 @@ public class RegionFileStorage implements AutoCloseable { - net.minecraft.server.MinecraftServer.LOGGER.error("Failed to save chunk {}", pos, lastException); - } - // Paper end - Chunk save reattempt -+ // Paper start - rewrite chunk system -+ } finally { -+ regionfile.fileLock.unlock(); -+ } -+ // Paper end - rewrite chunk system - } - -- public void close() throws IOException { -+ public synchronized void close() throws IOException { // Paper -> synchronized - ExceptionCollector exceptionsuppressor = new ExceptionCollector<>(); - ObjectIterator objectiterator = this.regionCache.values().iterator(); - -@@ -198,7 +281,7 @@ public class RegionFileStorage implements AutoCloseable { - exceptionsuppressor.throwIfPresent(); - } - -- public void flush() throws IOException { -+ public synchronized void flush() throws IOException { // Paper - synchronize - ObjectIterator objectiterator = this.regionCache.values().iterator(); - - while (objectiterator.hasNext()) { -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java -index 151fcbca34e02783e19fbb7b54ec4fbec2eed190..883fbe5c81e3be27007a1a0489f80ba1863e5a04 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java -@@ -12,6 +12,7 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectMap; - import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; - import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; - import java.io.IOException; -+import java.nio.file.Path; - import java.util.Map; - import java.util.Optional; - import java.util.concurrent.CompletableFuture; -@@ -31,25 +32,30 @@ import net.minecraft.world.level.ChunkPos; - import net.minecraft.world.level.LevelHeightAccessor; - import org.slf4j.Logger; - --public class SectionStorage implements AutoCloseable { -+public class SectionStorage extends RegionFileStorage implements AutoCloseable { // Paper - nuke IOWorker - private static final Logger LOGGER = LogUtils.getLogger(); - private static final String SECTIONS_TAG = "Sections"; -- private final SimpleRegionStorage simpleRegionStorage; -+ // Paper - remove mojang I/O thread - private final Long2ObjectMap> storage = new Long2ObjectOpenHashMap<>(); - private final LongLinkedOpenHashSet dirty = new LongLinkedOpenHashSet(); - private final Function> codec; - private final Function factory; -- private final RegistryAccess registryAccess; -+ public final RegistryAccess registryAccess; // Paper - rewrite chunk system - public - protected final LevelHeightAccessor levelHeightAccessor; - - public SectionStorage( -+ // Paper start -+ RegionStorageInfo regionStorageInfo, -+ Path path, -+ boolean dsync, -+ // Paper end - SimpleRegionStorage storageAccess, - Function> codecFactory, - Function factory, - RegistryAccess registryManager, - LevelHeightAccessor world - ) { -- this.simpleRegionStorage = storageAccess; -+ super(regionStorageInfo, path, dsync); // Paper - remove mojang I/O thread - this.codec = codecFactory; - this.factory = factory; - this.registryAccess = registryManager; -@@ -112,23 +118,21 @@ public class SectionStorage implements AutoCloseable { - } - - private void readColumn(ChunkPos pos) { -- Optional optional = this.tryRead(pos).join(); -- RegistryOps registryOps = this.registryAccess.createSerializationContext(NbtOps.INSTANCE); -- this.readColumn(pos, registryOps, optional.orElse(null)); -+ throw new IllegalStateException("Only chunk system can load in state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system - } - - private CompletableFuture> tryRead(ChunkPos pos) { -- return this.simpleRegionStorage.read(pos).exceptionally(throwable -> { -- if (throwable instanceof IOException iOException) { -- LOGGER.error("Error reading chunk {} data from disk", pos, iOException); -- return Optional.empty(); -- } else { -- throw new CompletionException(throwable); -- } -- }); -+ // Paper start - rewrite chunk system -+ try { -+ return CompletableFuture.completedFuture(Optional.ofNullable(this.read(pos))); -+ } catch (Throwable thr) { -+ return CompletableFuture.failedFuture(thr); -+ } -+ // Paper end - rewrite chunk system - } - - private void readColumn(ChunkPos pos, RegistryOps ops, @Nullable CompoundTag nbt) { -+ if (true) throw new IllegalStateException("Only chunk system can load in state, offending class:" + this.getClass().getName()); // Paper - rewrite chunk system - if (nbt == null) { - for (int i = this.levelHeightAccessor.getMinSection(); i < this.levelHeightAccessor.getMaxSection(); i++) { - this.storage.put(getKey(pos, i), Optional.empty()); -@@ -138,7 +142,7 @@ public class SectionStorage implements AutoCloseable { - int j = getVersion(dynamic); - int k = SharedConstants.getCurrentVersion().getDataVersion().getVersion(); - boolean bl = j != k; -- Dynamic dynamic2 = this.simpleRegionStorage.upgradeChunkTag(dynamic, j); -+ Dynamic dynamic2 = null; // Paper - rewrite chunk system - OptionalDynamic optionalDynamic = dynamic2.get("Sections"); - - for (int l = this.levelHeightAccessor.getMinSection(); l < this.levelHeightAccessor.getMaxSection(); l++) { -@@ -162,7 +166,7 @@ public class SectionStorage implements AutoCloseable { - Dynamic dynamic = this.writeColumn(pos, registryOps); - Tag tag = dynamic.getValue(); - if (tag instanceof CompoundTag) { -- this.simpleRegionStorage.write(pos, (CompoundTag)tag); -+ try { this.write(pos, (CompoundTag)tag); } catch (IOException ex) { SectionStorage.LOGGER.error("Error writing poi chunk data to disk for chunk " + pos, ex); } // Paper - nuke IOWorker - } else { - LOGGER.error("Expected compound tag, got {}", tag); - } -@@ -212,7 +216,7 @@ public class SectionStorage implements AutoCloseable { - } - - private static int getVersion(Dynamic dynamic) { -- return dynamic.get("DataVersion").asInt(1945); -+ return dynamic.get("DataVersion").asInt(1945); // Paper - diff on change, constant used in ChunkLoadTask - } - - public void flush(ChunkPos pos) { -@@ -229,6 +233,6 @@ public class SectionStorage implements AutoCloseable { - - @Override - public void close() throws IOException { -- this.simpleRegionStorage.close(); -+ super.close(); // Paper - nuke I/O worker - don't call the worker - } - } -diff --git a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java -index 74a285b8b018a9c94ccea519f1ce8b9e2ef3cb64..83a39f900551e39d5af6f17a339a386ddee4feef 100644 ---- a/src/main/java/net/minecraft/world/level/entity/EntityTickList.java -+++ b/src/main/java/net/minecraft/world/level/entity/EntityTickList.java -@@ -9,52 +9,41 @@ import javax.annotation.Nullable; - import net.minecraft.world.entity.Entity; - - public class EntityTickList { -- private Int2ObjectMap active = new Int2ObjectLinkedOpenHashMap<>(); -- private Int2ObjectMap passive = new Int2ObjectLinkedOpenHashMap<>(); -- @Nullable -- private Int2ObjectMap iterated; -+ private final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet entities = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(true); // Paper - rewrite this, always keep this updated - why would we EVER tick an entity that's not ticking? - - private void ensureActiveIsNotIterated() { -- if (this.iterated == this.active) { -- this.passive.clear(); -- -- for (Entry entry : Int2ObjectMaps.fastIterable(this.active)) { -- this.passive.put(entry.getIntKey(), entry.getValue()); -- } -- -- Int2ObjectMap int2ObjectMap = this.active; -- this.active = this.passive; -- this.passive = int2ObjectMap; -- } -+ // Paper - replace with better logic, do not delay removals - } - - public void add(Entity entity) { -+ io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist addition"); // Paper - this.ensureActiveIsNotIterated(); -- this.active.put(entity.getId(), entity); -+ this.entities.add(entity); // Paper - replace with better logic, do not delay removals/additions - } - - public void remove(Entity entity) { -+ io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist removal"); // Paper - this.ensureActiveIsNotIterated(); -- this.active.remove(entity.getId()); -+ this.entities.remove(entity); // Paper - replace with better logic, do not delay removals/additions - } - - public boolean contains(Entity entity) { -- return this.active.containsKey(entity.getId()); -+ return this.entities.contains(entity); // Paper - replace with better logic, do not delay removals/additions - } - - public void forEach(Consumer action) { -- if (this.iterated != null) { -- throw new UnsupportedOperationException("Only one concurrent iteration supported"); -- } else { -- this.iterated = this.active; -- -- try { -- for (Entity entity : this.active.values()) { -- action.accept(entity); -- } -- } finally { -- this.iterated = null; -+ io.papermc.paper.util.TickThread.ensureTickThread("Asynchronous entity ticklist iteration"); // Paper -+ // Paper start - replace with better logic, do not delay removals/additions -+ // To ensure nothing weird happens with dimension travelling, do not iterate over new entries... -+ // (by dfl iterator() is configured to not iterate over new entries) -+ io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.Iterator iterator = this.entities.iterator(); -+ try { -+ while (iterator.hasNext()) { -+ action.accept(iterator.next()); - } -+ } finally { -+ iterator.finishedIterating(); - } -+ // Paper end - replace with better logic, do not delay removals/additions - } - } -diff --git a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java -index 5d15c228c044a36c67014793decb314240cf6be1..dc765b92cc90f5f370254e68bbbdfa5add7935ce 100644 ---- a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java -+++ b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java -@@ -87,7 +87,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator { - return CompletableFuture.supplyAsync(Util.wrapThreadWithTaskName("init_biomes", () -> { - this.doCreateBiomes(blender, noiseConfig, structureAccessor, chunk); - return chunk; -- }), Util.backgroundExecutor()); -+ }), executor); // Paper - run with supplied executor - } - - private void doCreateBiomes(Blender blender, RandomState noiseConfig, StructureManager structureAccessor, ChunkAccess chunk) { -@@ -286,7 +286,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator { - - return CompletableFuture.supplyAsync(Util.wrapThreadWithTaskName("wgen_fill_noise", () -> { - return this.doFill(blender, structureAccessor, noiseConfig, chunk, j, k); -- }), Util.backgroundExecutor()).whenCompleteAsync((ichunkaccess1, throwable) -> { -+ }), executor).whenCompleteAsync((ichunkaccess1, throwable) -> { // Paper - run with supplied executor - Iterator iterator = set.iterator(); - - while (iterator.hasNext()) { -diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java -index 609100ed7aa0b23aa5a9c6fbf6878ea320bd3a93..7068657b28a9bc175ee23f5a18defb41168f1d76 100644 ---- a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java -+++ b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureCheck.java -@@ -47,8 +47,101 @@ public class StructureCheck { - private final BiomeSource biomeSource; - private final long seed; - private final DataFixer fixerUpper; -- private final Long2ObjectMap> loadedChunks = new Long2ObjectOpenHashMap<>(); -- private final Map featureChecks = new HashMap<>(); -+ // Paper start - rewrite chunk system - synchronise this class -+ // additionally, make sure to purge entries from the maps so it does not leak memory -+ private static final int CHUNK_TOTAL_LIMIT = 50 * (2 * 100 + 1) * (2 * 100 + 1); // cache 50 structure lookups -+ private static final int PER_FEATURE_CHECK_LIMIT = 50 * (2 * 100 + 1) * (2 * 100 + 1); // cache 50 structure lookups -+ -+ private final SynchronisedLong2ObjectMap> loadedChunksSafe = new SynchronisedLong2ObjectMap<>(CHUNK_TOTAL_LIMIT); -+ private final java.util.concurrent.ConcurrentHashMap featureChecksSafe = new java.util.concurrent.ConcurrentHashMap<>(); -+ -+ private static final class SynchronisedLong2ObjectMap { -+ private final it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap map = new it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap<>(); -+ private final int limit; -+ -+ public SynchronisedLong2ObjectMap(final int limit) { -+ this.limit = limit; -+ } -+ -+ // must hold lock on map -+ private void purgeEntries() { -+ while (this.map.size() > this.limit) { -+ this.map.removeLast(); -+ } -+ } -+ -+ public V get(final long key) { -+ synchronized (this.map) { -+ return this.map.getAndMoveToFirst(key); -+ } -+ } -+ -+ public V put(final long key, final V value) { -+ synchronized (this.map) { -+ final V ret = this.map.putAndMoveToFirst(key, value); -+ this.purgeEntries(); -+ return ret; -+ } -+ } -+ -+ public V compute(final long key, final java.util.function.BiFunction remappingFunction) { -+ synchronized (this.map) { -+ // first, compute the value - if one is added, it will be at the last entry -+ this.map.compute(key, remappingFunction); -+ // move the entry to first, just in case it was added at last -+ final V ret = this.map.getAndMoveToFirst(key); -+ // now purge the last entries -+ this.purgeEntries(); -+ -+ return ret; -+ } -+ } -+ } -+ -+ private static final class SynchronisedLong2BooleanMap { -+ private final it.unimi.dsi.fastutil.longs.Long2BooleanLinkedOpenHashMap map = new it.unimi.dsi.fastutil.longs.Long2BooleanLinkedOpenHashMap(); -+ private final int limit; -+ -+ public SynchronisedLong2BooleanMap(final int limit) { -+ this.limit = limit; -+ } -+ -+ // must hold lock on map -+ private void purgeEntries() { -+ while (this.map.size() > this.limit) { -+ this.map.removeLastBoolean(); -+ } -+ } -+ -+ public boolean remove(final long key) { -+ synchronized (this.map) { -+ return this.map.remove(key); -+ } -+ } -+ -+ // note: -+ public boolean getOrCompute(final long key, final it.unimi.dsi.fastutil.longs.Long2BooleanFunction ifAbsent) { -+ synchronized (this.map) { -+ if (this.map.containsKey(key)) { -+ return this.map.getAndMoveToFirst(key); -+ } -+ } -+ -+ final boolean put = ifAbsent.get(key); -+ -+ synchronized (this.map) { -+ if (this.map.containsKey(key)) { -+ return this.map.getAndMoveToFirst(key); -+ } -+ this.map.putAndMoveToFirst(key, put); -+ -+ this.purgeEntries(); -+ -+ return put; -+ } -+ } -+ } -+ // Paper end - rewrite chunk system - synchronise this class - - public StructureCheck( - ChunkScanAccess chunkIoWorker, -@@ -90,7 +183,7 @@ public class StructureCheck { - - public StructureCheckResult checkStart(ChunkPos pos, Structure type, StructurePlacement placement, boolean skipReferencedStructures) { - long l = pos.toLong(); -- Object2IntMap object2IntMap = this.loadedChunks.get(l); -+ Object2IntMap object2IntMap = this.loadedChunksSafe.get(l); // Paper - rewrite chunk system - synchronise this class - if (object2IntMap != null) { - return this.checkStructureInfo(object2IntMap, type, skipReferencedStructures); - } else { -@@ -100,9 +193,9 @@ public class StructureCheck { - } else if (!placement.applyAdditionalChunkRestrictions(pos.x, pos.z, this.seed, this.getSaltOverride(type))) { // Paper - add missing structure seed configs - return StructureCheckResult.START_NOT_PRESENT; - } else { -- boolean bl = this.featureChecks -- .computeIfAbsent(type, structure2 -> new Long2BooleanOpenHashMap()) -- .computeIfAbsent(l, chunkPos -> this.canCreateStructure(pos, type)); -+ boolean bl = this.featureChecksSafe // Paper - rewrite chunk system - synchronise this class -+ .computeIfAbsent(type, structure2 -> new SynchronisedLong2BooleanMap(PER_FEATURE_CHECK_LIMIT)) // Paper - rewrite chunk system - synchronise this class -+ .getOrCompute(l, chunkPos -> this.canCreateStructure(pos, type)); // Paper - rewrite chunk system - synchronise this class - return !bl ? StructureCheckResult.START_NOT_PRESENT : StructureCheckResult.CHUNK_LOAD_NEEDED; - } - } -@@ -228,15 +321,26 @@ public class StructureCheck { - } - - private void storeFullResults(long pos, Object2IntMap referencesByStructure) { -- this.loadedChunks.put(pos, deduplicateEmptyMap(referencesByStructure)); -- this.featureChecks.values().forEach(generationPossibilityByChunkPos -> generationPossibilityByChunkPos.remove(pos)); -+ // Paper start - rewrite chunk system - synchronise this class -+ this.loadedChunksSafe.put(pos, deduplicateEmptyMap(referencesByStructure)); -+ // once we insert into loadedChunks, we don't really need to be very careful about removing everything -+ // from this map, as everything that checks this map uses loadedChunks first -+ // so, one way or another it's a race condition that doesn't matter -+ for (SynchronisedLong2BooleanMap value : this.featureChecksSafe.values()) { -+ value.remove(pos); -+ } -+ // Paper end - rewrite chunk system - synchronise this class - } - - public void incrementReference(ChunkPos pos, Structure structure) { -- this.loadedChunks.compute(pos.toLong(), (posx, referencesByStructure) -> { -- if (referencesByStructure == null || referencesByStructure.isEmpty()) { -+ this.loadedChunksSafe.compute(pos.toLong(), (posx, referencesByStructure) -> { // Paper start - rewrite chunk system - synchronise this class -+ // make this COW so that we do not mutate state that may be currently in use -+ if (referencesByStructure == null) { - referencesByStructure = new Object2IntOpenHashMap<>(); -+ } else { -+ referencesByStructure = referencesByStructure instanceof Object2IntOpenHashMap fastClone ? fastClone.clone() : new Object2IntOpenHashMap<>(referencesByStructure); - } -+ // Paper end - rewrite chunk system - synchronise this class - - referencesByStructure.computeInt(structure, (feature, references) -> references == null ? 1 : references + 1); - return referencesByStructure; -diff --git a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java -index 47c2b2da9799690291396effb9e1b06d71efc6fd..2cdd18f724296f10cd4a522d1e8196723d39cf45 100644 ---- a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java -+++ b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java -@@ -26,6 +26,19 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon - @Nullable - private BiConsumer, ScheduledTick> onTickAdded; - -+ // Paper start - add dirty flag -+ private boolean dirty; -+ private long lastSaved = Long.MIN_VALUE; -+ -+ public boolean isDirty(final long tick) { -+ return this.dirty || (!this.tickQueue.isEmpty() && tick != this.lastSaved); -+ } -+ -+ public void clearDirty() { -+ this.dirty = false; -+ } -+ // Paper end - add dirty flag -+ - public LevelChunkTicks() { - } - -@@ -50,6 +63,7 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon - public ScheduledTick poll() { - ScheduledTick scheduledTick = this.tickQueue.poll(); - if (scheduledTick != null) { -+ this.dirty = true; // Paper - add dirty flag - this.ticksPerPosition.remove(scheduledTick); - } - -@@ -59,6 +73,7 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon - @Override - public void schedule(ScheduledTick orderedTick) { - if (this.ticksPerPosition.add(orderedTick)) { -+ this.dirty = true; // Paper - add dirty flag - this.scheduleUnchecked(orderedTick); - } - } -@@ -81,7 +96,7 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon - while (iterator.hasNext()) { - ScheduledTick scheduledTick = iterator.next(); - if (predicate.test(scheduledTick)) { -- iterator.remove(); -+ iterator.remove(); this.dirty = true; // Paper - add dirty flag - this.ticksPerPosition.remove(scheduledTick); - } - } -@@ -98,6 +113,7 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon - - @Override - public ListTag save(long l, Function function) { -+ this.lastSaved = l; // Paper - add dirty system to level ticks - ListTag listTag = new ListTag(); - if (this.pendingTicks != null) { - for (SavedTick savedTick : this.pendingTicks) { -@@ -114,6 +130,11 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon - - public void unpack(long time) { - if (this.pendingTicks != null) { -+ // Paper start - add dirty system to level chunk ticks -+ if (this.tickQueue.isEmpty()) { -+ this.lastSaved = time; -+ } -+ // Paper end - add dirty system to level chunk ticks - int i = -this.pendingTicks.size(); - - for (SavedTick savedTick : this.pendingTicks) { -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java -index 7dae8d91b74cc7df0745f0c121e3bea09b8d0b6d..1e2530c9e5212b6d2bdbc94817beddb4247dac73 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java -@@ -115,7 +115,7 @@ public class CraftChunk implements Chunk { - - @Override - public boolean isEntitiesLoaded() { -- return this.getCraftWorld().getHandle().entityManager.areEntitiesLoaded(ChunkPos.asLong(this.x, this.z)); -+ return this.getCraftWorld().getHandle().areEntitiesLoaded(io.papermc.paper.util.CoordinateUtils.getChunkKey(this.x, this.z)); // Paper - rewrite chunk system - } - - @Override -@@ -124,51 +124,7 @@ public class CraftChunk implements Chunk { - this.getWorld().getChunkAt(this.x, this.z); // Transient load for this tick - } - -- PersistentEntitySectionManager entityManager = this.getCraftWorld().getHandle().entityManager; -- long pair = ChunkPos.asLong(this.x, this.z); -- -- if (entityManager.areEntitiesLoaded(pair)) { -- return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream() -- .map(net.minecraft.world.entity.Entity::getBukkitEntity) -- .filter(Objects::nonNull).toArray(Entity[]::new); -- } -- -- entityManager.ensureChunkQueuedForLoad(pair); // Start entity loading -- -- // SPIGOT-6772: Use entity mailbox and re-schedule entities if they get unloaded -- ProcessorMailbox mailbox = ((EntityStorage) entityManager.permanentStorage).entityDeserializerQueue; -- BooleanSupplier supplier = () -> { -- // only execute inbox if our entities are not present -- if (entityManager.areEntitiesLoaded(pair)) { -- return true; -- } -- -- if (!entityManager.isPending(pair)) { -- // Our entities got unloaded, this should normally not happen. -- entityManager.ensureChunkQueuedForLoad(pair); // Re-start entity loading -- } -- -- // tick loading inbox, which loads the created entities to the world -- // (if present) -- entityManager.tick(); -- // check if our entities are loaded -- return entityManager.areEntitiesLoaded(pair); -- }; -- -- // now we wait until the entities are loaded, -- // the converting from NBT to entity object is done on the main Thread which is why we wait -- while (!supplier.getAsBoolean()) { -- if (mailbox.size() != 0) { -- mailbox.run(); -- } else { -- Thread.yield(); -- LockSupport.parkNanos("waiting for entity loading", 100000L); -- } -- } -- -- return entityManager.getEntities(new ChunkPos(this.x, this.z)).stream() -- .map(net.minecraft.world.entity.Entity::getBukkitEntity) -- .filter(Objects::nonNull).toArray(Entity[]::new); -+ return this.getCraftWorld().getHandle().getChunkEntities(this.x, this.z); // Paper - rewrite chunk system - } - - @Override -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index 5467a5985eaf159e2a4f19dd5723d05d8f2d598e..66fa93a44a348f9cd300ec0c4a2d09a7a2bf47e1 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -@@ -1407,7 +1407,6 @@ public final class CraftServer implements Server { - // Paper - Put world into worldlist before initing the world; move up - - this.getServer().prepareLevels(internal.getChunkSource().chunkMap.progressListener, internal); -- internal.entityManager.tick(); // SPIGOT-6526: Load pending entities so they are available to the API - - this.pluginManager.callEvent(new WorldLoadEvent(internal.getWorld())); - return internal.getWorld(); -@@ -1452,7 +1451,7 @@ public final class CraftServer implements Server { - } - - handle.getChunkSource().close(save); -- handle.entityManager.close(save); // SPIGOT-6722: close entityManager -+ // handle.entityManager.close(save); // SPIGOT-6722: close entityManager // Paper - rewrite chunk system - handle.convertable.close(); - } catch (Exception ex) { - this.getLogger().log(Level.SEVERE, null, ex); -@@ -2488,7 +2487,7 @@ public final class CraftServer implements Server { - - @Override - public boolean isPrimaryThread() { -- return Thread.currentThread().equals(this.console.serverThread) || this.console.hasStopped() || !org.spigotmc.AsyncCatcher.enabled; // All bets are off if we have shut down (e.g. due to watchdog) -+ return io.papermc.paper.util.TickThread.isTickThread(); // Paper - rewrite chunk system - } - - // Paper start - Adventure -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index 4b6a04e47f5d4c071607516519098fab317dcf12..01fc74e6cc8ea8808b821583afb26309587dc003 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -@@ -518,10 +518,14 @@ public class CraftWorld extends CraftRegionAccessor implements World { - ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z)); - if (playerChunk == null) return false; - -- playerChunk.getTickingChunkFuture().thenAccept(either -> { -- either.ifSuccess(chunk -> { -+ // Paper start - rewrite player chunk loader -+ net.minecraft.world.level.chunk.LevelChunk chunk = playerChunk.getSendingChunk(); -+ if (chunk == null) { -+ return false; -+ } -+ // Paper end - rewrite player chunk loader - List playersInRange = playerChunk.playerProvider.getPlayers(playerChunk.getPos(), false); -- if (playersInRange.isEmpty()) return; -+ if (playersInRange.isEmpty()) return true; // Paper - rewrite player chunk loader - - ClientboundLevelChunkWithLightPacket refreshPacket = new ClientboundLevelChunkWithLightPacket(chunk, this.world.getLightEngine(), null, null); - for (ServerPlayer player : playersInRange) { -@@ -529,8 +533,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { - - player.connection.send(refreshPacket); - } -- }); -- }); -+ // Paper - rewrite player chunk loader - - return true; - } -@@ -609,20 +612,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { - @Override - public Collection getPluginChunkTickets(int x, int z) { - DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager; -- SortedArraySet> tickets = chunkDistanceManager.tickets.get(ChunkPos.asLong(x, z)); -- -- if (tickets == null) { -- return Collections.emptyList(); -- } -- -- ImmutableList.Builder ret = ImmutableList.builder(); -- for (Ticket ticket : tickets) { -- if (ticket.getType() == TicketType.PLUGIN_TICKET) { -- ret.add((Plugin) ticket.key); -- } -- } -- -- return ret.build(); -+ return chunkDistanceManager.getChunkHolderManager().getPluginChunkTickets(x, z); // Paper - rewrite chunk system - } - - @Override -@@ -630,7 +620,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { - Map> ret = new HashMap<>(); - DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager; - -- for (Long2ObjectMap.Entry>> chunkTickets : chunkDistanceManager.tickets.long2ObjectEntrySet()) { -+ for (Long2ObjectMap.Entry>> chunkTickets : chunkDistanceManager.getChunkHolderManager().getTicketsCopy().long2ObjectEntrySet()) { // Paper - rewrite chunk system - long chunkKey = chunkTickets.getLongKey(); - SortedArraySet> tickets = chunkTickets.getValue(); - -@@ -1327,12 +1317,12 @@ public class CraftWorld extends CraftRegionAccessor implements World { - - @Override - public int getViewDistance() { -- return this.world.getChunkSource().chunkMap.serverViewDistance; -+ return this.getHandle().playerChunkLoader.getAPIViewDistance(); // Paper - replace player chunk loader - } - - @Override - public int getSimulationDistance() { -- return this.world.getChunkSource().chunkMap.getDistanceManager().simulationDistance; -+ return this.getHandle().playerChunkLoader.getAPITickDistance(); // Paper - replace player chunk loader - } - - public BlockMetadataStore getBlockMetadata() { -@@ -2495,17 +2485,20 @@ public class CraftWorld extends CraftRegionAccessor implements World { - - @Override - public void setSimulationDistance(final int simulationDistance) { -- throw new UnsupportedOperationException("Not implemented yet"); -+ if (simulationDistance < 2 || simulationDistance > 32) { -+ throw new IllegalArgumentException("Simulation distance " + simulationDistance + " is out of range of [2, 32]"); -+ } -+ this.getHandle().chunkSource.chunkMap.setTickViewDistance(simulationDistance); - } - - @Override - public int getSendViewDistance() { -- return this.getViewDistance(); -+ return this.getHandle().playerChunkLoader.getAPISendViewDistance(); // Paper - replace player chunk loader - } - - @Override - public void setSendViewDistance(final int viewDistance) { -- throw new UnsupportedOperationException("Not implemented yet"); -+ this.getHandle().chunkSource.chunkMap.setSendViewDistance(viewDistance); // Paper - replace player chunk loader - } - - // Paper start - implement pointers -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index e1af41a8d938420694a5342f08b811615dc46003..33c553d1558b3cdfac22948f3878e1a31d70f02b 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -@@ -3457,31 +3457,31 @@ public class CraftPlayer extends CraftHumanEntity implements Player { - - @Override - public int getViewDistance() { -- return io.papermc.paper.chunk.system.ChunkSystem.getLoadViewDistance(this.getHandle()); -+ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPIViewDistance(this); - } - - @Override - public void setViewDistance(final int viewDistance) { -- throw new UnsupportedOperationException("Not implemented yet"); -+ this.getHandle().setLoadViewDistance(viewDistance < 0 ? viewDistance : viewDistance + 1); - } - - @Override - public int getSimulationDistance() { -- return io.papermc.paper.chunk.system.ChunkSystem.getTickViewDistance(this.getHandle()); -+ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPITickViewDistance(this); - } - - @Override - public void setSimulationDistance(final int simulationDistance) { -- throw new UnsupportedOperationException("Not implemented yet"); -+ this.getHandle().setTickViewDistance(simulationDistance); - } - - @Override - public int getSendViewDistance() { -- return io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(this.getHandle()); -+ return io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.getAPISendViewDistance(this); - } - - @Override - public void setSendViewDistance(final int viewDistance) { -- throw new UnsupportedOperationException("Not implemented yet"); -+ this.getHandle().setSendViewDistance(viewDistance); - } - } -diff --git a/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java b/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java -index b65710b648e31ab74204b5abd9397d9e6e26dac4..c77f722131e0e40e9de29bf8d42f9bc5d8fa2f7d 100644 ---- a/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java -+++ b/src/main/java/org/bukkit/craftbukkit/generator/CustomChunkGenerator.java -@@ -264,7 +264,7 @@ public class CustomChunkGenerator extends InternalChunkGenerator { - return ichunkaccess1; - }; - -- return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), net.minecraft.Util.backgroundExecutor()) : future.thenApply(function); -+ return future == null ? CompletableFuture.supplyAsync(() -> function.apply(chunk), executor) : future.thenApply(function); // Paper - run with supplied executor - } - - @Override -diff --git a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java -index cd7f1309cf01a5f01a28aded03a36fe15adb1756..41a291d42667c38d3e5bbe47236772761e85929b 100644 ---- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java -+++ b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java -@@ -815,19 +815,39 @@ public abstract class DelegatedGeneratorAccess implements WorldGenLevel { - @Nullable - @Override - public BlockState getBlockStateIfLoaded(final BlockPos blockposition) { -- return null; -+ return this.handle.getBlockStateIfLoaded(blockposition); - } - - @Nullable - @Override - public FluidState getFluidIfLoaded(final BlockPos blockposition) { -- return null; -+ return this.handle.getFluidIfLoaded(blockposition); - } - - @Nullable - @Override - public ChunkAccess getChunkIfLoadedImmediately(final int x, final int z) { -- return null; -+ return this.handle.getChunkIfLoadedImmediately(x, z); -+ } -+ -+ @Override -+ public void getHardCollidingEntities(final Entity except, final AABB box, final Predicate predicate, final List into) { -+ this.handle.getHardCollidingEntities(except, box, predicate, into); -+ } -+ -+ @Override -+ public List getHardCollidingEntities(final Entity except, final AABB box, final Predicate predicate) { -+ return this.handle.getHardCollidingEntities(except, box, predicate); -+ } -+ -+ @Override -+ public void getEntities(final Entity except, final AABB box, final Predicate predicate, final List into) { -+ this.handle.getEntities(except, box, predicate, into); -+ } -+ -+ @Override -+ public void getEntitiesByClass(final Class clazz, final Entity except, final AABB box, final List into, final Predicate predicate) { -+ this.handle.getEntitiesByClass(clazz, except, box, into, predicate); - } - // Paper end - } -diff --git a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java -index e8a73d34dbb372581b03018aade170a31c266099..210f454a840aa5564f7cbf33b83d31aa74814c84 100644 ---- a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java -+++ b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java -@@ -268,4 +268,19 @@ public class DummyGeneratorAccess implements WorldGenLevel { - @Override - public void scheduleTick(BlockPos pos, Fluid fluid, int delay, net.minecraft.world.ticks.TickPriority priority) {} - // Paper end - add more methods -+ // Paper start -+ @Override -+ public List getHardCollidingEntities(Entity except, AABB box, Predicate predicate) { -+ return java.util.Collections.emptyList(); -+ } -+ -+ @Override -+ public void getEntities(Entity except, AABB box, Predicate predicate, List into) {} -+ -+ @Override -+ public void getHardCollidingEntities(Entity except, AABB box, Predicate predicate, List into) {} -+ -+ @Override -+ public void getEntitiesByClass(Class clazz, Entity except, AABB box, List into, Predicate predicate) {} -+ // Paper end - } -diff --git a/src/main/java/org/spigotmc/AsyncCatcher.java b/src/main/java/org/spigotmc/AsyncCatcher.java -index e8e3cc48cf1c58bd8151d1f28df28781859cd0e3..2e074c16dab1ead47914070329da0398c3274048 100644 ---- a/src/main/java/org/spigotmc/AsyncCatcher.java -+++ b/src/main/java/org/spigotmc/AsyncCatcher.java -@@ -9,7 +9,7 @@ public class AsyncCatcher - - public static void catchOp(String reason) - { -- if ( (AsyncCatcher.enabled || io.papermc.paper.util.TickThread.STRICT_THREAD_CHECKS) && Thread.currentThread() != MinecraftServer.getServer().serverThread ) // Paper -+ if (!(io.papermc.paper.util.TickThread.isTickThread())) // Paper - { - MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); // Paper - throw new IllegalStateException( "Asynchronous " + reason + "!" ); -diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java -index ad282d34919716b75acd10426cd071da9d064a51..9e5d08f57aa448552d100ca892c211d44441ef68 100644 ---- a/src/main/java/org/spigotmc/WatchdogThread.java -+++ b/src/main/java/org/spigotmc/WatchdogThread.java -@@ -8,7 +8,7 @@ import java.util.logging.Logger; - import net.minecraft.server.MinecraftServer; - import org.bukkit.Bukkit; - --public class WatchdogThread extends Thread -+public final class WatchdogThread extends io.papermc.paper.util.TickThread // Paper - rewrite chunk system - { - - private static WatchdogThread instance; -@@ -115,6 +115,7 @@ public class WatchdogThread extends Thread - // Paper end - Different message for short timeout - log.log( Level.SEVERE, "------------------------------" ); - log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper -+ io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(isLongTimeout); // Paper - rewrite chunk system - WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log ); - log.log( Level.SEVERE, "------------------------------" ); - // diff --git a/patches/server/0986-incremental-chunk-and-player-saving.patch b/patches/server/0986-incremental-chunk-and-player-saving.patch new file mode 100644 index 0000000000..22e1d98765 --- /dev/null +++ b/patches/server/0986-incremental-chunk-and-player-saving.patch @@ -0,0 +1,167 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Shane Freeder +Date: Sun, 9 Jun 2019 03:53:22 +0100 +Subject: [PATCH] incremental chunk and player saving + + +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 40708dfd84dc21bc5e047d9fb3b092cb854a8041..7f3359745089584448b8f32f0570cf86b73076c7 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -916,7 +916,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0 && this.ticksUntilAutosave <= 0) { +- this.ticksUntilAutosave = this.autosavePeriod; +- // CraftBukkit end +- MinecraftServer.LOGGER.debug("Autosave started"); +- this.profiler.push("save"); +- this.saveEverything(true, false, false); +- this.profiler.pop(); +- MinecraftServer.LOGGER.debug("Autosave finished"); ++ // Paper start - Incremental chunk and player saving ++ int playerSaveInterval = io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.rate; ++ if (playerSaveInterval < 0) { ++ playerSaveInterval = autosavePeriod; + } ++ this.profiler.push("save"); ++ final boolean fullSave = autosavePeriod > 0 && this.tickCount % autosavePeriod == 0; ++ try { ++ this.isSaving = true; ++ if (playerSaveInterval > 0) { ++ this.playerList.saveAll(playerSaveInterval); ++ } ++ for (ServerLevel level : this.getAllLevels()) { ++ if (level.paperConfig().chunks.autoSaveInterval.value() > 0) { ++ level.saveIncrementally(fullSave); ++ } ++ } ++ } finally { ++ this.isSaving = false; ++ } ++ this.profiler.pop(); ++ // Paper end - Incremental chunk and player saving + io.papermc.paper.util.CachedLists.reset(); // Paper + // Paper start - move executeAll() into full server tick timing + try (co.aikar.timings.Timing ignored = MinecraftTimings.processTasksTimer.startTiming()) { +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index f74efe41cd0da2f9749fc96fb9e0f7cf237ad1c6..d1728e13a7b649f308bde90ab633c79d86c10822 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -430,6 +430,15 @@ public class ServerChunkCache extends ChunkSource { + } // Paper - Timings + } + ++ // Paper start - Incremental chunk and player saving; duplicate save, but call incremental ++ public void saveIncrementally() { ++ this.runDistanceManagerUpdates(); ++ try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings ++ this.chunkMap.saveIncrementally(); ++ } // Paper - Timings ++ } ++ // Paper end - Incremental chunk and player saving ++ + @Override + public void close() throws IOException { + // CraftBukkit start +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 7f8793fa8260cb8a1f55d0ba405e6df0ae6f7321..db6a54c752ac39012537ec85ad31d5c8b886be1a 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -1302,6 +1302,37 @@ public class ServerLevel extends Level implements WorldGenLevel { + return !this.server.isUnderSpawnProtection(this, pos, player) && this.getWorldBorder().isWithinBounds(pos); + } + ++ // Paper start - Incremental chunk and player saving ++ public void saveIncrementally(boolean doFull) { ++ ServerChunkCache chunkproviderserver = this.getChunkSource(); ++ ++ if (doFull) { ++ org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(getWorld())); ++ } ++ ++ try (co.aikar.timings.Timing ignored = this.timings.worldSave.startTiming()) { ++ if (doFull) { ++ this.saveLevelData(); ++ } ++ ++ this.timings.worldSaveChunks.startTiming(); // Paper ++ if (!this.noSave()) chunkproviderserver.saveIncrementally(); ++ this.timings.worldSaveChunks.stopTiming(); // Paper ++ ++ // Copied from save() ++ // CraftBukkit start - moved from MinecraftServer.saveChunks ++ if (doFull) { // Paper ++ ServerLevel worldserver1 = this; ++ ++ this.serverLevelData.setWorldBorder(worldserver1.getWorldBorder().createSettings()); ++ this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save(this.registryAccess())); ++ this.convertable.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData()); ++ } ++ // CraftBukkit end ++ } ++ } ++ // Paper end - Incremental chunk and player saving ++ + public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled) { + // Paper start - rewrite chunk system - add close param + this.save(progressListener, flush, savingDisabled, false); +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index 65e6623c2d1a6cf48729f78948fc48e82057114c..fc8e9cd699bdefb6ddadc6aa30e6459417c73194 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -198,6 +198,7 @@ import org.bukkit.inventory.MainHand; + public class ServerPlayer extends Player { + + private static final Logger LOGGER = LogUtils.getLogger(); ++ public long lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving + private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32; + private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10; + private static final int FLY_STAT_RECORDING_SPEED = 25; +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index c4b4e5f5c9366b241686e881cda34568a57b4877..57164b266d23715c224a85660531220477d4dda8 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -575,6 +575,7 @@ public abstract class PlayerList { + + protected void save(ServerPlayer player) { + if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit ++ player.lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving + this.playerIo.save(player); + ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) player.getStats(); // CraftBukkit + +@@ -1227,10 +1228,22 @@ public abstract class PlayerList { + } + + public void saveAll() { ++ // Paper start - Incremental chunk and player saving ++ this.saveAll(-1); ++ } ++ ++ public void saveAll(int interval) { + io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main + MinecraftTimings.savePlayers.startTiming(); // Paper ++ int numSaved = 0; ++ long now = MinecraftServer.currentTick; + for (int i = 0; i < this.players.size(); ++i) { +- this.save(this.players.get(i)); ++ ServerPlayer entityplayer = this.players.get(i); ++ if (interval == -1 || now - entityplayer.lastSave >= interval) { ++ this.save(entityplayer); ++ if (interval != -1 && ++numSaved >= io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.maxPerTick()) { break; } ++ } ++ // Paper end - Incremental chunk and player saving + } + MinecraftTimings.savePlayers.stopTiming(); // Paper + return null; }); // Paper - ensure main diff --git a/patches/server/0987-Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch b/patches/server/0987-Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch new file mode 100644 index 0000000000..474f3d6165 --- /dev/null +++ b/patches/server/0987-Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch @@ -0,0 +1,99 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Thu, 3 Mar 2016 02:07:55 -0600 +Subject: [PATCH] Optimize isInWorldBounds and getBlockState for inlining + +Hot methods, so reduce # of instructions for the method. + +Move is valid location test to the BlockPosition class so that it can access local variables. + +Replace all calls to the new place to the unnecessary forward. + +Optimize getType and getBlockData to manually inline and optimize the calls + +diff --git a/src/main/java/net/minecraft/core/Vec3i.java b/src/main/java/net/minecraft/core/Vec3i.java +index 02367ef1371dde94ff6c4cd40bd32e800d6ccaaf..7b0fc7135bc107103dcaed6dc0707b1829928fae 100644 +--- a/src/main/java/net/minecraft/core/Vec3i.java ++++ b/src/main/java/net/minecraft/core/Vec3i.java +@@ -28,6 +28,12 @@ public class Vec3i implements Comparable { + ); + } + ++ // Paper start ++ public final boolean isInsideBuildHeightAndWorldBoundsHorizontal(net.minecraft.world.level.LevelHeightAccessor levelHeightAccessor) { ++ return getX() >= -30000000 && getZ() >= -30000000 && getX() < 30000000 && getZ() < 30000000 && !levelHeightAccessor.isOutsideBuildHeight(getY()); ++ } ++ // Paper end ++ + public Vec3i(int x, int y, int z) { + this.x = x; + this.y = y; +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index d3137c9e5cc42ef191ea233b0d37eafeffc6f82c..eb7df68d146f9e841bdf99fa2d85fb4b04e603ce 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -339,7 +339,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + // Paper end + + public boolean isInWorldBounds(BlockPos pos) { +- return !this.isOutsideBuildHeight(pos) && Level.isInWorldBoundsHorizontal(pos); ++ return pos.isInsideBuildHeightAndWorldBoundsHorizontal(this); // Paper - use better/optimized check + } + + public static boolean isInSpawnableBounds(BlockPos pos) { +diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java +index 1f8c72b6c7d8683d67880fa175843c73b3d39b78..383dc47c81b3f34e8166bce180a51a2ccbfaf6ca 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java +@@ -175,6 +175,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom + return GameEventListenerRegistry.NOOP; + } + ++ public abstract BlockState getBlockState(final int x, final int y, final int z); // Paper + @Nullable + public abstract BlockState setBlockState(BlockPos pos, BlockState state, boolean moved); + +diff --git a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java +index aa5dee839d4c0dbc3c2abee9b501ec250c575cb3..6359d2d80f4c1360f8450bf6555ccfea3b69daa5 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java +@@ -99,6 +99,12 @@ public class ImposterProtoChunk extends ProtoChunk { + public BlockState getBlockState(BlockPos pos) { + return this.wrapped.getBlockState(pos); + } ++ // Paper start ++ @Override ++ public final BlockState getBlockState(final int x, final int y, final int z) { ++ return this.wrapped.getBlockStateFinal(x, y, z); ++ } ++ // Paper end + + @Override + public FluidState getFluidState(BlockPos pos) { +diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java +index 576ae0cb138b265c8a3995de7b5ebc827d50949d..bfdee41d3095a869408abc89a6085556ff188d8d 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java +@@ -97,14 +97,18 @@ public class ProtoChunk extends ChunkAccess { + + @Override + public BlockState getBlockState(BlockPos pos) { +- int i = pos.getY(); +- if (this.isOutsideBuildHeight(i)) { ++ // Paper start ++ return getBlockState(pos.getX(), pos.getY(), pos.getZ()); ++ } ++ public BlockState getBlockState(final int x, final int y, final int z) { ++ if (this.isOutsideBuildHeight(y)) { + return Blocks.VOID_AIR.defaultBlockState(); + } else { +- LevelChunkSection levelChunkSection = this.getSection(this.getSectionIndex(i)); +- return levelChunkSection.hasOnlyAir() ? Blocks.AIR.defaultBlockState() : levelChunkSection.getBlockState(pos.getX() & 15, i & 15, pos.getZ() & 15); ++ LevelChunkSection levelChunkSection = this.getSections()[this.getSectionIndex(y)]; ++ return levelChunkSection.hasOnlyAir() ? Blocks.AIR.defaultBlockState() : levelChunkSection.getBlockState(x & 15, y & 15, z & 15); + } + } ++ // Paper end + + @Override + public FluidState getFluidState(BlockPos pos) { diff --git a/patches/server/0987-incremental-chunk-and-player-saving.patch b/patches/server/0987-incremental-chunk-and-player-saving.patch deleted file mode 100644 index 22e1d98765..0000000000 --- a/patches/server/0987-incremental-chunk-and-player-saving.patch +++ /dev/null @@ -1,167 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Shane Freeder -Date: Sun, 9 Jun 2019 03:53:22 +0100 -Subject: [PATCH] incremental chunk and player saving - - -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 40708dfd84dc21bc5e047d9fb3b092cb854a8041..7f3359745089584448b8f32f0570cf86b73076c7 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -916,7 +916,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0 && this.ticksUntilAutosave <= 0) { -- this.ticksUntilAutosave = this.autosavePeriod; -- // CraftBukkit end -- MinecraftServer.LOGGER.debug("Autosave started"); -- this.profiler.push("save"); -- this.saveEverything(true, false, false); -- this.profiler.pop(); -- MinecraftServer.LOGGER.debug("Autosave finished"); -+ // Paper start - Incremental chunk and player saving -+ int playerSaveInterval = io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.rate; -+ if (playerSaveInterval < 0) { -+ playerSaveInterval = autosavePeriod; - } -+ this.profiler.push("save"); -+ final boolean fullSave = autosavePeriod > 0 && this.tickCount % autosavePeriod == 0; -+ try { -+ this.isSaving = true; -+ if (playerSaveInterval > 0) { -+ this.playerList.saveAll(playerSaveInterval); -+ } -+ for (ServerLevel level : this.getAllLevels()) { -+ if (level.paperConfig().chunks.autoSaveInterval.value() > 0) { -+ level.saveIncrementally(fullSave); -+ } -+ } -+ } finally { -+ this.isSaving = false; -+ } -+ this.profiler.pop(); -+ // Paper end - Incremental chunk and player saving - io.papermc.paper.util.CachedLists.reset(); // Paper - // Paper start - move executeAll() into full server tick timing - try (co.aikar.timings.Timing ignored = MinecraftTimings.processTasksTimer.startTiming()) { -diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -index f74efe41cd0da2f9749fc96fb9e0f7cf237ad1c6..d1728e13a7b649f308bde90ab633c79d86c10822 100644 ---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java -+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -@@ -430,6 +430,15 @@ public class ServerChunkCache extends ChunkSource { - } // Paper - Timings - } - -+ // Paper start - Incremental chunk and player saving; duplicate save, but call incremental -+ public void saveIncrementally() { -+ this.runDistanceManagerUpdates(); -+ try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings -+ this.chunkMap.saveIncrementally(); -+ } // Paper - Timings -+ } -+ // Paper end - Incremental chunk and player saving -+ - @Override - public void close() throws IOException { - // CraftBukkit start -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 7f8793fa8260cb8a1f55d0ba405e6df0ae6f7321..db6a54c752ac39012537ec85ad31d5c8b886be1a 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -1302,6 +1302,37 @@ public class ServerLevel extends Level implements WorldGenLevel { - return !this.server.isUnderSpawnProtection(this, pos, player) && this.getWorldBorder().isWithinBounds(pos); - } - -+ // Paper start - Incremental chunk and player saving -+ public void saveIncrementally(boolean doFull) { -+ ServerChunkCache chunkproviderserver = this.getChunkSource(); -+ -+ if (doFull) { -+ org.bukkit.Bukkit.getPluginManager().callEvent(new org.bukkit.event.world.WorldSaveEvent(getWorld())); -+ } -+ -+ try (co.aikar.timings.Timing ignored = this.timings.worldSave.startTiming()) { -+ if (doFull) { -+ this.saveLevelData(); -+ } -+ -+ this.timings.worldSaveChunks.startTiming(); // Paper -+ if (!this.noSave()) chunkproviderserver.saveIncrementally(); -+ this.timings.worldSaveChunks.stopTiming(); // Paper -+ -+ // Copied from save() -+ // CraftBukkit start - moved from MinecraftServer.saveChunks -+ if (doFull) { // Paper -+ ServerLevel worldserver1 = this; -+ -+ this.serverLevelData.setWorldBorder(worldserver1.getWorldBorder().createSettings()); -+ this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save(this.registryAccess())); -+ this.convertable.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData()); -+ } -+ // CraftBukkit end -+ } -+ } -+ // Paper end - Incremental chunk and player saving -+ - public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled) { - // Paper start - rewrite chunk system - add close param - this.save(progressListener, flush, savingDisabled, false); -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index 65e6623c2d1a6cf48729f78948fc48e82057114c..fc8e9cd699bdefb6ddadc6aa30e6459417c73194 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -198,6 +198,7 @@ import org.bukkit.inventory.MainHand; - public class ServerPlayer extends Player { - - private static final Logger LOGGER = LogUtils.getLogger(); -+ public long lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving - private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32; - private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10; - private static final int FLY_STAT_RECORDING_SPEED = 25; -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index c4b4e5f5c9366b241686e881cda34568a57b4877..57164b266d23715c224a85660531220477d4dda8 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -575,6 +575,7 @@ public abstract class PlayerList { - - protected void save(ServerPlayer player) { - if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit -+ player.lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving - this.playerIo.save(player); - ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) player.getStats(); // CraftBukkit - -@@ -1227,10 +1228,22 @@ public abstract class PlayerList { - } - - public void saveAll() { -+ // Paper start - Incremental chunk and player saving -+ this.saveAll(-1); -+ } -+ -+ public void saveAll(int interval) { - io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main - MinecraftTimings.savePlayers.startTiming(); // Paper -+ int numSaved = 0; -+ long now = MinecraftServer.currentTick; - for (int i = 0; i < this.players.size(); ++i) { -- this.save(this.players.get(i)); -+ ServerPlayer entityplayer = this.players.get(i); -+ if (interval == -1 || now - entityplayer.lastSave >= interval) { -+ this.save(entityplayer); -+ if (interval != -1 && ++numSaved >= io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.maxPerTick()) { break; } -+ } -+ // Paper end - Incremental chunk and player saving - } - MinecraftTimings.savePlayers.stopTiming(); // Paper - return null; }); // Paper - ensure main diff --git a/patches/server/0988-Improve-Maps-in-item-frames-performance-and-bug-fixe.patch b/patches/server/0988-Improve-Maps-in-item-frames-performance-and-bug-fixe.patch new file mode 100644 index 0000000000..c004efe2f5 --- /dev/null +++ b/patches/server/0988-Improve-Maps-in-item-frames-performance-and-bug-fixe.patch @@ -0,0 +1,128 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Fri, 29 Apr 2016 20:02:00 -0400 +Subject: [PATCH] Improve Maps (in item frames) performance and bug fixes + +Maps used a modified version of rendering to support plugin controlled +imaging on maps. The Craft Map Renderer is much slower than Vanilla, +causing maps in item frames to cause a noticeable hit on server performance. + +This updates the map system to not use the Craft system if we detect that no +custom renderers are in use, defaulting to the much simpler Vanilla system. + +Additionally, numerous issues to player position tracking on maps has been fixed. + +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index db6a54c752ac39012537ec85ad31d5c8b886be1a..5285cc76b001a8b5fd5e11764a3aa11e45c9d999 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -2588,6 +2588,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + { + if ( iter.next().player == entity ) + { ++ map.decorations.remove(entity.getName().getString()); // Paper + iter.remove(); + } + } +diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java +index 70d225dbf5649e6a3fa658291de734f54a6f73dc..72b3e64cf6d184e90a6e2c0d822b83312312d67d 100644 +--- a/src/main/java/net/minecraft/world/entity/player/Player.java ++++ b/src/main/java/net/minecraft/world/entity/player/Player.java +@@ -789,6 +789,14 @@ public abstract class Player extends LivingEntity { + return null; + } + // CraftBukkit end ++ // Paper start - remove player from map on drop ++ if (itemstack.getItem() == Items.FILLED_MAP) { ++ net.minecraft.world.level.saveddata.maps.MapItemSavedData worldmap = net.minecraft.world.item.MapItem.getSavedData(itemstack, this.level()); ++ if (worldmap != null) { ++ worldmap.tickCarriedBy(this, itemstack); ++ } ++ } ++ // Paper end + + return entityitem; + } +diff --git a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +index ad24a79e190d07c75d8e29e816fc398894771c2c..cf8ae635fce7ea66d4e1ab1dc05575f035fa95ef 100644 +--- a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java ++++ b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +@@ -78,6 +78,7 @@ public class MapItemSavedData extends SavedData { + public final Map decorations = Maps.newLinkedHashMap(); + private final Map frameMarkers = Maps.newHashMap(); + private int trackedDecorationCount; ++ private org.bukkit.craftbukkit.map.RenderData vanillaRender = new org.bukkit.craftbukkit.map.RenderData(); // Paper + + // CraftBukkit start + public final CraftMapView mapView; +@@ -104,6 +105,7 @@ public class MapItemSavedData extends SavedData { + // CraftBukkit start + this.mapView = new CraftMapView(this); + this.server = (CraftServer) org.bukkit.Bukkit.getServer(); ++ this.vanillaRender.buffer = colors; // Paper + // CraftBukkit end + } + +@@ -178,6 +180,7 @@ public class MapItemSavedData extends SavedData { + if (abyte.length == 16384) { + worldmap.colors = abyte; + } ++ worldmap.vanillaRender.buffer = abyte; // Paper + + RegistryOps registryops = registryLookup.createSerializationContext(NbtOps.INSTANCE); + List list = (List) MapBanner.LIST_CODEC.parse(registryops, nbt.get("banners")).resultOrPartial((s) -> { +@@ -583,6 +586,21 @@ public class MapItemSavedData extends SavedData { + + public class HoldingPlayer { + ++ // Paper start ++ private void addSeenPlayers(java.util.Collection icons) { ++ org.bukkit.entity.Player player = (org.bukkit.entity.Player) this.player.getBukkitEntity(); ++ MapItemSavedData.this.decorations.forEach((name, mapIcon) -> { ++ // If this cursor is for a player check visibility with vanish system ++ org.bukkit.entity.Player other = org.bukkit.Bukkit.getPlayerExact(name); // Spigot ++ if (other == null || player.canSee(other)) { ++ icons.add(mapIcon); ++ } ++ }); ++ } ++ private boolean shouldUseVanillaMap() { ++ return mapView.getRenderers().size() == 1 && mapView.getRenderers().get(0).getClass() == org.bukkit.craftbukkit.map.CraftMapRenderer.class; ++ } ++ // Paper end + public final Player player; + private boolean dirtyData = true; + private int minDirtyX; +@@ -616,7 +634,9 @@ public class MapItemSavedData extends SavedData { + @Nullable + Packet nextUpdatePacket(MapId mapId) { + MapItemSavedData.MapPatch worldmap_b; +- org.bukkit.craftbukkit.map.RenderData render = MapItemSavedData.this.mapView.render((org.bukkit.craftbukkit.entity.CraftPlayer) this.player.getBukkitEntity()); // CraftBukkit ++ if (!this.dirtyData && this.tick % 5 != 0) { this.tick++; return null; } // Paper - this won't end up sending, so don't render it! ++ boolean vanillaMaps = shouldUseVanillaMap(); // Paper ++ org.bukkit.craftbukkit.map.RenderData render = !vanillaMaps ? MapItemSavedData.this.mapView.render((org.bukkit.craftbukkit.entity.CraftPlayer) this.player.getBukkitEntity()) : MapItemSavedData.this.vanillaRender; // CraftBukkit // Paper + + if (this.dirtyData) { + this.dirtyData = false; +@@ -632,6 +652,8 @@ public class MapItemSavedData extends SavedData { + // CraftBukkit start + java.util.Collection icons = new java.util.ArrayList(); + ++ if (vanillaMaps) addSeenPlayers(icons); // Paper ++ + for (org.bukkit.map.MapCursor cursor : render.cursors) { + if (cursor.isVisible()) { + icons.add(new MapDecoration(CraftMapCursor.CraftType.bukkitToMinecraftHolder(cursor.getType()), cursor.getX(), cursor.getY(), cursor.getDirection(), Optional.ofNullable(PaperAdventure.asVanilla(cursor.caption())))); +diff --git a/src/main/java/org/bukkit/craftbukkit/map/RenderData.java b/src/main/java/org/bukkit/craftbukkit/map/RenderData.java +index 256a131781721c86dd6cdbc329335964570cbe8c..5768cd512ec166f1e8d1f4a28792015347297c3f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/map/RenderData.java ++++ b/src/main/java/org/bukkit/craftbukkit/map/RenderData.java +@@ -5,7 +5,7 @@ import org.bukkit.map.MapCursor; + + public class RenderData { + +- public final byte[] buffer; ++ public byte[] buffer; // Paper + public final ArrayList cursors; + + public RenderData() { diff --git a/patches/server/0988-Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch b/patches/server/0988-Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch deleted file mode 100644 index 474f3d6165..0000000000 --- a/patches/server/0988-Optimize-isInWorldBounds-and-getBlockState-for-inlin.patch +++ /dev/null @@ -1,99 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Thu, 3 Mar 2016 02:07:55 -0600 -Subject: [PATCH] Optimize isInWorldBounds and getBlockState for inlining - -Hot methods, so reduce # of instructions for the method. - -Move is valid location test to the BlockPosition class so that it can access local variables. - -Replace all calls to the new place to the unnecessary forward. - -Optimize getType and getBlockData to manually inline and optimize the calls - -diff --git a/src/main/java/net/minecraft/core/Vec3i.java b/src/main/java/net/minecraft/core/Vec3i.java -index 02367ef1371dde94ff6c4cd40bd32e800d6ccaaf..7b0fc7135bc107103dcaed6dc0707b1829928fae 100644 ---- a/src/main/java/net/minecraft/core/Vec3i.java -+++ b/src/main/java/net/minecraft/core/Vec3i.java -@@ -28,6 +28,12 @@ public class Vec3i implements Comparable { - ); - } - -+ // Paper start -+ public final boolean isInsideBuildHeightAndWorldBoundsHorizontal(net.minecraft.world.level.LevelHeightAccessor levelHeightAccessor) { -+ return getX() >= -30000000 && getZ() >= -30000000 && getX() < 30000000 && getZ() < 30000000 && !levelHeightAccessor.isOutsideBuildHeight(getY()); -+ } -+ // Paper end -+ - public Vec3i(int x, int y, int z) { - this.x = x; - this.y = y; -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index d3137c9e5cc42ef191ea233b0d37eafeffc6f82c..eb7df68d146f9e841bdf99fa2d85fb4b04e603ce 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -339,7 +339,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - // Paper end - - public boolean isInWorldBounds(BlockPos pos) { -- return !this.isOutsideBuildHeight(pos) && Level.isInWorldBoundsHorizontal(pos); -+ return pos.isInsideBuildHeightAndWorldBoundsHorizontal(this); // Paper - use better/optimized check - } - - public static boolean isInSpawnableBounds(BlockPos pos) { -diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java -index 1f8c72b6c7d8683d67880fa175843c73b3d39b78..383dc47c81b3f34e8166bce180a51a2ccbfaf6ca 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java -+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java -@@ -175,6 +175,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom - return GameEventListenerRegistry.NOOP; - } - -+ public abstract BlockState getBlockState(final int x, final int y, final int z); // Paper - @Nullable - public abstract BlockState setBlockState(BlockPos pos, BlockState state, boolean moved); - -diff --git a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java -index aa5dee839d4c0dbc3c2abee9b501ec250c575cb3..6359d2d80f4c1360f8450bf6555ccfea3b69daa5 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java -@@ -99,6 +99,12 @@ public class ImposterProtoChunk extends ProtoChunk { - public BlockState getBlockState(BlockPos pos) { - return this.wrapped.getBlockState(pos); - } -+ // Paper start -+ @Override -+ public final BlockState getBlockState(final int x, final int y, final int z) { -+ return this.wrapped.getBlockStateFinal(x, y, z); -+ } -+ // Paper end - - @Override - public FluidState getFluidState(BlockPos pos) { -diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java -index 576ae0cb138b265c8a3995de7b5ebc827d50949d..bfdee41d3095a869408abc89a6085556ff188d8d 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java -@@ -97,14 +97,18 @@ public class ProtoChunk extends ChunkAccess { - - @Override - public BlockState getBlockState(BlockPos pos) { -- int i = pos.getY(); -- if (this.isOutsideBuildHeight(i)) { -+ // Paper start -+ return getBlockState(pos.getX(), pos.getY(), pos.getZ()); -+ } -+ public BlockState getBlockState(final int x, final int y, final int z) { -+ if (this.isOutsideBuildHeight(y)) { - return Blocks.VOID_AIR.defaultBlockState(); - } else { -- LevelChunkSection levelChunkSection = this.getSection(this.getSectionIndex(i)); -- return levelChunkSection.hasOnlyAir() ? Blocks.AIR.defaultBlockState() : levelChunkSection.getBlockState(pos.getX() & 15, i & 15, pos.getZ() & 15); -+ LevelChunkSection levelChunkSection = this.getSections()[this.getSectionIndex(y)]; -+ return levelChunkSection.hasOnlyAir() ? Blocks.AIR.defaultBlockState() : levelChunkSection.getBlockState(x & 15, y & 15, z & 15); - } - } -+ // Paper end - - @Override - public FluidState getFluidState(BlockPos pos) { diff --git a/patches/server/0989-Improve-Maps-in-item-frames-performance-and-bug-fixe.patch b/patches/server/0989-Improve-Maps-in-item-frames-performance-and-bug-fixe.patch deleted file mode 100644 index c004efe2f5..0000000000 --- a/patches/server/0989-Improve-Maps-in-item-frames-performance-and-bug-fixe.patch +++ /dev/null @@ -1,128 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Fri, 29 Apr 2016 20:02:00 -0400 -Subject: [PATCH] Improve Maps (in item frames) performance and bug fixes - -Maps used a modified version of rendering to support plugin controlled -imaging on maps. The Craft Map Renderer is much slower than Vanilla, -causing maps in item frames to cause a noticeable hit on server performance. - -This updates the map system to not use the Craft system if we detect that no -custom renderers are in use, defaulting to the much simpler Vanilla system. - -Additionally, numerous issues to player position tracking on maps has been fixed. - -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index db6a54c752ac39012537ec85ad31d5c8b886be1a..5285cc76b001a8b5fd5e11764a3aa11e45c9d999 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -2588,6 +2588,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - { - if ( iter.next().player == entity ) - { -+ map.decorations.remove(entity.getName().getString()); // Paper - iter.remove(); - } - } -diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java -index 70d225dbf5649e6a3fa658291de734f54a6f73dc..72b3e64cf6d184e90a6e2c0d822b83312312d67d 100644 ---- a/src/main/java/net/minecraft/world/entity/player/Player.java -+++ b/src/main/java/net/minecraft/world/entity/player/Player.java -@@ -789,6 +789,14 @@ public abstract class Player extends LivingEntity { - return null; - } - // CraftBukkit end -+ // Paper start - remove player from map on drop -+ if (itemstack.getItem() == Items.FILLED_MAP) { -+ net.minecraft.world.level.saveddata.maps.MapItemSavedData worldmap = net.minecraft.world.item.MapItem.getSavedData(itemstack, this.level()); -+ if (worldmap != null) { -+ worldmap.tickCarriedBy(this, itemstack); -+ } -+ } -+ // Paper end - - return entityitem; - } -diff --git a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java -index ad24a79e190d07c75d8e29e816fc398894771c2c..cf8ae635fce7ea66d4e1ab1dc05575f035fa95ef 100644 ---- a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java -+++ b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java -@@ -78,6 +78,7 @@ public class MapItemSavedData extends SavedData { - public final Map decorations = Maps.newLinkedHashMap(); - private final Map frameMarkers = Maps.newHashMap(); - private int trackedDecorationCount; -+ private org.bukkit.craftbukkit.map.RenderData vanillaRender = new org.bukkit.craftbukkit.map.RenderData(); // Paper - - // CraftBukkit start - public final CraftMapView mapView; -@@ -104,6 +105,7 @@ public class MapItemSavedData extends SavedData { - // CraftBukkit start - this.mapView = new CraftMapView(this); - this.server = (CraftServer) org.bukkit.Bukkit.getServer(); -+ this.vanillaRender.buffer = colors; // Paper - // CraftBukkit end - } - -@@ -178,6 +180,7 @@ public class MapItemSavedData extends SavedData { - if (abyte.length == 16384) { - worldmap.colors = abyte; - } -+ worldmap.vanillaRender.buffer = abyte; // Paper - - RegistryOps registryops = registryLookup.createSerializationContext(NbtOps.INSTANCE); - List list = (List) MapBanner.LIST_CODEC.parse(registryops, nbt.get("banners")).resultOrPartial((s) -> { -@@ -583,6 +586,21 @@ public class MapItemSavedData extends SavedData { - - public class HoldingPlayer { - -+ // Paper start -+ private void addSeenPlayers(java.util.Collection icons) { -+ org.bukkit.entity.Player player = (org.bukkit.entity.Player) this.player.getBukkitEntity(); -+ MapItemSavedData.this.decorations.forEach((name, mapIcon) -> { -+ // If this cursor is for a player check visibility with vanish system -+ org.bukkit.entity.Player other = org.bukkit.Bukkit.getPlayerExact(name); // Spigot -+ if (other == null || player.canSee(other)) { -+ icons.add(mapIcon); -+ } -+ }); -+ } -+ private boolean shouldUseVanillaMap() { -+ return mapView.getRenderers().size() == 1 && mapView.getRenderers().get(0).getClass() == org.bukkit.craftbukkit.map.CraftMapRenderer.class; -+ } -+ // Paper end - public final Player player; - private boolean dirtyData = true; - private int minDirtyX; -@@ -616,7 +634,9 @@ public class MapItemSavedData extends SavedData { - @Nullable - Packet nextUpdatePacket(MapId mapId) { - MapItemSavedData.MapPatch worldmap_b; -- org.bukkit.craftbukkit.map.RenderData render = MapItemSavedData.this.mapView.render((org.bukkit.craftbukkit.entity.CraftPlayer) this.player.getBukkitEntity()); // CraftBukkit -+ if (!this.dirtyData && this.tick % 5 != 0) { this.tick++; return null; } // Paper - this won't end up sending, so don't render it! -+ boolean vanillaMaps = shouldUseVanillaMap(); // Paper -+ org.bukkit.craftbukkit.map.RenderData render = !vanillaMaps ? MapItemSavedData.this.mapView.render((org.bukkit.craftbukkit.entity.CraftPlayer) this.player.getBukkitEntity()) : MapItemSavedData.this.vanillaRender; // CraftBukkit // Paper - - if (this.dirtyData) { - this.dirtyData = false; -@@ -632,6 +652,8 @@ public class MapItemSavedData extends SavedData { - // CraftBukkit start - java.util.Collection icons = new java.util.ArrayList(); - -+ if (vanillaMaps) addSeenPlayers(icons); // Paper -+ - for (org.bukkit.map.MapCursor cursor : render.cursors) { - if (cursor.isVisible()) { - icons.add(new MapDecoration(CraftMapCursor.CraftType.bukkitToMinecraftHolder(cursor.getType()), cursor.getX(), cursor.getY(), cursor.getDirection(), Optional.ofNullable(PaperAdventure.asVanilla(cursor.caption())))); -diff --git a/src/main/java/org/bukkit/craftbukkit/map/RenderData.java b/src/main/java/org/bukkit/craftbukkit/map/RenderData.java -index 256a131781721c86dd6cdbc329335964570cbe8c..5768cd512ec166f1e8d1f4a28792015347297c3f 100644 ---- a/src/main/java/org/bukkit/craftbukkit/map/RenderData.java -+++ b/src/main/java/org/bukkit/craftbukkit/map/RenderData.java -@@ -5,7 +5,7 @@ import org.bukkit.map.MapCursor; - - public class RenderData { - -- public final byte[] buffer; -+ public byte[] buffer; // Paper - public final ArrayList cursors; - - public RenderData() { diff --git a/patches/server/0989-Strip-raytracing-for-EntityLiving-hasLineOfSight.patch b/patches/server/0989-Strip-raytracing-for-EntityLiving-hasLineOfSight.patch new file mode 100644 index 0000000000..1a86ce02de --- /dev/null +++ b/patches/server/0989-Strip-raytracing-for-EntityLiving-hasLineOfSight.patch @@ -0,0 +1,158 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Paul Sauve +Date: Sat, 31 Oct 2020 18:43:02 -0500 +Subject: [PATCH] Strip raytracing for EntityLiving#hasLineOfSight + +The BlockGetter#clip method is very wasteful in both allocations, +and in logic. While EntityLiving#hasLineOfSight provides static +parameters for collisions with blocks and fluids, the method still does +a lot of dynamic checks for both of these, which result in extra work. +As well, since the fluid collision option is set to NONE, the entire +fluid collision system is completely unneeded, yet used anyways. + +Copyright (C) 2020 Technove LLC + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index 0416af1342dad6a6789aa743aadf61b32b70f338..5854e58a014b5581fa065d1db256ffc0aa947ce2 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -3730,7 +3730,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + Vec3 vec3d1 = new Vec3(entity.getX(), entity.getEyeY(), entity.getZ()); + + // Paper - diff on change - used in CraftLivingEntity#hasLineOfSight(Location) and CraftWorld#lineOfSightExists +- return vec3d1.distanceToSqr(vec3d) > 128.0D * 128.0D ? false : this.level().clip(new ClipContext(vec3d, vec3d1, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)).getType() == HitResult.Type.MISS; // Paper - Perf: Use distance squared ++ return vec3d1.distanceToSqr(vec3d) > 128.0D * 128.0D ? false : this.level().clipDirect(vec3d, vec3d1, net.minecraft.world.phys.shapes.CollisionContext.of(this)) == HitResult.Type.MISS; // Paper - Perf: Use distance squared & strip raytracing + } + } + +diff --git a/src/main/java/net/minecraft/world/level/BlockGetter.java b/src/main/java/net/minecraft/world/level/BlockGetter.java +index bb8e962e63c7a2d931f9bd7f7c002aa35cfa5fd3..0fa131a6c98adb498fc8d534e0e39647e80c6923 100644 +--- a/src/main/java/net/minecraft/world/level/BlockGetter.java ++++ b/src/main/java/net/minecraft/world/level/BlockGetter.java +@@ -68,6 +68,18 @@ public interface BlockGetter extends LevelHeightAccessor { + }); + } + ++ // Paper start - Broken down variant of the method below, used by Level#clipDirect ++ @Nullable ++ default BlockHitResult.Type clipDirect(Vec3 start, Vec3 end, BlockPos pos, BlockState state, net.minecraft.world.phys.shapes.CollisionContext collisionContext) { ++ if (state.isAir()) { ++ return null; ++ } ++ ++ final VoxelShape voxelshape = ClipContext.Block.COLLIDER.get(state, this, pos, collisionContext); ++ final BlockHitResult hitResult = this.clipWithInteractionOverride(start, end, pos, voxelshape, state); ++ return hitResult == null ? null : hitResult.getType(); ++ } ++ // Paper end + // CraftBukkit start - moved block handling into separate method for use by Block#rayTrace + default BlockHitResult clip(ClipContext raytrace1, BlockPos blockposition) { + // Paper start - Add predicate for blocks when raytracing +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index eb7df68d146f9e841bdf99fa2d85fb4b04e603ce..12ab7097562d49dc466737378046707dc1b36b3b 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -331,10 +331,87 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + return null; + } + +- // Paper start ++ // Paper start - Broken down method of raytracing for EntityLiving#hasLineOfSight, replaces BlockGetter#clip(CollisionContext) + public net.minecraft.world.phys.BlockHitResult.Type clipDirect(Vec3 start, Vec3 end, net.minecraft.world.phys.shapes.CollisionContext context) { +- // To be patched over +- return this.clip(new ClipContext(start, end, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, context)).getType(); ++ // most of this code comes from BlockGetter#clip(CollisionContext, BiFunction, Function), but removes the needless functions ++ if (start.equals(end)) { ++ return net.minecraft.world.phys.BlockHitResult.Type.MISS; ++ } ++ ++ final double endX = Mth.lerp(-1.0E-7D, end.x, start.x); ++ final double endY = Mth.lerp(-1.0E-7D, end.y, start.y); ++ final double endZ = Mth.lerp(-1.0E-7D, end.z, start.z); ++ ++ final double startX = Mth.lerp(-1.0E-7D, start.x, end.x); ++ final double startY = Mth.lerp(-1.0E-7D, start.y, end.y); ++ final double startZ = Mth.lerp(-1.0E-7D, start.z, end.z); ++ ++ int currentX = Mth.floor(startX); ++ int currentY = Mth.floor(startY); ++ int currentZ = Mth.floor(startZ); ++ ++ final BlockPos.MutableBlockPos currentBlock = new BlockPos.MutableBlockPos(currentX, currentY, currentZ); ++ ++ LevelChunk chunk = this.getChunkIfLoaded(currentBlock); ++ if (chunk == null) { ++ return net.minecraft.world.phys.BlockHitResult.Type.MISS; ++ } ++ ++ final net.minecraft.world.phys.BlockHitResult.Type initialCheck = this.clipDirect(start, end, currentBlock, chunk.getBlockState(currentBlock), context); ++ if (initialCheck != null) { ++ return initialCheck; ++ } ++ ++ final double diffX = endX - startX; ++ final double diffY = endY - startY; ++ final double diffZ = endZ - startZ; ++ ++ final int xDirection = Mth.sign(diffX); ++ final int yDirection = Mth.sign(diffY); ++ final int zDirection = Mth.sign(diffZ); ++ ++ final double normalizedX = xDirection == 0 ? Double.MAX_VALUE : (double) xDirection / diffX; ++ final double normalizedY = yDirection == 0 ? Double.MAX_VALUE : (double) yDirection / diffY; ++ final double normalizedZ = zDirection == 0 ? Double.MAX_VALUE : (double) zDirection / diffZ; ++ ++ double normalizedXDirection = normalizedX * (xDirection > 0 ? 1.0D - Mth.frac(startX) : Mth.frac(startX)); ++ double normalizedYDirection = normalizedY * (yDirection > 0 ? 1.0D - Mth.frac(startY) : Mth.frac(startY)); ++ double normalizedZDirection = normalizedZ * (zDirection > 0 ? 1.0D - Mth.frac(startZ) : Mth.frac(startZ)); ++ ++ net.minecraft.world.phys.BlockHitResult.Type result; ++ ++ do { ++ if (normalizedXDirection > 1.0D && normalizedYDirection > 1.0D && normalizedZDirection > 1.0D) { ++ return net.minecraft.world.phys.BlockHitResult.Type.MISS; ++ } ++ ++ if (normalizedXDirection < normalizedYDirection) { ++ if (normalizedXDirection < normalizedZDirection) { ++ currentX += xDirection; ++ normalizedXDirection += normalizedX; ++ } else { ++ currentZ += zDirection; ++ normalizedZDirection += normalizedZ; ++ } ++ } else if (normalizedYDirection < normalizedZDirection) { ++ currentY += yDirection; ++ normalizedYDirection += normalizedY; ++ } else { ++ currentZ += zDirection; ++ normalizedZDirection += normalizedZ; ++ } ++ ++ currentBlock.set(currentX, currentY, currentZ); ++ if (chunk.getPos().x != currentBlock.getX() >> 4 || chunk.getPos().z != currentBlock.getZ() >> 4) { ++ chunk = this.getChunkIfLoaded(currentBlock); ++ if (chunk == null) { ++ return net.minecraft.world.phys.BlockHitResult.Type.MISS; ++ } ++ } ++ result = this.clipDirect(start, end, currentBlock, chunk.getBlockState(currentBlock), context); ++ } while (result == null); ++ ++ return result; + } + // Paper end + diff --git a/patches/server/0990-Optimize-Network-Manager-and-add-advanced-packet-sup.patch b/patches/server/0990-Optimize-Network-Manager-and-add-advanced-packet-sup.patch new file mode 100644 index 0000000000..84396ba635 --- /dev/null +++ b/patches/server/0990-Optimize-Network-Manager-and-add-advanced-packet-sup.patch @@ -0,0 +1,395 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Wed, 6 May 2020 04:53:35 -0400 +Subject: [PATCH] Optimize Network Manager and add advanced packet support + +Adds ability for 1 packet to bundle other packets to follow it +Adds ability for a packet to delay sending more packets until a state is ready. + +Removes synchronization from sending packets +Removes processing packet queue off of main thread + - for the few cases where it is allowed, order is not necessary nor + should it even be happening concurrently in first place (handshaking/login/status) + +Ensures packets sent asynchronously are dispatched on main thread + +This helps ensure safety for ProtocolLib as packet listeners +are commonly accessing world state. This will allow you to schedule +a packet to be sent async, but itll be dispatched sync for packet +listeners to process. + +This should solve some deadlock risks + +Also adds Netty Channel Flush Consolidation to reduce the amount of flushing + +Also avoids spamming closed channel exception by rechecking closed state in dispatch +and then catch exceptions and close if they fire. + +Part of this commit was authored by: Spottedleaf, sandtechnology + +diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java +index f57679d88cd015caa8996d44b486da694df29521..f94458d4270042062b9ae18931cc8179ef1fb420 100644 +--- a/src/main/java/net/minecraft/network/Connection.java ++++ b/src/main/java/net/minecraft/network/Connection.java +@@ -93,7 +93,7 @@ public class Connection extends SimpleChannelInboundHandler> { + private static final ProtocolInfo INITIAL_PROTOCOL = HandshakeProtocols.SERVERBOUND; + private final PacketFlow receiving; + private volatile boolean sendLoginDisconnect = true; +- private final Queue> pendingActions = Queues.newConcurrentLinkedQueue(); ++ private final Queue pendingActions = Queues.newConcurrentLinkedQueue(); // Paper + public Channel channel; + public SocketAddress address; + // Spigot Start +@@ -125,6 +125,10 @@ public class Connection extends SimpleChannelInboundHandler> { + public java.net.InetSocketAddress virtualHost; + private static boolean enableExplicitFlush = Boolean.getBoolean("paper.explicit-flush"); // Paper - Disable explicit network manager flushing + // Paper end ++ // Paper start - Optimize network ++ public boolean isPending = true; ++ public boolean queueImmunity; ++ // Paper end - Optimize network + + // Paper start - add utility methods + public final net.minecraft.server.level.ServerPlayer getPlayer() { +@@ -415,15 +419,39 @@ public class Connection extends SimpleChannelInboundHandler> { + } + + public void send(Packet packet, @Nullable PacketSendListener callbacks, boolean flush) { +- if (this.isConnected()) { +- this.flushQueue(); ++ // Paper start - Optimize network: Handle oversized packets better ++ final boolean connected = this.isConnected(); ++ if (!connected && !this.preparing) { ++ return; ++ } ++ ++ packet.onPacketDispatch(this.getPlayer()); ++ if (connected && (InnerUtil.canSendImmediate(this, packet) ++ || (io.papermc.paper.util.MCUtil.isMainThread() && packet.isReady() && this.pendingActions.isEmpty() ++ && (packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty())))) { + this.sendPacket(packet, callbacks, flush); + } else { +- this.pendingActions.add((networkmanager) -> { +- networkmanager.sendPacket(packet, callbacks, flush); +- }); +- } ++ // Write the packets to the queue, then flush - antixray hooks there already ++ final java.util.List> extraPackets = InnerUtil.buildExtraPackets(packet); ++ final boolean hasExtraPackets = extraPackets != null && !extraPackets.isEmpty(); ++ if (!hasExtraPackets) { ++ this.pendingActions.add(new PacketSendAction(packet, callbacks, flush)); ++ } else { ++ final java.util.List actions = new java.util.ArrayList<>(1 + extraPackets.size()); ++ actions.add(new PacketSendAction(packet, null, false)); // Delay the future listener until the end of the extra packets ++ ++ for (int i = 0, len = extraPackets.size(); i < len;) { ++ final Packet extraPacket = extraPackets.get(i); ++ final boolean end = ++i == len; ++ actions.add(new PacketSendAction(extraPacket, end ? callbacks : null, end)); // Append listener to the end ++ } ++ ++ this.pendingActions.addAll(actions); ++ } + ++ this.flushQueue(); ++ // Paper end - Optimize network ++ } + } + + public void runOnceConnected(Consumer task) { +@@ -431,7 +459,7 @@ public class Connection extends SimpleChannelInboundHandler> { + this.flushQueue(); + task.accept(this); + } else { +- this.pendingActions.add(task); ++ this.pendingActions.add(new WrappedConsumer(task)); // Paper - Optimize network + } + + } +@@ -449,6 +477,14 @@ public class Connection extends SimpleChannelInboundHandler> { + } + + private void doSendPacket(Packet packet, @Nullable PacketSendListener callbacks, boolean flush) { ++ // Paper start - Optimize network ++ final net.minecraft.server.level.ServerPlayer player = this.getPlayer(); ++ if (!this.isConnected()) { ++ packet.onPacketDispatchFinish(player, null); ++ return; ++ } ++ try { ++ // Paper end - Optimize network + ChannelFuture channelfuture = flush ? this.channel.writeAndFlush(packet) : this.channel.write(packet); + + if (callbacks != null) { +@@ -468,14 +504,24 @@ public class Connection extends SimpleChannelInboundHandler> { + }); + } + ++ // Paper start - Optimize network ++ if (packet.hasFinishListener()) { ++ channelfuture.addListener((ChannelFutureListener) channelFuture -> packet.onPacketDispatchFinish(player, channelFuture)); ++ } + channelfuture.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); ++ } catch (final Exception e) { ++ LOGGER.error("NetworkException: {}", player, e); ++ this.disconnect(Component.translatable("disconnect.genericReason", "Internal Exception: " + e.getMessage())); ++ packet.onPacketDispatchFinish(player, null); ++ } ++ // Paper end - Optimize network + } + + public void flushChannel() { + if (this.isConnected()) { + this.flush(); + } else { +- this.pendingActions.add(Connection::flush); ++ this.pendingActions.add(new WrappedConsumer(Connection::flush)); // Paper - Optimize network + } + + } +@@ -491,20 +537,57 @@ public class Connection extends SimpleChannelInboundHandler> { + + } + +- private void flushQueue() { +- if (this.channel != null && this.channel.isOpen()) { +- Queue queue = this.pendingActions; +- ++ // Paper start - Optimize network: Rewrite this to be safer if ran off main thread ++ private boolean flushQueue() { ++ if (!this.isConnected()) { ++ return true; ++ } ++ if (io.papermc.paper.util.MCUtil.isMainThread()) { ++ return this.processQueue(); ++ } else if (this.isPending) { ++ // Should only happen during login/status stages + synchronized (this.pendingActions) { +- Consumer consumer; ++ return this.processQueue(); ++ } ++ } ++ return false; ++ } ++ ++ private boolean processQueue() { ++ if (this.pendingActions.isEmpty()) { ++ return true; ++ } + +- while ((consumer = (Consumer) this.pendingActions.poll()) != null) { +- consumer.accept(this); ++ // 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 ++ final java.util.Iterator iterator = this.pendingActions.iterator(); ++ while (iterator.hasNext()) { ++ final WrappedConsumer queued = iterator.next(); // poll -> peek ++ ++ // Fix NPE (Spigot bug caused by handleDisconnection()) ++ if (queued == null) { ++ return true; ++ } ++ ++ if (queued.isConsumed()) { ++ continue; ++ } ++ ++ if (queued instanceof PacketSendAction packetSendAction) { ++ final Packet packet = packetSendAction.packet; ++ if (!packet.isReady()) { ++ return false; + } ++ } + ++ iterator.remove(); ++ if (queued.tryMarkConsumed()) { ++ queued.accept(this); + } + } ++ return true; + } ++ // Paper end - Optimize network + + private static final int MAX_PER_TICK = io.papermc.paper.configuration.GlobalConfiguration.get().misc.maxJoinsPerTick; // Paper - Buffer joins to world + private static int joinAttemptsThisTick; // Paper - Buffer joins to world +@@ -564,6 +647,7 @@ public class Connection extends SimpleChannelInboundHandler> { + public void disconnect(Component disconnectReason) { + // Spigot Start + this.preparing = false; ++ this.clearPacketQueue(); // Paper - Optimize network + // Spigot End + if (this.channel == null) { + this.delayedDisconnect = disconnectReason; +@@ -751,7 +835,7 @@ public class Connection extends SimpleChannelInboundHandler> { + public void handleDisconnection() { + if (this.channel != null && !this.channel.isOpen()) { + if (this.disconnectionHandled) { +- Connection.LOGGER.warn("handleDisconnection() called twice"); ++ // Connection.LOGGER.warn("handleDisconnection() called twice"); // Paper - Don't log useless message + } else { + this.disconnectionHandled = true; + PacketListener packetlistener = this.getPacketListener(); +@@ -764,7 +848,7 @@ public class Connection extends SimpleChannelInboundHandler> { + + packetlistener1.onDisconnect(ichatbasecomponent); + } +- this.pendingActions.clear(); // Free up packet queue. ++ this.clearPacketQueue(); // Paper - Optimize network + // Paper start - Add PlayerConnectionCloseEvent + final PacketListener packetListener = this.getPacketListener(); + if (packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) { +@@ -801,4 +885,93 @@ public class Connection extends SimpleChannelInboundHandler> { + public void setBandwidthLogger(LocalSampleLogger log) { + this.bandwidthDebugMonitor = new BandwidthDebugMonitor(log); + } ++ ++ // Paper start - Optimize network ++ public void clearPacketQueue() { ++ final net.minecraft.server.level.ServerPlayer player = getPlayer(); ++ for (final Consumer queuedAction : this.pendingActions) { ++ if (queuedAction instanceof PacketSendAction packetSendAction) { ++ final Packet packet = packetSendAction.packet; ++ if (packet.hasFinishListener()) { ++ packet.onPacketDispatchFinish(player, null); ++ } ++ } ++ } ++ this.pendingActions.clear(); ++ } ++ ++ private static class InnerUtil { // Attempt to hide these methods from ProtocolLib, so it doesn't accidently pick them up. ++ ++ @Nullable ++ private static java.util.List> buildExtraPackets(final Packet packet) { ++ final java.util.List> extra = packet.getExtraPackets(); ++ if (extra == null || extra.isEmpty()) { ++ return null; ++ } ++ ++ final java.util.List> ret = new java.util.ArrayList<>(1 + extra.size()); ++ buildExtraPackets0(extra, ret); ++ return ret; ++ } ++ ++ private static void buildExtraPackets0(final java.util.List> extraPackets, final java.util.List> into) { ++ for (final Packet extra : extraPackets) { ++ into.add(extra); ++ final java.util.List> extraExtra = extra.getExtraPackets(); ++ if (extraExtra != null && !extraExtra.isEmpty()) { ++ buildExtraPackets0(extraExtra, into); ++ } ++ } ++ } ++ ++ private static boolean canSendImmediate(final Connection networkManager, final net.minecraft.network.protocol.Packet packet) { ++ return networkManager.isPending || networkManager.packetListener.protocol() != ConnectionProtocol.PLAY || ++ packet instanceof net.minecraft.network.protocol.common.ClientboundKeepAlivePacket || ++ packet instanceof net.minecraft.network.protocol.game.ClientboundPlayerChatPacket || ++ packet instanceof net.minecraft.network.protocol.game.ClientboundSystemChatPacket || ++ packet instanceof net.minecraft.network.protocol.game.ClientboundCommandSuggestionsPacket || ++ packet instanceof net.minecraft.network.protocol.game.ClientboundSetTitleTextPacket || ++ packet instanceof net.minecraft.network.protocol.game.ClientboundSetSubtitleTextPacket || ++ packet instanceof net.minecraft.network.protocol.game.ClientboundSetActionBarTextPacket || ++ packet instanceof net.minecraft.network.protocol.game.ClientboundSetTitlesAnimationPacket || ++ packet instanceof net.minecraft.network.protocol.game.ClientboundClearTitlesPacket || ++ packet instanceof net.minecraft.network.protocol.game.ClientboundSoundPacket || ++ packet instanceof net.minecraft.network.protocol.game.ClientboundSoundEntityPacket || ++ packet instanceof net.minecraft.network.protocol.game.ClientboundStopSoundPacket || ++ packet instanceof net.minecraft.network.protocol.game.ClientboundLevelParticlesPacket || ++ packet instanceof net.minecraft.network.protocol.game.ClientboundBossEventPacket; ++ } ++ } ++ ++ private static class WrappedConsumer implements Consumer { ++ private final Consumer delegate; ++ private final java.util.concurrent.atomic.AtomicBoolean consumed = new java.util.concurrent.atomic.AtomicBoolean(false); ++ ++ private WrappedConsumer(final Consumer delegate) { ++ this.delegate = delegate; ++ } ++ ++ @Override ++ public void accept(final Connection connection) { ++ this.delegate.accept(connection); ++ } ++ ++ public boolean tryMarkConsumed() { ++ return consumed.compareAndSet(false, true); ++ } ++ ++ public boolean isConsumed() { ++ return consumed.get(); ++ } ++ } ++ ++ private static final class PacketSendAction extends WrappedConsumer { ++ private final Packet packet; ++ ++ private PacketSendAction(final Packet packet, @Nullable final PacketSendListener packetSendListener, final boolean flush) { ++ super(connection -> connection.sendPacket(packet, packetSendListener, flush)); ++ this.packet = packet; ++ } ++ } ++ // Paper end - Optimize network + } +diff --git a/src/main/java/net/minecraft/network/protocol/Packet.java b/src/main/java/net/minecraft/network/protocol/Packet.java +index 4c776c591dd0a7b36945a6487fdfe86d1187b4af..5ee2ba1225fb7e4f02152b45adeb66f79ed1650d 100644 +--- a/src/main/java/net/minecraft/network/protocol/Packet.java ++++ b/src/main/java/net/minecraft/network/protocol/Packet.java +@@ -22,4 +22,31 @@ public interface Packet { + static > StreamCodec codec(StreamMemberEncoder encoder, StreamDecoder decoder) { + return StreamCodec.ofMember(encoder, decoder); + } ++ ++ // Paper start ++ /** ++ * @param player Null if not at PLAY stage yet ++ */ ++ default void onPacketDispatch(@org.jetbrains.annotations.Nullable net.minecraft.server.level.ServerPlayer player) { ++ } ++ ++ /** ++ * @param player Null if not at PLAY stage yet ++ * @param future Can be null if packet was cancelled ++ */ ++ default void onPacketDispatchFinish(@org.jetbrains.annotations.Nullable net.minecraft.server.level.ServerPlayer player, @org.jetbrains.annotations.Nullable io.netty.channel.ChannelFuture future) {} ++ ++ default boolean hasFinishListener() { ++ return false; ++ } ++ ++ default boolean isReady() { ++ return true; ++ } ++ ++ @org.jetbrains.annotations.Nullable ++ default java.util.List> getExtraPackets() { ++ return null; ++ } ++ // Paper end + } +diff --git a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java +index 96355e1da8feb6687ea0069dda4a82fcd7e25e8a..a08d9aa6e420f691795df9b627a9cd5b5c0112c5 100644 +--- a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java ++++ b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java +@@ -63,10 +63,12 @@ public class ServerConnectionListener { + final List connections = Collections.synchronizedList(Lists.newArrayList()); + // Paper start - prevent blocking on adding a new connection while the server is ticking + private final java.util.Queue pending = new java.util.concurrent.ConcurrentLinkedQueue<>(); ++ private static final boolean disableFlushConsolidation = Boolean.getBoolean("Paper.disableFlushConsolidate"); // Paper - Optimize network + private final void addPending() { + Connection connection; + while ((connection = pending.poll()) != null) { + connections.add(connection); ++ connection.isPending = false; // Paper - Optimize network + } + } + // Paper end - prevent blocking on adding a new connection while the server is ticking +@@ -112,6 +114,7 @@ public class ServerConnectionListener { + ; + } + ++ if (!disableFlushConsolidation) channel.pipeline().addFirst(new io.netty.handler.flush.FlushConsolidationHandler()); // Paper - Optimize network + ChannelPipeline channelpipeline = channel.pipeline().addLast("timeout", new ReadTimeoutHandler(30)); + + if (ServerConnectionListener.this.server.repliesToStatus()) { diff --git a/patches/server/0990-Strip-raytracing-for-EntityLiving-hasLineOfSight.patch b/patches/server/0990-Strip-raytracing-for-EntityLiving-hasLineOfSight.patch deleted file mode 100644 index 1a86ce02de..0000000000 --- a/patches/server/0990-Strip-raytracing-for-EntityLiving-hasLineOfSight.patch +++ /dev/null @@ -1,158 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Paul Sauve -Date: Sat, 31 Oct 2020 18:43:02 -0500 -Subject: [PATCH] Strip raytracing for EntityLiving#hasLineOfSight - -The BlockGetter#clip method is very wasteful in both allocations, -and in logic. While EntityLiving#hasLineOfSight provides static -parameters for collisions with blocks and fluids, the method still does -a lot of dynamic checks for both of these, which result in extra work. -As well, since the fluid collision option is set to NONE, the entire -fluid collision system is completely unneeded, yet used anyways. - -Copyright (C) 2020 Technove LLC - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index 0416af1342dad6a6789aa743aadf61b32b70f338..5854e58a014b5581fa065d1db256ffc0aa947ce2 100644 ---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -3730,7 +3730,7 @@ public abstract class LivingEntity extends Entity implements Attackable { - Vec3 vec3d1 = new Vec3(entity.getX(), entity.getEyeY(), entity.getZ()); - - // Paper - diff on change - used in CraftLivingEntity#hasLineOfSight(Location) and CraftWorld#lineOfSightExists -- return vec3d1.distanceToSqr(vec3d) > 128.0D * 128.0D ? false : this.level().clip(new ClipContext(vec3d, vec3d1, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)).getType() == HitResult.Type.MISS; // Paper - Perf: Use distance squared -+ return vec3d1.distanceToSqr(vec3d) > 128.0D * 128.0D ? false : this.level().clipDirect(vec3d, vec3d1, net.minecraft.world.phys.shapes.CollisionContext.of(this)) == HitResult.Type.MISS; // Paper - Perf: Use distance squared & strip raytracing - } - } - -diff --git a/src/main/java/net/minecraft/world/level/BlockGetter.java b/src/main/java/net/minecraft/world/level/BlockGetter.java -index bb8e962e63c7a2d931f9bd7f7c002aa35cfa5fd3..0fa131a6c98adb498fc8d534e0e39647e80c6923 100644 ---- a/src/main/java/net/minecraft/world/level/BlockGetter.java -+++ b/src/main/java/net/minecraft/world/level/BlockGetter.java -@@ -68,6 +68,18 @@ public interface BlockGetter extends LevelHeightAccessor { - }); - } - -+ // Paper start - Broken down variant of the method below, used by Level#clipDirect -+ @Nullable -+ default BlockHitResult.Type clipDirect(Vec3 start, Vec3 end, BlockPos pos, BlockState state, net.minecraft.world.phys.shapes.CollisionContext collisionContext) { -+ if (state.isAir()) { -+ return null; -+ } -+ -+ final VoxelShape voxelshape = ClipContext.Block.COLLIDER.get(state, this, pos, collisionContext); -+ final BlockHitResult hitResult = this.clipWithInteractionOverride(start, end, pos, voxelshape, state); -+ return hitResult == null ? null : hitResult.getType(); -+ } -+ // Paper end - // CraftBukkit start - moved block handling into separate method for use by Block#rayTrace - default BlockHitResult clip(ClipContext raytrace1, BlockPos blockposition) { - // Paper start - Add predicate for blocks when raytracing -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index eb7df68d146f9e841bdf99fa2d85fb4b04e603ce..12ab7097562d49dc466737378046707dc1b36b3b 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -331,10 +331,87 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - return null; - } - -- // Paper start -+ // Paper start - Broken down method of raytracing for EntityLiving#hasLineOfSight, replaces BlockGetter#clip(CollisionContext) - public net.minecraft.world.phys.BlockHitResult.Type clipDirect(Vec3 start, Vec3 end, net.minecraft.world.phys.shapes.CollisionContext context) { -- // To be patched over -- return this.clip(new ClipContext(start, end, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, context)).getType(); -+ // most of this code comes from BlockGetter#clip(CollisionContext, BiFunction, Function), but removes the needless functions -+ if (start.equals(end)) { -+ return net.minecraft.world.phys.BlockHitResult.Type.MISS; -+ } -+ -+ final double endX = Mth.lerp(-1.0E-7D, end.x, start.x); -+ final double endY = Mth.lerp(-1.0E-7D, end.y, start.y); -+ final double endZ = Mth.lerp(-1.0E-7D, end.z, start.z); -+ -+ final double startX = Mth.lerp(-1.0E-7D, start.x, end.x); -+ final double startY = Mth.lerp(-1.0E-7D, start.y, end.y); -+ final double startZ = Mth.lerp(-1.0E-7D, start.z, end.z); -+ -+ int currentX = Mth.floor(startX); -+ int currentY = Mth.floor(startY); -+ int currentZ = Mth.floor(startZ); -+ -+ final BlockPos.MutableBlockPos currentBlock = new BlockPos.MutableBlockPos(currentX, currentY, currentZ); -+ -+ LevelChunk chunk = this.getChunkIfLoaded(currentBlock); -+ if (chunk == null) { -+ return net.minecraft.world.phys.BlockHitResult.Type.MISS; -+ } -+ -+ final net.minecraft.world.phys.BlockHitResult.Type initialCheck = this.clipDirect(start, end, currentBlock, chunk.getBlockState(currentBlock), context); -+ if (initialCheck != null) { -+ return initialCheck; -+ } -+ -+ final double diffX = endX - startX; -+ final double diffY = endY - startY; -+ final double diffZ = endZ - startZ; -+ -+ final int xDirection = Mth.sign(diffX); -+ final int yDirection = Mth.sign(diffY); -+ final int zDirection = Mth.sign(diffZ); -+ -+ final double normalizedX = xDirection == 0 ? Double.MAX_VALUE : (double) xDirection / diffX; -+ final double normalizedY = yDirection == 0 ? Double.MAX_VALUE : (double) yDirection / diffY; -+ final double normalizedZ = zDirection == 0 ? Double.MAX_VALUE : (double) zDirection / diffZ; -+ -+ double normalizedXDirection = normalizedX * (xDirection > 0 ? 1.0D - Mth.frac(startX) : Mth.frac(startX)); -+ double normalizedYDirection = normalizedY * (yDirection > 0 ? 1.0D - Mth.frac(startY) : Mth.frac(startY)); -+ double normalizedZDirection = normalizedZ * (zDirection > 0 ? 1.0D - Mth.frac(startZ) : Mth.frac(startZ)); -+ -+ net.minecraft.world.phys.BlockHitResult.Type result; -+ -+ do { -+ if (normalizedXDirection > 1.0D && normalizedYDirection > 1.0D && normalizedZDirection > 1.0D) { -+ return net.minecraft.world.phys.BlockHitResult.Type.MISS; -+ } -+ -+ if (normalizedXDirection < normalizedYDirection) { -+ if (normalizedXDirection < normalizedZDirection) { -+ currentX += xDirection; -+ normalizedXDirection += normalizedX; -+ } else { -+ currentZ += zDirection; -+ normalizedZDirection += normalizedZ; -+ } -+ } else if (normalizedYDirection < normalizedZDirection) { -+ currentY += yDirection; -+ normalizedYDirection += normalizedY; -+ } else { -+ currentZ += zDirection; -+ normalizedZDirection += normalizedZ; -+ } -+ -+ currentBlock.set(currentX, currentY, currentZ); -+ if (chunk.getPos().x != currentBlock.getX() >> 4 || chunk.getPos().z != currentBlock.getZ() >> 4) { -+ chunk = this.getChunkIfLoaded(currentBlock); -+ if (chunk == null) { -+ return net.minecraft.world.phys.BlockHitResult.Type.MISS; -+ } -+ } -+ result = this.clipDirect(start, end, currentBlock, chunk.getBlockState(currentBlock), context); -+ } while (result == null); -+ -+ return result; - } - // Paper end - diff --git a/patches/server/0991-Allow-Saving-of-Oversized-Chunks.patch b/patches/server/0991-Allow-Saving-of-Oversized-Chunks.patch new file mode 100644 index 0000000000..c190834bbb --- /dev/null +++ b/patches/server/0991-Allow-Saving-of-Oversized-Chunks.patch @@ -0,0 +1,204 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Fri, 15 Feb 2019 01:08:19 -0500 +Subject: [PATCH] Allow Saving of Oversized Chunks + +Note 1.17 update: With 1.17, Entities are no longer stored in chunk slices, so this needs updating!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +The Minecraft World Region File format has a hard cap of 1MB per chunk. +This is due to the fact that the header of the file format only allocates +a single byte for sector count, meaning a maximum of 256 sectors, at 4k per sector. + +This limit can be reached fairly easily with books, resulting in the chunk being unable +to save to the world. Worse off, is that nothing printed when this occured, and silently +performed a chunk rollback on next load. + +This leads to security risk with duplication and is being actively exploited. + +This patch catches the too large scenario, falls back and moves any large Entity +or Tile Entity into a new compound, and this compound is saved into a different file. + +On Chunk Load, we check for oversized status, and if so, we load the extra file and +merge the Entities and Tile Entities from the oversized chunk back into the level to +then be loaded as normal. + +Once a chunk is returned back to normal size, the oversized flag will clear, and no +extra data file will exist. + +This fix maintains compatability with all existing Anvil Region Format tools as it +does not alter the save format. They will just not know about the extra entities. + +This fix also maintains compatability if someone switches server jars to one without +this fix, as the data will remain in the oversized file. Once the server returns +to a jar with this fix, the data will be restored. + +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java +index 307196b2a58d4f8db3e6e3c3517a8004d4908b13..f994f91dd4b4a0a6d540a5605af0b6623e229f84 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java +@@ -18,10 +18,13 @@ import java.nio.file.LinkOption; + import java.nio.file.Path; + import java.nio.file.StandardCopyOption; + import java.nio.file.StandardOpenOption; ++import java.util.zip.InflaterInputStream; // Paper + import javax.annotation.Nullable; + import net.minecraft.Util; + import net.minecraft.resources.ResourceLocation; + import net.minecraft.util.profiling.jfr.JvmProfiler; ++import net.minecraft.nbt.CompoundTag; // Paper ++import net.minecraft.nbt.NbtIo; // Paper + import net.minecraft.world.level.ChunkPos; + import org.slf4j.Logger; + +@@ -59,6 +62,7 @@ public class RegionFile implements AutoCloseable { + this.usedSectors = new RegionBitmap(); + this.info = storageKey; + this.path = path; ++ initOversizedState(); // Paper + this.version = compressionFormat; + if (!Files.isDirectory(directory, new LinkOption[0])) { + throw new IllegalArgumentException("Expected directory, got " + String.valueOf(directory.toAbsolutePath())); +@@ -453,6 +457,74 @@ public class RegionFile implements AutoCloseable { + } + + public static final int MAX_CHUNK_SIZE = 500 * 1024 * 1024; // Paper - don't write garbage data to disk if writing serialization fails ++ // Paper start ++ private final byte[] oversized = new byte[1024]; ++ private int oversizedCount; ++ ++ private synchronized void initOversizedState() throws IOException { ++ Path metaFile = getOversizedMetaFile(); ++ if (Files.exists(metaFile)) { ++ final byte[] read = java.nio.file.Files.readAllBytes(metaFile); ++ System.arraycopy(read, 0, oversized, 0, oversized.length); ++ for (byte temp : oversized) { ++ oversizedCount += temp; ++ } ++ } ++ } ++ ++ private static int getChunkIndex(int x, int z) { ++ return (x & 31) + (z & 31) * 32; ++ } ++ synchronized boolean isOversized(int x, int z) { ++ return this.oversized[getChunkIndex(x, z)] == 1; ++ } ++ synchronized void setOversized(int x, int z, boolean oversized) throws IOException { ++ final int offset = getChunkIndex(x, z); ++ boolean previous = this.oversized[offset] == 1; ++ this.oversized[offset] = (byte) (oversized ? 1 : 0); ++ if (!previous && oversized) { ++ oversizedCount++; ++ } else if (!oversized && previous) { ++ oversizedCount--; ++ } ++ if (previous && !oversized) { ++ Path oversizedFile = getOversizedFile(x, z); ++ if (Files.exists(oversizedFile)) { ++ Files.delete(oversizedFile); ++ } ++ } ++ if (oversizedCount > 0) { ++ if (previous != oversized) { ++ writeOversizedMeta(); ++ } ++ } else if (previous) { ++ Path oversizedMetaFile = getOversizedMetaFile(); ++ if (Files.exists(oversizedMetaFile)) { ++ Files.delete(oversizedMetaFile); ++ } ++ } ++ } ++ ++ private void writeOversizedMeta() throws IOException { ++ java.nio.file.Files.write(getOversizedMetaFile(), oversized); ++ } ++ ++ private Path getOversizedMetaFile() { ++ return this.path.getParent().resolve(this.path.getFileName().toString().replaceAll("\\.mca$", "") + ".oversized.nbt"); ++ } ++ ++ private Path getOversizedFile(int x, int z) { ++ return this.path.getParent().resolve(this.path.getFileName().toString().replaceAll("\\.mca$", "") + "_oversized_" + x + "_" + z + ".nbt"); ++ } ++ ++ synchronized CompoundTag getOversizedData(int x, int z) throws IOException { ++ Path file = getOversizedFile(x, z); ++ try (DataInputStream out = new DataInputStream(new java.io.BufferedInputStream(new InflaterInputStream(Files.newInputStream(file))))) { ++ return NbtIo.read((java.io.DataInput) out); ++ } ++ ++ } ++ // Paper end + private class ChunkBuffer extends ByteArrayOutputStream { + + private final ChunkPos pos; +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +index 3f6955be976064eb542b5c50a9d6d74457c1833c..c2838ae91b7f078369b63503df57a1eb5d2b0df5 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +@@ -123,6 +123,43 @@ public class RegionFileStorage implements AutoCloseable { + } + } + ++ // Paper start ++ private static void printOversizedLog(String msg, Path file, int x, int z) { ++ org.apache.logging.log4j.LogManager.getLogger().fatal(msg + " (" + file.toString().replaceAll(".+[\\\\/]", "") + " - " + x + "," + z + ") Go clean it up to remove this message. /minecraft:tp " + (x<<4)+" 128 "+(z<<4) + " - DO NOT REPORT THIS TO PAPER - You may ask for help on Discord, but do not file an issue. These error messages can not be removed."); ++ } ++ ++ private static CompoundTag readOversizedChunk(RegionFile regionfile, ChunkPos chunkCoordinate) throws IOException { ++ synchronized (regionfile) { ++ try (DataInputStream datainputstream = regionfile.getChunkDataInputStream(chunkCoordinate)) { ++ CompoundTag oversizedData = regionfile.getOversizedData(chunkCoordinate.x, chunkCoordinate.z); ++ CompoundTag chunk = NbtIo.read((DataInput) datainputstream); ++ if (oversizedData == null) { ++ return chunk; ++ } ++ CompoundTag oversizedLevel = oversizedData.getCompound("Level"); ++ ++ mergeChunkList(chunk.getCompound("Level"), oversizedLevel, "Entities", "Entities"); ++ mergeChunkList(chunk.getCompound("Level"), oversizedLevel, "TileEntities", "TileEntities"); ++ ++ return chunk; ++ } catch (Throwable throwable) { ++ throwable.printStackTrace(); ++ throw throwable; ++ } ++ } ++ } ++ ++ private static void mergeChunkList(CompoundTag level, CompoundTag oversizedLevel, String key, String oversizedKey) { ++ net.minecraft.nbt.ListTag levelList = level.getList(key, net.minecraft.nbt.Tag.TAG_COMPOUND); ++ net.minecraft.nbt.ListTag oversizedList = oversizedLevel.getList(oversizedKey, net.minecraft.nbt.Tag.TAG_COMPOUND); ++ ++ if (!oversizedList.isEmpty()) { ++ levelList.addAll(oversizedList); ++ level.put(key, levelList); ++ } ++ } ++ // Paper end ++ + @Nullable + public CompoundTag read(ChunkPos pos) throws IOException { + // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing +@@ -134,6 +171,12 @@ public class RegionFileStorage implements AutoCloseable { + try { // Paper + DataInputStream datainputstream = regionfile.getChunkDataInputStream(pos); + ++ // Paper start ++ if (regionfile.isOversized(pos.x, pos.z)) { ++ printOversizedLog("Loading Oversized Chunk!", regionfile.getPath(), pos.x, pos.z); ++ return readOversizedChunk(regionfile, pos); ++ } ++ // Paper end + CompoundTag nbttagcompound; + label43: + { +@@ -225,6 +268,7 @@ public class RegionFileStorage implements AutoCloseable { + + try { + NbtIo.write(nbt, (DataOutput) dataoutputstream); ++ regionfile.setOversized(pos.x, pos.z, false); // Paper - We don't do this anymore, mojang stores differently, but clear old meta flag if it exists to get rid of our own meta file once last oversized is gone + // Paper start - don't write garbage data to disk if writing serialization fails + dataoutputstream.close(); // Only write if successful + } catch (final RegionFileSizeException e) { diff --git a/patches/server/0991-Optimize-Network-Manager-and-add-advanced-packet-sup.patch b/patches/server/0991-Optimize-Network-Manager-and-add-advanced-packet-sup.patch deleted file mode 100644 index 84396ba635..0000000000 --- a/patches/server/0991-Optimize-Network-Manager-and-add-advanced-packet-sup.patch +++ /dev/null @@ -1,395 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Wed, 6 May 2020 04:53:35 -0400 -Subject: [PATCH] Optimize Network Manager and add advanced packet support - -Adds ability for 1 packet to bundle other packets to follow it -Adds ability for a packet to delay sending more packets until a state is ready. - -Removes synchronization from sending packets -Removes processing packet queue off of main thread - - for the few cases where it is allowed, order is not necessary nor - should it even be happening concurrently in first place (handshaking/login/status) - -Ensures packets sent asynchronously are dispatched on main thread - -This helps ensure safety for ProtocolLib as packet listeners -are commonly accessing world state. This will allow you to schedule -a packet to be sent async, but itll be dispatched sync for packet -listeners to process. - -This should solve some deadlock risks - -Also adds Netty Channel Flush Consolidation to reduce the amount of flushing - -Also avoids spamming closed channel exception by rechecking closed state in dispatch -and then catch exceptions and close if they fire. - -Part of this commit was authored by: Spottedleaf, sandtechnology - -diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java -index f57679d88cd015caa8996d44b486da694df29521..f94458d4270042062b9ae18931cc8179ef1fb420 100644 ---- a/src/main/java/net/minecraft/network/Connection.java -+++ b/src/main/java/net/minecraft/network/Connection.java -@@ -93,7 +93,7 @@ public class Connection extends SimpleChannelInboundHandler> { - private static final ProtocolInfo INITIAL_PROTOCOL = HandshakeProtocols.SERVERBOUND; - private final PacketFlow receiving; - private volatile boolean sendLoginDisconnect = true; -- private final Queue> pendingActions = Queues.newConcurrentLinkedQueue(); -+ private final Queue pendingActions = Queues.newConcurrentLinkedQueue(); // Paper - public Channel channel; - public SocketAddress address; - // Spigot Start -@@ -125,6 +125,10 @@ public class Connection extends SimpleChannelInboundHandler> { - public java.net.InetSocketAddress virtualHost; - private static boolean enableExplicitFlush = Boolean.getBoolean("paper.explicit-flush"); // Paper - Disable explicit network manager flushing - // Paper end -+ // Paper start - Optimize network -+ public boolean isPending = true; -+ public boolean queueImmunity; -+ // Paper end - Optimize network - - // Paper start - add utility methods - public final net.minecraft.server.level.ServerPlayer getPlayer() { -@@ -415,15 +419,39 @@ public class Connection extends SimpleChannelInboundHandler> { - } - - public void send(Packet packet, @Nullable PacketSendListener callbacks, boolean flush) { -- if (this.isConnected()) { -- this.flushQueue(); -+ // Paper start - Optimize network: Handle oversized packets better -+ final boolean connected = this.isConnected(); -+ if (!connected && !this.preparing) { -+ return; -+ } -+ -+ packet.onPacketDispatch(this.getPlayer()); -+ if (connected && (InnerUtil.canSendImmediate(this, packet) -+ || (io.papermc.paper.util.MCUtil.isMainThread() && packet.isReady() && this.pendingActions.isEmpty() -+ && (packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty())))) { - this.sendPacket(packet, callbacks, flush); - } else { -- this.pendingActions.add((networkmanager) -> { -- networkmanager.sendPacket(packet, callbacks, flush); -- }); -- } -+ // Write the packets to the queue, then flush - antixray hooks there already -+ final java.util.List> extraPackets = InnerUtil.buildExtraPackets(packet); -+ final boolean hasExtraPackets = extraPackets != null && !extraPackets.isEmpty(); -+ if (!hasExtraPackets) { -+ this.pendingActions.add(new PacketSendAction(packet, callbacks, flush)); -+ } else { -+ final java.util.List actions = new java.util.ArrayList<>(1 + extraPackets.size()); -+ actions.add(new PacketSendAction(packet, null, false)); // Delay the future listener until the end of the extra packets -+ -+ for (int i = 0, len = extraPackets.size(); i < len;) { -+ final Packet extraPacket = extraPackets.get(i); -+ final boolean end = ++i == len; -+ actions.add(new PacketSendAction(extraPacket, end ? callbacks : null, end)); // Append listener to the end -+ } -+ -+ this.pendingActions.addAll(actions); -+ } - -+ this.flushQueue(); -+ // Paper end - Optimize network -+ } - } - - public void runOnceConnected(Consumer task) { -@@ -431,7 +459,7 @@ public class Connection extends SimpleChannelInboundHandler> { - this.flushQueue(); - task.accept(this); - } else { -- this.pendingActions.add(task); -+ this.pendingActions.add(new WrappedConsumer(task)); // Paper - Optimize network - } - - } -@@ -449,6 +477,14 @@ public class Connection extends SimpleChannelInboundHandler> { - } - - private void doSendPacket(Packet packet, @Nullable PacketSendListener callbacks, boolean flush) { -+ // Paper start - Optimize network -+ final net.minecraft.server.level.ServerPlayer player = this.getPlayer(); -+ if (!this.isConnected()) { -+ packet.onPacketDispatchFinish(player, null); -+ return; -+ } -+ try { -+ // Paper end - Optimize network - ChannelFuture channelfuture = flush ? this.channel.writeAndFlush(packet) : this.channel.write(packet); - - if (callbacks != null) { -@@ -468,14 +504,24 @@ public class Connection extends SimpleChannelInboundHandler> { - }); - } - -+ // Paper start - Optimize network -+ if (packet.hasFinishListener()) { -+ channelfuture.addListener((ChannelFutureListener) channelFuture -> packet.onPacketDispatchFinish(player, channelFuture)); -+ } - channelfuture.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); -+ } catch (final Exception e) { -+ LOGGER.error("NetworkException: {}", player, e); -+ this.disconnect(Component.translatable("disconnect.genericReason", "Internal Exception: " + e.getMessage())); -+ packet.onPacketDispatchFinish(player, null); -+ } -+ // Paper end - Optimize network - } - - public void flushChannel() { - if (this.isConnected()) { - this.flush(); - } else { -- this.pendingActions.add(Connection::flush); -+ this.pendingActions.add(new WrappedConsumer(Connection::flush)); // Paper - Optimize network - } - - } -@@ -491,20 +537,57 @@ public class Connection extends SimpleChannelInboundHandler> { - - } - -- private void flushQueue() { -- if (this.channel != null && this.channel.isOpen()) { -- Queue queue = this.pendingActions; -- -+ // Paper start - Optimize network: Rewrite this to be safer if ran off main thread -+ private boolean flushQueue() { -+ if (!this.isConnected()) { -+ return true; -+ } -+ if (io.papermc.paper.util.MCUtil.isMainThread()) { -+ return this.processQueue(); -+ } else if (this.isPending) { -+ // Should only happen during login/status stages - synchronized (this.pendingActions) { -- Consumer consumer; -+ return this.processQueue(); -+ } -+ } -+ return false; -+ } -+ -+ private boolean processQueue() { -+ if (this.pendingActions.isEmpty()) { -+ return true; -+ } - -- while ((consumer = (Consumer) this.pendingActions.poll()) != null) { -- consumer.accept(this); -+ // 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 -+ final java.util.Iterator iterator = this.pendingActions.iterator(); -+ while (iterator.hasNext()) { -+ final WrappedConsumer queued = iterator.next(); // poll -> peek -+ -+ // Fix NPE (Spigot bug caused by handleDisconnection()) -+ if (queued == null) { -+ return true; -+ } -+ -+ if (queued.isConsumed()) { -+ continue; -+ } -+ -+ if (queued instanceof PacketSendAction packetSendAction) { -+ final Packet packet = packetSendAction.packet; -+ if (!packet.isReady()) { -+ return false; - } -+ } - -+ iterator.remove(); -+ if (queued.tryMarkConsumed()) { -+ queued.accept(this); - } - } -+ return true; - } -+ // Paper end - Optimize network - - private static final int MAX_PER_TICK = io.papermc.paper.configuration.GlobalConfiguration.get().misc.maxJoinsPerTick; // Paper - Buffer joins to world - private static int joinAttemptsThisTick; // Paper - Buffer joins to world -@@ -564,6 +647,7 @@ public class Connection extends SimpleChannelInboundHandler> { - public void disconnect(Component disconnectReason) { - // Spigot Start - this.preparing = false; -+ this.clearPacketQueue(); // Paper - Optimize network - // Spigot End - if (this.channel == null) { - this.delayedDisconnect = disconnectReason; -@@ -751,7 +835,7 @@ public class Connection extends SimpleChannelInboundHandler> { - public void handleDisconnection() { - if (this.channel != null && !this.channel.isOpen()) { - if (this.disconnectionHandled) { -- Connection.LOGGER.warn("handleDisconnection() called twice"); -+ // Connection.LOGGER.warn("handleDisconnection() called twice"); // Paper - Don't log useless message - } else { - this.disconnectionHandled = true; - PacketListener packetlistener = this.getPacketListener(); -@@ -764,7 +848,7 @@ public class Connection extends SimpleChannelInboundHandler> { - - packetlistener1.onDisconnect(ichatbasecomponent); - } -- this.pendingActions.clear(); // Free up packet queue. -+ this.clearPacketQueue(); // Paper - Optimize network - // Paper start - Add PlayerConnectionCloseEvent - final PacketListener packetListener = this.getPacketListener(); - if (packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) { -@@ -801,4 +885,93 @@ public class Connection extends SimpleChannelInboundHandler> { - public void setBandwidthLogger(LocalSampleLogger log) { - this.bandwidthDebugMonitor = new BandwidthDebugMonitor(log); - } -+ -+ // Paper start - Optimize network -+ public void clearPacketQueue() { -+ final net.minecraft.server.level.ServerPlayer player = getPlayer(); -+ for (final Consumer queuedAction : this.pendingActions) { -+ if (queuedAction instanceof PacketSendAction packetSendAction) { -+ final Packet packet = packetSendAction.packet; -+ if (packet.hasFinishListener()) { -+ packet.onPacketDispatchFinish(player, null); -+ } -+ } -+ } -+ this.pendingActions.clear(); -+ } -+ -+ private static class InnerUtil { // Attempt to hide these methods from ProtocolLib, so it doesn't accidently pick them up. -+ -+ @Nullable -+ private static java.util.List> buildExtraPackets(final Packet packet) { -+ final java.util.List> extra = packet.getExtraPackets(); -+ if (extra == null || extra.isEmpty()) { -+ return null; -+ } -+ -+ final java.util.List> ret = new java.util.ArrayList<>(1 + extra.size()); -+ buildExtraPackets0(extra, ret); -+ return ret; -+ } -+ -+ private static void buildExtraPackets0(final java.util.List> extraPackets, final java.util.List> into) { -+ for (final Packet extra : extraPackets) { -+ into.add(extra); -+ final java.util.List> extraExtra = extra.getExtraPackets(); -+ if (extraExtra != null && !extraExtra.isEmpty()) { -+ buildExtraPackets0(extraExtra, into); -+ } -+ } -+ } -+ -+ private static boolean canSendImmediate(final Connection networkManager, final net.minecraft.network.protocol.Packet packet) { -+ return networkManager.isPending || networkManager.packetListener.protocol() != ConnectionProtocol.PLAY || -+ packet instanceof net.minecraft.network.protocol.common.ClientboundKeepAlivePacket || -+ packet instanceof net.minecraft.network.protocol.game.ClientboundPlayerChatPacket || -+ packet instanceof net.minecraft.network.protocol.game.ClientboundSystemChatPacket || -+ packet instanceof net.minecraft.network.protocol.game.ClientboundCommandSuggestionsPacket || -+ packet instanceof net.minecraft.network.protocol.game.ClientboundSetTitleTextPacket || -+ packet instanceof net.minecraft.network.protocol.game.ClientboundSetSubtitleTextPacket || -+ packet instanceof net.minecraft.network.protocol.game.ClientboundSetActionBarTextPacket || -+ packet instanceof net.minecraft.network.protocol.game.ClientboundSetTitlesAnimationPacket || -+ packet instanceof net.minecraft.network.protocol.game.ClientboundClearTitlesPacket || -+ packet instanceof net.minecraft.network.protocol.game.ClientboundSoundPacket || -+ packet instanceof net.minecraft.network.protocol.game.ClientboundSoundEntityPacket || -+ packet instanceof net.minecraft.network.protocol.game.ClientboundStopSoundPacket || -+ packet instanceof net.minecraft.network.protocol.game.ClientboundLevelParticlesPacket || -+ packet instanceof net.minecraft.network.protocol.game.ClientboundBossEventPacket; -+ } -+ } -+ -+ private static class WrappedConsumer implements Consumer { -+ private final Consumer delegate; -+ private final java.util.concurrent.atomic.AtomicBoolean consumed = new java.util.concurrent.atomic.AtomicBoolean(false); -+ -+ private WrappedConsumer(final Consumer delegate) { -+ this.delegate = delegate; -+ } -+ -+ @Override -+ public void accept(final Connection connection) { -+ this.delegate.accept(connection); -+ } -+ -+ public boolean tryMarkConsumed() { -+ return consumed.compareAndSet(false, true); -+ } -+ -+ public boolean isConsumed() { -+ return consumed.get(); -+ } -+ } -+ -+ private static final class PacketSendAction extends WrappedConsumer { -+ private final Packet packet; -+ -+ private PacketSendAction(final Packet packet, @Nullable final PacketSendListener packetSendListener, final boolean flush) { -+ super(connection -> connection.sendPacket(packet, packetSendListener, flush)); -+ this.packet = packet; -+ } -+ } -+ // Paper end - Optimize network - } -diff --git a/src/main/java/net/minecraft/network/protocol/Packet.java b/src/main/java/net/minecraft/network/protocol/Packet.java -index 4c776c591dd0a7b36945a6487fdfe86d1187b4af..5ee2ba1225fb7e4f02152b45adeb66f79ed1650d 100644 ---- a/src/main/java/net/minecraft/network/protocol/Packet.java -+++ b/src/main/java/net/minecraft/network/protocol/Packet.java -@@ -22,4 +22,31 @@ public interface Packet { - static > StreamCodec codec(StreamMemberEncoder encoder, StreamDecoder decoder) { - return StreamCodec.ofMember(encoder, decoder); - } -+ -+ // Paper start -+ /** -+ * @param player Null if not at PLAY stage yet -+ */ -+ default void onPacketDispatch(@org.jetbrains.annotations.Nullable net.minecraft.server.level.ServerPlayer player) { -+ } -+ -+ /** -+ * @param player Null if not at PLAY stage yet -+ * @param future Can be null if packet was cancelled -+ */ -+ default void onPacketDispatchFinish(@org.jetbrains.annotations.Nullable net.minecraft.server.level.ServerPlayer player, @org.jetbrains.annotations.Nullable io.netty.channel.ChannelFuture future) {} -+ -+ default boolean hasFinishListener() { -+ return false; -+ } -+ -+ default boolean isReady() { -+ return true; -+ } -+ -+ @org.jetbrains.annotations.Nullable -+ default java.util.List> getExtraPackets() { -+ return null; -+ } -+ // Paper end - } -diff --git a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java -index 96355e1da8feb6687ea0069dda4a82fcd7e25e8a..a08d9aa6e420f691795df9b627a9cd5b5c0112c5 100644 ---- a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java -+++ b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java -@@ -63,10 +63,12 @@ public class ServerConnectionListener { - final List connections = Collections.synchronizedList(Lists.newArrayList()); - // Paper start - prevent blocking on adding a new connection while the server is ticking - private final java.util.Queue pending = new java.util.concurrent.ConcurrentLinkedQueue<>(); -+ private static final boolean disableFlushConsolidation = Boolean.getBoolean("Paper.disableFlushConsolidate"); // Paper - Optimize network - private final void addPending() { - Connection connection; - while ((connection = pending.poll()) != null) { - connections.add(connection); -+ connection.isPending = false; // Paper - Optimize network - } - } - // Paper end - prevent blocking on adding a new connection while the server is ticking -@@ -112,6 +114,7 @@ public class ServerConnectionListener { - ; - } - -+ if (!disableFlushConsolidation) channel.pipeline().addFirst(new io.netty.handler.flush.FlushConsolidationHandler()); // Paper - Optimize network - ChannelPipeline channelpipeline = channel.pipeline().addLast("timeout", new ReadTimeoutHandler(30)); - - if (ServerConnectionListener.this.server.repliesToStatus()) { diff --git a/patches/server/0992-Allow-Saving-of-Oversized-Chunks.patch b/patches/server/0992-Allow-Saving-of-Oversized-Chunks.patch deleted file mode 100644 index c190834bbb..0000000000 --- a/patches/server/0992-Allow-Saving-of-Oversized-Chunks.patch +++ /dev/null @@ -1,204 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Fri, 15 Feb 2019 01:08:19 -0500 -Subject: [PATCH] Allow Saving of Oversized Chunks - -Note 1.17 update: With 1.17, Entities are no longer stored in chunk slices, so this needs updating!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - -The Minecraft World Region File format has a hard cap of 1MB per chunk. -This is due to the fact that the header of the file format only allocates -a single byte for sector count, meaning a maximum of 256 sectors, at 4k per sector. - -This limit can be reached fairly easily with books, resulting in the chunk being unable -to save to the world. Worse off, is that nothing printed when this occured, and silently -performed a chunk rollback on next load. - -This leads to security risk with duplication and is being actively exploited. - -This patch catches the too large scenario, falls back and moves any large Entity -or Tile Entity into a new compound, and this compound is saved into a different file. - -On Chunk Load, we check for oversized status, and if so, we load the extra file and -merge the Entities and Tile Entities from the oversized chunk back into the level to -then be loaded as normal. - -Once a chunk is returned back to normal size, the oversized flag will clear, and no -extra data file will exist. - -This fix maintains compatability with all existing Anvil Region Format tools as it -does not alter the save format. They will just not know about the extra entities. - -This fix also maintains compatability if someone switches server jars to one without -this fix, as the data will remain in the oversized file. Once the server returns -to a jar with this fix, the data will be restored. - -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -index 307196b2a58d4f8db3e6e3c3517a8004d4908b13..f994f91dd4b4a0a6d540a5605af0b6623e229f84 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -@@ -18,10 +18,13 @@ import java.nio.file.LinkOption; - import java.nio.file.Path; - import java.nio.file.StandardCopyOption; - import java.nio.file.StandardOpenOption; -+import java.util.zip.InflaterInputStream; // Paper - import javax.annotation.Nullable; - import net.minecraft.Util; - import net.minecraft.resources.ResourceLocation; - import net.minecraft.util.profiling.jfr.JvmProfiler; -+import net.minecraft.nbt.CompoundTag; // Paper -+import net.minecraft.nbt.NbtIo; // Paper - import net.minecraft.world.level.ChunkPos; - import org.slf4j.Logger; - -@@ -59,6 +62,7 @@ public class RegionFile implements AutoCloseable { - this.usedSectors = new RegionBitmap(); - this.info = storageKey; - this.path = path; -+ initOversizedState(); // Paper - this.version = compressionFormat; - if (!Files.isDirectory(directory, new LinkOption[0])) { - throw new IllegalArgumentException("Expected directory, got " + String.valueOf(directory.toAbsolutePath())); -@@ -453,6 +457,74 @@ public class RegionFile implements AutoCloseable { - } - - public static final int MAX_CHUNK_SIZE = 500 * 1024 * 1024; // Paper - don't write garbage data to disk if writing serialization fails -+ // Paper start -+ private final byte[] oversized = new byte[1024]; -+ private int oversizedCount; -+ -+ private synchronized void initOversizedState() throws IOException { -+ Path metaFile = getOversizedMetaFile(); -+ if (Files.exists(metaFile)) { -+ final byte[] read = java.nio.file.Files.readAllBytes(metaFile); -+ System.arraycopy(read, 0, oversized, 0, oversized.length); -+ for (byte temp : oversized) { -+ oversizedCount += temp; -+ } -+ } -+ } -+ -+ private static int getChunkIndex(int x, int z) { -+ return (x & 31) + (z & 31) * 32; -+ } -+ synchronized boolean isOversized(int x, int z) { -+ return this.oversized[getChunkIndex(x, z)] == 1; -+ } -+ synchronized void setOversized(int x, int z, boolean oversized) throws IOException { -+ final int offset = getChunkIndex(x, z); -+ boolean previous = this.oversized[offset] == 1; -+ this.oversized[offset] = (byte) (oversized ? 1 : 0); -+ if (!previous && oversized) { -+ oversizedCount++; -+ } else if (!oversized && previous) { -+ oversizedCount--; -+ } -+ if (previous && !oversized) { -+ Path oversizedFile = getOversizedFile(x, z); -+ if (Files.exists(oversizedFile)) { -+ Files.delete(oversizedFile); -+ } -+ } -+ if (oversizedCount > 0) { -+ if (previous != oversized) { -+ writeOversizedMeta(); -+ } -+ } else if (previous) { -+ Path oversizedMetaFile = getOversizedMetaFile(); -+ if (Files.exists(oversizedMetaFile)) { -+ Files.delete(oversizedMetaFile); -+ } -+ } -+ } -+ -+ private void writeOversizedMeta() throws IOException { -+ java.nio.file.Files.write(getOversizedMetaFile(), oversized); -+ } -+ -+ private Path getOversizedMetaFile() { -+ return this.path.getParent().resolve(this.path.getFileName().toString().replaceAll("\\.mca$", "") + ".oversized.nbt"); -+ } -+ -+ private Path getOversizedFile(int x, int z) { -+ return this.path.getParent().resolve(this.path.getFileName().toString().replaceAll("\\.mca$", "") + "_oversized_" + x + "_" + z + ".nbt"); -+ } -+ -+ synchronized CompoundTag getOversizedData(int x, int z) throws IOException { -+ Path file = getOversizedFile(x, z); -+ try (DataInputStream out = new DataInputStream(new java.io.BufferedInputStream(new InflaterInputStream(Files.newInputStream(file))))) { -+ return NbtIo.read((java.io.DataInput) out); -+ } -+ -+ } -+ // Paper end - private class ChunkBuffer extends ByteArrayOutputStream { - - private final ChunkPos pos; -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -index 3f6955be976064eb542b5c50a9d6d74457c1833c..c2838ae91b7f078369b63503df57a1eb5d2b0df5 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -@@ -123,6 +123,43 @@ public class RegionFileStorage implements AutoCloseable { - } - } - -+ // Paper start -+ private static void printOversizedLog(String msg, Path file, int x, int z) { -+ org.apache.logging.log4j.LogManager.getLogger().fatal(msg + " (" + file.toString().replaceAll(".+[\\\\/]", "") + " - " + x + "," + z + ") Go clean it up to remove this message. /minecraft:tp " + (x<<4)+" 128 "+(z<<4) + " - DO NOT REPORT THIS TO PAPER - You may ask for help on Discord, but do not file an issue. These error messages can not be removed."); -+ } -+ -+ private static CompoundTag readOversizedChunk(RegionFile regionfile, ChunkPos chunkCoordinate) throws IOException { -+ synchronized (regionfile) { -+ try (DataInputStream datainputstream = regionfile.getChunkDataInputStream(chunkCoordinate)) { -+ CompoundTag oversizedData = regionfile.getOversizedData(chunkCoordinate.x, chunkCoordinate.z); -+ CompoundTag chunk = NbtIo.read((DataInput) datainputstream); -+ if (oversizedData == null) { -+ return chunk; -+ } -+ CompoundTag oversizedLevel = oversizedData.getCompound("Level"); -+ -+ mergeChunkList(chunk.getCompound("Level"), oversizedLevel, "Entities", "Entities"); -+ mergeChunkList(chunk.getCompound("Level"), oversizedLevel, "TileEntities", "TileEntities"); -+ -+ return chunk; -+ } catch (Throwable throwable) { -+ throwable.printStackTrace(); -+ throw throwable; -+ } -+ } -+ } -+ -+ private static void mergeChunkList(CompoundTag level, CompoundTag oversizedLevel, String key, String oversizedKey) { -+ net.minecraft.nbt.ListTag levelList = level.getList(key, net.minecraft.nbt.Tag.TAG_COMPOUND); -+ net.minecraft.nbt.ListTag oversizedList = oversizedLevel.getList(oversizedKey, net.minecraft.nbt.Tag.TAG_COMPOUND); -+ -+ if (!oversizedList.isEmpty()) { -+ levelList.addAll(oversizedList); -+ level.put(key, levelList); -+ } -+ } -+ // Paper end -+ - @Nullable - public CompoundTag read(ChunkPos pos) throws IOException { - // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing -@@ -134,6 +171,12 @@ public class RegionFileStorage implements AutoCloseable { - try { // Paper - DataInputStream datainputstream = regionfile.getChunkDataInputStream(pos); - -+ // Paper start -+ if (regionfile.isOversized(pos.x, pos.z)) { -+ printOversizedLog("Loading Oversized Chunk!", regionfile.getPath(), pos.x, pos.z); -+ return readOversizedChunk(regionfile, pos); -+ } -+ // Paper end - CompoundTag nbttagcompound; - label43: - { -@@ -225,6 +268,7 @@ public class RegionFileStorage implements AutoCloseable { - - try { - NbtIo.write(nbt, (DataOutput) dataoutputstream); -+ regionfile.setOversized(pos.x, pos.z, false); // Paper - We don't do this anymore, mojang stores differently, but clear old meta flag if it exists to get rid of our own meta file once last oversized is gone - // Paper start - don't write garbage data to disk if writing serialization fails - dataoutputstream.close(); // Only write if successful - } catch (final RegionFileSizeException e) { diff --git a/patches/server/0992-Fix-World-isChunkGenerated-calls.patch b/patches/server/0992-Fix-World-isChunkGenerated-calls.patch new file mode 100644 index 0000000000..85cc5c81aa --- /dev/null +++ b/patches/server/0992-Fix-World-isChunkGenerated-calls.patch @@ -0,0 +1,243 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sat, 15 Jun 2019 08:54:33 -0700 +Subject: [PATCH] Fix World#isChunkGenerated calls + +Optimize World#loadChunk() too +This patch also adds a chunk status cache on region files (note that +its only purpose is to cache the status on DISK) + +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index d6ecee1db17cb9eaeffa94b3d8dd150238fdefe5..1d2d4d38ff3414896d07f7f4e40d0edb9a8c3993 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -735,9 +735,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // Paper end + + private CompletableFuture> readChunk(ChunkPos chunkPos) { +- return this.read(chunkPos).thenApplyAsync((optional) -> { +- return optional.map((nbttagcompound) -> this.upgradeChunkTag(nbttagcompound, chunkPos)); // CraftBukkit +- }, Util.backgroundExecutor()); ++ // Paper start - Cache chunk status on disk ++ try { ++ return CompletableFuture.completedFuture(Optional.ofNullable(this.readConvertChunkSync(chunkPos))); ++ } catch (Throwable thr) { ++ return CompletableFuture.failedFuture(thr); ++ } ++ // Paper end - Cache chunk status on disk + } + + // CraftBukkit start +@@ -746,6 +750,60 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // CraftBukkit end + } + ++ // Paper start - Cache chunk status on disk ++ @Nullable ++ public CompoundTag readConvertChunkSync(ChunkPos pos) throws IOException { ++ CompoundTag nbttagcompound = this.readSync(pos); ++ if (nbttagcompound == null) { ++ return null; ++ } ++ ++ nbttagcompound = this.upgradeChunkTag(nbttagcompound, pos); // CraftBukkit ++ if (nbttagcompound == null) { ++ return null; ++ } ++ ++ this.updateChunkStatusOnDisk(pos, nbttagcompound); ++ ++ return nbttagcompound; ++ } ++ ++ public ChunkStatus getChunkStatusOnDiskIfCached(ChunkPos chunkPos) { ++ net.minecraft.world.level.chunk.storage.RegionFile regionFile = regionFileCache.getRegionFileIfLoaded(chunkPos); ++ ++ return regionFile == null ? null : regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); ++ } ++ ++ public ChunkStatus getChunkStatusOnDisk(ChunkPos chunkPos) throws IOException { ++ net.minecraft.world.level.chunk.storage.RegionFile regionFile = regionFileCache.getRegionFile(chunkPos, true); ++ ++ if (regionFile == null || !regionFileCache.chunkExists(chunkPos)) { ++ return null; ++ } ++ ++ ChunkStatus status = regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); ++ ++ if (status != null) { ++ return status; ++ } ++ ++ this.readChunk(chunkPos); ++ ++ return regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); ++ } ++ ++ public void updateChunkStatusOnDisk(ChunkPos chunkPos, @Nullable CompoundTag compound) throws IOException { ++ net.minecraft.world.level.chunk.storage.RegionFile regionFile = regionFileCache.getRegionFile(chunkPos, false); ++ ++ regionFile.setStatus(chunkPos.x, chunkPos.z, ChunkSerializer.getStatus(compound)); ++ } ++ ++ public ChunkAccess getUnloadingChunk(int chunkX, int chunkZ) { ++ ChunkHolder chunkHolder = io.papermc.paper.chunk.system.ChunkSystem.getUnloadingChunkHolder(this.level, chunkX, chunkZ); ++ return chunkHolder == null ? null : chunkHolder.getAvailableChunkNow(); ++ } ++ // Paper end - Cache chunk status on disk ++ + public boolean anyPlayerCloseEnoughForSpawning(ChunkPos pos) { // Paper - public + // Spigot start + return this.anyPlayerCloseEnoughForSpawning(pos, false); +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java +index f994f91dd4b4a0a6d540a5605af0b6623e229f84..cf43daa019b239464401784938d01af83f9da47c 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java +@@ -52,6 +52,29 @@ public class RegionFile implements AutoCloseable { + @VisibleForTesting + protected final RegionBitmap usedSectors; + public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(); // Paper ++ // Paper start - Cache chunk status ++ private final net.minecraft.world.level.chunk.status.ChunkStatus[] statuses = new net.minecraft.world.level.chunk.status.ChunkStatus[32 * 32]; ++ ++ private boolean closed; ++ ++ // invoked on write/read ++ public void setStatus(int x, int z, net.minecraft.world.level.chunk.status.ChunkStatus status) { ++ if (this.closed) { ++ // We've used an invalid region file. ++ throw new IllegalStateException("RegionFile is closed"); ++ } ++ this.statuses[getChunkLocation(x, z)] = status; ++ } ++ ++ public net.minecraft.world.level.chunk.status.ChunkStatus getStatusIfCached(int x, int z) { ++ if (this.closed) { ++ // We've used an invalid region file. ++ throw new IllegalStateException("RegionFile is closed"); ++ } ++ final int location = getChunkLocation(x, z); ++ return this.statuses[location]; ++ } ++ // Paper end - Cache chunk status + + public RegionFile(RegionStorageInfo storageKey, Path directory, Path path, boolean dsync) throws IOException { + this(storageKey, directory, path, RegionFileVersion.getCompressionFormat(), dsync); // Paper - Configurable region compression format +@@ -417,6 +440,7 @@ public class RegionFile implements AutoCloseable { + return this.getOffset(pos) != 0; + } + ++ private static int getChunkLocation(int x, int z) { return (x & 31) + (z & 31) * 32; } // Paper - Cache chunk status; OBFHELPER - sort of, mirror of logic below + private static int getOffsetIndex(ChunkPos pos) { + return pos.getRegionLocalX() + pos.getRegionLocalZ() * 32; + } +@@ -427,6 +451,7 @@ public class RegionFile implements AutoCloseable { + synchronized (this) { + try { + // Paper end ++ this.closed = true; // Paper - Cache chunk status + try { + this.padToFullSector(); + } finally { +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +index c2838ae91b7f078369b63503df57a1eb5d2b0df5..c33640859aab837c85f3e860fe2241a0e78bb09a 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +@@ -268,6 +268,7 @@ public class RegionFileStorage implements AutoCloseable { + + try { + NbtIo.write(nbt, (DataOutput) dataoutputstream); ++ regionfile.setStatus(pos.x, pos.z, ChunkSerializer.getStatus(nbt)); // Paper - Cache chunk status + regionfile.setOversized(pos.x, pos.z, false); // Paper - We don't do this anymore, mojang stores differently, but clear old meta flag if it exists to get rid of our own meta file once last oversized is gone + // Paper start - don't write garbage data to disk if writing serialization fails + dataoutputstream.close(); // Only write if successful +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 01fc74e6cc8ea8808b821583afb26309587dc003..0ce0589204a01051aa1251d6af752eed4ac69c0f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -391,9 +391,23 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public boolean isChunkGenerated(int x, int z) { ++ // Paper start - Fix this method ++ if (!Bukkit.isPrimaryThread()) { ++ return java.util.concurrent.CompletableFuture.supplyAsync(() -> { ++ return CraftWorld.this.isChunkGenerated(x, z); ++ }, world.getChunkSource().mainThreadProcessor).join(); ++ } ++ ChunkAccess chunk = world.getChunkSource().getChunkAtImmediately(x, z); ++ if (chunk == null) { ++ chunk = world.getChunkSource().chunkMap.getUnloadingChunk(x, z); ++ } ++ if (chunk != null) { ++ return chunk instanceof ImposterProtoChunk || chunk instanceof net.minecraft.world.level.chunk.LevelChunk; ++ } + try { +- return this.isChunkLoaded(x, z) || this.world.getChunkSource().chunkMap.read(new ChunkPos(x, z)).get().isPresent(); +- } catch (InterruptedException | ExecutionException ex) { ++ return world.getChunkSource().chunkMap.getChunkStatusOnDisk(new ChunkPos(x, z)) == ChunkStatus.FULL; ++ } catch (java.io.IOException ex) { ++ // Paper end - Fix this method + throw new RuntimeException(ex); + } + } +@@ -547,20 +561,48 @@ public class CraftWorld extends CraftRegionAccessor implements World { + public boolean loadChunk(int x, int z, boolean generate) { + org.spigotmc.AsyncCatcher.catchOp("chunk load"); // Spigot + warnUnsafeChunk("loading a faraway chunk", x, z); // Paper +- ChunkAccess chunk = this.world.getChunkSource().getChunk(x, z, generate || isChunkGenerated(x, z) ? ChunkStatus.FULL : ChunkStatus.EMPTY, true); // Paper +- +- // If generate = false, but the chunk already exists, we will get this back. +- if (chunk instanceof ImposterProtoChunk) { +- // We then cycle through again to get the full chunk immediately, rather than after the ticket addition +- chunk = this.world.getChunkSource().getChunk(x, z, ChunkStatus.FULL, true); +- } +- +- if (chunk instanceof net.minecraft.world.level.chunk.LevelChunk) { +- this.world.getChunkSource().addRegionTicket(TicketType.PLUGIN, new ChunkPos(x, z), 0, Unit.INSTANCE); // Paper ++ // Paper start - Optimize this method ++ ChunkPos chunkPos = new ChunkPos(x, z); ++ ChunkAccess immediate = world.getChunkSource().getChunkAtIfLoadedImmediately(x, z); ++ if (immediate != null) { ++ // Plugins should use plugin tickets instead of this method to keep a chunk perpetually loaded + return true; + } + +- return false; ++ if (!generate) { ++ immediate = world.getChunkSource().chunkMap.getUnloadingChunk(x, z); ++ if (immediate != null) { ++ if (!(immediate instanceof ImposterProtoChunk) && !(immediate instanceof net.minecraft.world.level.chunk.LevelChunk)) { ++ return false; // not full status ++ } ++ world.getChunkSource().addRegionTicket(TicketType.PLUGIN, chunkPos, 0, Unit.INSTANCE); // Paper ++ world.getChunk(x, z); // make sure we're at ticket level 32 or lower ++ return true; ++ } ++ net.minecraft.world.level.chunk.storage.RegionFile file; ++ try { ++ file = world.getChunkSource().chunkMap.regionFileCache.getRegionFile(chunkPos, false); ++ } catch (java.io.IOException ex) { ++ throw new RuntimeException(ex); ++ } ++ ++ ChunkStatus status = file.getStatusIfCached(x, z); ++ if (!file.hasChunk(chunkPos) || (status != null && status != ChunkStatus.FULL)) { ++ return false; ++ } ++ ++ ChunkAccess chunk = world.getChunkSource().getChunk(x, z, ChunkStatus.EMPTY, true); ++ if (!(chunk instanceof ImposterProtoChunk) && !(chunk instanceof net.minecraft.world.level.chunk.LevelChunk)) { ++ return false; ++ } ++ ++ // fall through to load ++ // we do this so we do not re-read the chunk data on disk ++ } ++ world.getChunkSource().addRegionTicket(TicketType.PLUGIN, chunkPos, 0, Unit.INSTANCE); // Paper ++ world.getChunkSource().getChunk(x, z, ChunkStatus.FULL, true); ++ return true; ++ // Paper end - Optimize this method + } + + @Override diff --git a/patches/server/0993-Fix-World-isChunkGenerated-calls.patch b/patches/server/0993-Fix-World-isChunkGenerated-calls.patch deleted file mode 100644 index 85cc5c81aa..0000000000 --- a/patches/server/0993-Fix-World-isChunkGenerated-calls.patch +++ /dev/null @@ -1,243 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sat, 15 Jun 2019 08:54:33 -0700 -Subject: [PATCH] Fix World#isChunkGenerated calls - -Optimize World#loadChunk() too -This patch also adds a chunk status cache on region files (note that -its only purpose is to cache the status on DISK) - -diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java -index d6ecee1db17cb9eaeffa94b3d8dd150238fdefe5..1d2d4d38ff3414896d07f7f4e40d0edb9a8c3993 100644 ---- a/src/main/java/net/minecraft/server/level/ChunkMap.java -+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java -@@ -735,9 +735,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - // Paper end - - private CompletableFuture> readChunk(ChunkPos chunkPos) { -- return this.read(chunkPos).thenApplyAsync((optional) -> { -- return optional.map((nbttagcompound) -> this.upgradeChunkTag(nbttagcompound, chunkPos)); // CraftBukkit -- }, Util.backgroundExecutor()); -+ // Paper start - Cache chunk status on disk -+ try { -+ return CompletableFuture.completedFuture(Optional.ofNullable(this.readConvertChunkSync(chunkPos))); -+ } catch (Throwable thr) { -+ return CompletableFuture.failedFuture(thr); -+ } -+ // Paper end - Cache chunk status on disk - } - - // CraftBukkit start -@@ -746,6 +750,60 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - // CraftBukkit end - } - -+ // Paper start - Cache chunk status on disk -+ @Nullable -+ public CompoundTag readConvertChunkSync(ChunkPos pos) throws IOException { -+ CompoundTag nbttagcompound = this.readSync(pos); -+ if (nbttagcompound == null) { -+ return null; -+ } -+ -+ nbttagcompound = this.upgradeChunkTag(nbttagcompound, pos); // CraftBukkit -+ if (nbttagcompound == null) { -+ return null; -+ } -+ -+ this.updateChunkStatusOnDisk(pos, nbttagcompound); -+ -+ return nbttagcompound; -+ } -+ -+ public ChunkStatus getChunkStatusOnDiskIfCached(ChunkPos chunkPos) { -+ net.minecraft.world.level.chunk.storage.RegionFile regionFile = regionFileCache.getRegionFileIfLoaded(chunkPos); -+ -+ return regionFile == null ? null : regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); -+ } -+ -+ public ChunkStatus getChunkStatusOnDisk(ChunkPos chunkPos) throws IOException { -+ net.minecraft.world.level.chunk.storage.RegionFile regionFile = regionFileCache.getRegionFile(chunkPos, true); -+ -+ if (regionFile == null || !regionFileCache.chunkExists(chunkPos)) { -+ return null; -+ } -+ -+ ChunkStatus status = regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); -+ -+ if (status != null) { -+ return status; -+ } -+ -+ this.readChunk(chunkPos); -+ -+ return regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); -+ } -+ -+ public void updateChunkStatusOnDisk(ChunkPos chunkPos, @Nullable CompoundTag compound) throws IOException { -+ net.minecraft.world.level.chunk.storage.RegionFile regionFile = regionFileCache.getRegionFile(chunkPos, false); -+ -+ regionFile.setStatus(chunkPos.x, chunkPos.z, ChunkSerializer.getStatus(compound)); -+ } -+ -+ public ChunkAccess getUnloadingChunk(int chunkX, int chunkZ) { -+ ChunkHolder chunkHolder = io.papermc.paper.chunk.system.ChunkSystem.getUnloadingChunkHolder(this.level, chunkX, chunkZ); -+ return chunkHolder == null ? null : chunkHolder.getAvailableChunkNow(); -+ } -+ // Paper end - Cache chunk status on disk -+ - public boolean anyPlayerCloseEnoughForSpawning(ChunkPos pos) { // Paper - public - // Spigot start - return this.anyPlayerCloseEnoughForSpawning(pos, false); -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -index f994f91dd4b4a0a6d540a5605af0b6623e229f84..cf43daa019b239464401784938d01af83f9da47c 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -@@ -52,6 +52,29 @@ public class RegionFile implements AutoCloseable { - @VisibleForTesting - protected final RegionBitmap usedSectors; - public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(); // Paper -+ // Paper start - Cache chunk status -+ private final net.minecraft.world.level.chunk.status.ChunkStatus[] statuses = new net.minecraft.world.level.chunk.status.ChunkStatus[32 * 32]; -+ -+ private boolean closed; -+ -+ // invoked on write/read -+ public void setStatus(int x, int z, net.minecraft.world.level.chunk.status.ChunkStatus status) { -+ if (this.closed) { -+ // We've used an invalid region file. -+ throw new IllegalStateException("RegionFile is closed"); -+ } -+ this.statuses[getChunkLocation(x, z)] = status; -+ } -+ -+ public net.minecraft.world.level.chunk.status.ChunkStatus getStatusIfCached(int x, int z) { -+ if (this.closed) { -+ // We've used an invalid region file. -+ throw new IllegalStateException("RegionFile is closed"); -+ } -+ final int location = getChunkLocation(x, z); -+ return this.statuses[location]; -+ } -+ // Paper end - Cache chunk status - - public RegionFile(RegionStorageInfo storageKey, Path directory, Path path, boolean dsync) throws IOException { - this(storageKey, directory, path, RegionFileVersion.getCompressionFormat(), dsync); // Paper - Configurable region compression format -@@ -417,6 +440,7 @@ public class RegionFile implements AutoCloseable { - return this.getOffset(pos) != 0; - } - -+ private static int getChunkLocation(int x, int z) { return (x & 31) + (z & 31) * 32; } // Paper - Cache chunk status; OBFHELPER - sort of, mirror of logic below - private static int getOffsetIndex(ChunkPos pos) { - return pos.getRegionLocalX() + pos.getRegionLocalZ() * 32; - } -@@ -427,6 +451,7 @@ public class RegionFile implements AutoCloseable { - synchronized (this) { - try { - // Paper end -+ this.closed = true; // Paper - Cache chunk status - try { - this.padToFullSector(); - } finally { -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -index c2838ae91b7f078369b63503df57a1eb5d2b0df5..c33640859aab837c85f3e860fe2241a0e78bb09a 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -@@ -268,6 +268,7 @@ public class RegionFileStorage implements AutoCloseable { - - try { - NbtIo.write(nbt, (DataOutput) dataoutputstream); -+ regionfile.setStatus(pos.x, pos.z, ChunkSerializer.getStatus(nbt)); // Paper - Cache chunk status - regionfile.setOversized(pos.x, pos.z, false); // Paper - We don't do this anymore, mojang stores differently, but clear old meta flag if it exists to get rid of our own meta file once last oversized is gone - // Paper start - don't write garbage data to disk if writing serialization fails - dataoutputstream.close(); // Only write if successful -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index 01fc74e6cc8ea8808b821583afb26309587dc003..0ce0589204a01051aa1251d6af752eed4ac69c0f 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -@@ -391,9 +391,23 @@ public class CraftWorld extends CraftRegionAccessor implements World { - - @Override - public boolean isChunkGenerated(int x, int z) { -+ // Paper start - Fix this method -+ if (!Bukkit.isPrimaryThread()) { -+ return java.util.concurrent.CompletableFuture.supplyAsync(() -> { -+ return CraftWorld.this.isChunkGenerated(x, z); -+ }, world.getChunkSource().mainThreadProcessor).join(); -+ } -+ ChunkAccess chunk = world.getChunkSource().getChunkAtImmediately(x, z); -+ if (chunk == null) { -+ chunk = world.getChunkSource().chunkMap.getUnloadingChunk(x, z); -+ } -+ if (chunk != null) { -+ return chunk instanceof ImposterProtoChunk || chunk instanceof net.minecraft.world.level.chunk.LevelChunk; -+ } - try { -- return this.isChunkLoaded(x, z) || this.world.getChunkSource().chunkMap.read(new ChunkPos(x, z)).get().isPresent(); -- } catch (InterruptedException | ExecutionException ex) { -+ return world.getChunkSource().chunkMap.getChunkStatusOnDisk(new ChunkPos(x, z)) == ChunkStatus.FULL; -+ } catch (java.io.IOException ex) { -+ // Paper end - Fix this method - throw new RuntimeException(ex); - } - } -@@ -547,20 +561,48 @@ public class CraftWorld extends CraftRegionAccessor implements World { - public boolean loadChunk(int x, int z, boolean generate) { - org.spigotmc.AsyncCatcher.catchOp("chunk load"); // Spigot - warnUnsafeChunk("loading a faraway chunk", x, z); // Paper -- ChunkAccess chunk = this.world.getChunkSource().getChunk(x, z, generate || isChunkGenerated(x, z) ? ChunkStatus.FULL : ChunkStatus.EMPTY, true); // Paper -- -- // If generate = false, but the chunk already exists, we will get this back. -- if (chunk instanceof ImposterProtoChunk) { -- // We then cycle through again to get the full chunk immediately, rather than after the ticket addition -- chunk = this.world.getChunkSource().getChunk(x, z, ChunkStatus.FULL, true); -- } -- -- if (chunk instanceof net.minecraft.world.level.chunk.LevelChunk) { -- this.world.getChunkSource().addRegionTicket(TicketType.PLUGIN, new ChunkPos(x, z), 0, Unit.INSTANCE); // Paper -+ // Paper start - Optimize this method -+ ChunkPos chunkPos = new ChunkPos(x, z); -+ ChunkAccess immediate = world.getChunkSource().getChunkAtIfLoadedImmediately(x, z); -+ if (immediate != null) { -+ // Plugins should use plugin tickets instead of this method to keep a chunk perpetually loaded - return true; - } - -- return false; -+ if (!generate) { -+ immediate = world.getChunkSource().chunkMap.getUnloadingChunk(x, z); -+ if (immediate != null) { -+ if (!(immediate instanceof ImposterProtoChunk) && !(immediate instanceof net.minecraft.world.level.chunk.LevelChunk)) { -+ return false; // not full status -+ } -+ world.getChunkSource().addRegionTicket(TicketType.PLUGIN, chunkPos, 0, Unit.INSTANCE); // Paper -+ world.getChunk(x, z); // make sure we're at ticket level 32 or lower -+ return true; -+ } -+ net.minecraft.world.level.chunk.storage.RegionFile file; -+ try { -+ file = world.getChunkSource().chunkMap.regionFileCache.getRegionFile(chunkPos, false); -+ } catch (java.io.IOException ex) { -+ throw new RuntimeException(ex); -+ } -+ -+ ChunkStatus status = file.getStatusIfCached(x, z); -+ if (!file.hasChunk(chunkPos) || (status != null && status != ChunkStatus.FULL)) { -+ return false; -+ } -+ -+ ChunkAccess chunk = world.getChunkSource().getChunk(x, z, ChunkStatus.EMPTY, true); -+ if (!(chunk instanceof ImposterProtoChunk) && !(chunk instanceof net.minecraft.world.level.chunk.LevelChunk)) { -+ return false; -+ } -+ -+ // fall through to load -+ // we do this so we do not re-read the chunk data on disk -+ } -+ world.getChunkSource().addRegionTicket(TicketType.PLUGIN, chunkPos, 0, Unit.INSTANCE); // Paper -+ world.getChunkSource().getChunk(x, z, ChunkStatus.FULL, true); -+ return true; -+ // Paper end - Optimize this method - } - - @Override diff --git a/patches/server/0993-Flat-bedrock-generator-settings.patch b/patches/server/0993-Flat-bedrock-generator-settings.patch new file mode 100644 index 0000000000..7103386726 --- /dev/null +++ b/patches/server/0993-Flat-bedrock-generator-settings.patch @@ -0,0 +1,292 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Byteflux +Date: Wed, 2 Mar 2016 02:17:54 -0600 +Subject: [PATCH] Flat bedrock generator settings + +== AT == +public net.minecraft.world.level.levelgen.SurfaceRules$Condition +public net.minecraft.world.level.levelgen.SurfaceRules$Context +public net.minecraft.world.level.levelgen.SurfaceRules$Context blockX +public net.minecraft.world.level.levelgen.SurfaceRules$Context blockY +public net.minecraft.world.level.levelgen.SurfaceRules$Context blockZ +public net.minecraft.world.level.levelgen.SurfaceRules$Context context +public net.minecraft.world.level.levelgen.SurfaceRules$Context randomState +public net.minecraft.world.level.levelgen.SurfaceRules$LazyYCondition +public net.minecraft.world.level.levelgen.SurfaceRules$LazyCondition +public net.minecraft.world.level.levelgen.SurfaceRules$VerticalGradientConditionSource +public net.minecraft.world.level.levelgen.SurfaceRules$SurfaceRule + +Co-authored-by: Noah van der Aa + +diff --git a/src/main/java/io/papermc/paper/world/worldgen/OptionallyFlatBedrockConditionSource.java b/src/main/java/io/papermc/paper/world/worldgen/OptionallyFlatBedrockConditionSource.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e0d73113c937ddbcf8144f88b15a8d3d080b90a0 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/world/worldgen/OptionallyFlatBedrockConditionSource.java +@@ -0,0 +1,80 @@ ++package io.papermc.paper.world.worldgen; ++ ++import com.mojang.serialization.Codec; ++import com.mojang.serialization.MapCodec; ++import com.mojang.serialization.codecs.RecordCodecBuilder; ++import net.minecraft.core.Registry; ++import net.minecraft.core.registries.BuiltInRegistries; ++import net.minecraft.core.registries.Registries; ++import net.minecraft.resources.ResourceKey; ++import net.minecraft.resources.ResourceLocation; ++import net.minecraft.util.KeyDispatchDataCodec; ++import net.minecraft.util.Mth; ++import net.minecraft.util.RandomSource; ++import net.minecraft.world.level.levelgen.PositionalRandomFactory; ++import net.minecraft.world.level.levelgen.SurfaceRules; ++import net.minecraft.world.level.levelgen.VerticalAnchor; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++// Modelled off of SurfaceRules$VerticalGradientConditionSource ++@DefaultQualifier(NonNull.class) ++public record OptionallyFlatBedrockConditionSource(ResourceLocation randomName, VerticalAnchor trueAtAndBelow, VerticalAnchor falseAtAndAbove, boolean isRoof) implements SurfaceRules.ConditionSource { ++ ++ private static final ResourceKey> CODEC_RESOURCE_KEY = ResourceKey.create( ++ Registries.MATERIAL_CONDITION, ++ new ResourceLocation(ResourceLocation.PAPER_NAMESPACE, "optionally_flat_bedrock_condition_source") ++ ); ++ private static final KeyDispatchDataCodec CODEC = KeyDispatchDataCodec.of(RecordCodecBuilder.mapCodec((instance) -> { ++ return instance.group( ++ ResourceLocation.CODEC.fieldOf("random_name").forGetter(OptionallyFlatBedrockConditionSource::randomName), ++ VerticalAnchor.CODEC.fieldOf("true_at_and_below").forGetter(OptionallyFlatBedrockConditionSource::trueAtAndBelow), ++ VerticalAnchor.CODEC.fieldOf("false_at_and_above").forGetter(OptionallyFlatBedrockConditionSource::falseAtAndAbove), ++ Codec.BOOL.fieldOf("is_roof").forGetter(OptionallyFlatBedrockConditionSource::isRoof) ++ ).apply(instance, OptionallyFlatBedrockConditionSource::new); ++ })); ++ ++ public static void bootstrap() { ++ Registry.register(BuiltInRegistries.MATERIAL_CONDITION, CODEC_RESOURCE_KEY, CODEC.codec()); ++ } ++ ++ @Override ++ public KeyDispatchDataCodec codec() { ++ return CODEC; ++ } ++ ++ @Override ++ public SurfaceRules.Condition apply(final SurfaceRules.Context context) { ++ boolean hasFlatBedrock = context.context.getWorld().paperConfig().environment.generateFlatBedrock; ++ int tempTrueAtAndBelowY = this.trueAtAndBelow().resolveY(context.context); ++ int tempFalseAtAndAboveY = this.falseAtAndAbove().resolveY(context.context); ++ ++ int flatYLevel = this.isRoof ? Math.max(tempFalseAtAndAboveY, tempTrueAtAndBelowY) - 1 : Math.min(tempFalseAtAndAboveY, tempTrueAtAndBelowY); ++ final int trueAtAndBelowY = hasFlatBedrock ? flatYLevel : tempTrueAtAndBelowY; ++ final int falseAtAndAboveY = hasFlatBedrock ? flatYLevel : tempFalseAtAndAboveY; ++ ++ final PositionalRandomFactory positionalRandomFactory = context.randomState.getOrCreateRandomFactory(this.randomName()); ++ ++ class VerticalGradientCondition extends SurfaceRules.LazyYCondition { ++ VerticalGradientCondition(SurfaceRules.Context context) { ++ super(context); ++ } ++ ++ @Override ++ protected boolean compute() { ++ int blockY = this.context.blockY; ++ if (blockY <= trueAtAndBelowY) { ++ return true; ++ } else if (blockY >= falseAtAndAboveY) { ++ return false; ++ } else { ++ double d = Mth.map(blockY, trueAtAndBelowY, falseAtAndAboveY, 1.0D, 0.0D); ++ RandomSource randomSource = positionalRandomFactory.at(this.context.blockX, blockY, this.context.blockZ); ++ return (double)randomSource.nextFloat() < d; ++ } ++ } ++ } ++ ++ return new VerticalGradientCondition(context); ++ } ++} +diff --git a/src/main/java/net/minecraft/server/Bootstrap.java b/src/main/java/net/minecraft/server/Bootstrap.java +index 05f4be8dfff37ab6804a2d990b1f8c430d42dc7c..2a49a6a6bccabf588ba67b565d8f16fda04ba0b0 100644 +--- a/src/main/java/net/minecraft/server/Bootstrap.java ++++ b/src/main/java/net/minecraft/server/Bootstrap.java +@@ -76,6 +76,7 @@ public class Bootstrap { + CauldronInteraction.bootStrap(); + // Paper start + BuiltInRegistries.bootStrap(() -> { ++ io.papermc.paper.world.worldgen.OptionallyFlatBedrockConditionSource.bootstrap(); // Paper - Flat bedrock generator settings + io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler.enterBootstrappers(); // Paper - Entrypoint for bootstrapping + }); + // Paper end +diff --git a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java +index dc765b92cc90f5f370254e68bbbdfa5add7935ce..8ce870a5341a61fbbaf42021ef7f7f615a6a3e09 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java +@@ -207,7 +207,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator { + @Override + public void buildSurface(WorldGenRegion region, StructureManager structures, RandomState noiseConfig, ChunkAccess chunk) { + if (!SharedConstants.debugVoidTerrain(chunk.getPos())) { +- WorldGenerationContext worldgenerationcontext = new WorldGenerationContext(this, region); ++ WorldGenerationContext worldgenerationcontext = new WorldGenerationContext(this, region, region.getMinecraftWorld()); // Paper - Flat bedrock generator settings + + this.buildSurface(chunk, worldgenerationcontext, noiseConfig, structures, region.getBiomeManager(), region.registryAccess().registryOrThrow(Registries.BIOME), Blender.of(region)); + } +@@ -235,7 +235,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator { + return this.createNoiseChunk(ichunkaccess1, structureAccessor, Blender.of(chunkRegion), noiseConfig); + }); + Aquifer aquifer = noisechunk.aquifer(); +- CarvingContext carvingcontext = new CarvingContext(this, chunkRegion.registryAccess(), chunk.getHeightAccessorForGeneration(), noisechunk, noiseConfig, ((NoiseGeneratorSettings) this.settings.value()).surfaceRule()); ++ CarvingContext carvingcontext = new CarvingContext(this, chunkRegion.registryAccess(), chunk.getHeightAccessorForGeneration(), noisechunk, noiseConfig, ((NoiseGeneratorSettings) this.settings.value()).surfaceRule(), chunkRegion.getMinecraftWorld()); // Paper - Flat bedrock generator settings + CarvingMask carvingmask = ((ProtoChunk) chunk).getOrCreateCarvingMask(carverStep); + + for (int j = -8; j <= 8; ++j) { +diff --git a/src/main/java/net/minecraft/world/level/levelgen/WorldGenerationContext.java b/src/main/java/net/minecraft/world/level/levelgen/WorldGenerationContext.java +index b99283c31193e2110f6e3f39c23dbfc2442bab2b..a34e53249668d917c9d77c6837b91360a2349bbc 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/WorldGenerationContext.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/WorldGenerationContext.java +@@ -6,10 +6,13 @@ import net.minecraft.world.level.chunk.ChunkGenerator; + public class WorldGenerationContext { + private final int minY; + private final int height; ++ private final @javax.annotation.Nullable net.minecraft.world.level.Level level; // Paper - Flat bedrock generator settings + +- public WorldGenerationContext(ChunkGenerator generator, LevelHeightAccessor world) { ++ public WorldGenerationContext(ChunkGenerator generator, LevelHeightAccessor world) { this(generator, world, null); } // Paper - Flat bedrock generator settings ++ public WorldGenerationContext(ChunkGenerator generator, LevelHeightAccessor world, @org.jetbrains.annotations.Nullable net.minecraft.world.level.Level level) { // Paper - Flat bedrock generator settings + this.minY = Math.max(world.getMinBuildHeight(), generator.getMinY()); + this.height = Math.min(world.getHeight(), generator.getGenDepth()); ++ this.level = level; // Paper - Flat bedrock generator settings + } + + public int getMinGenY() { +@@ -19,4 +22,13 @@ public class WorldGenerationContext { + public int getGenDepth() { + return this.height; + } ++ ++ // Paper start - Flat bedrock generator settings ++ public net.minecraft.world.level.Level getWorld() { ++ if (this.level == null) { ++ throw new NullPointerException("WorldGenerationContext was initialized without a Level, but WorldGenerationContext#getWorld was called"); ++ } ++ return this.level; ++ } ++ // Paper end - Flat bedrock generator settings + } +diff --git a/src/main/java/net/minecraft/world/level/levelgen/carver/CarvingContext.java b/src/main/java/net/minecraft/world/level/levelgen/carver/CarvingContext.java +index 390bcf9c302effd9db42d7a0e65b5433cc2eadd6..b9e919d31e442f49300744395af3cf9431e86c57 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/carver/CarvingContext.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/carver/CarvingContext.java +@@ -27,9 +27,9 @@ public class CarvingContext extends WorldGenerationContext { + LevelHeightAccessor heightLimitView, + NoiseChunk chunkNoiseSampler, + RandomState noiseConfig, +- SurfaceRules.RuleSource materialRule ++ SurfaceRules.RuleSource materialRule, @javax.annotation.Nullable net.minecraft.world.level.Level level // Paper - Flat bedrock generator settings + ) { +- super(noiseChunkGenerator, heightLimitView); ++ super(noiseChunkGenerator, heightLimitView, level); // Paper - Flat bedrock generator settings + this.registryAccess = registryManager; + this.noiseChunk = chunkNoiseSampler; + this.randomState = noiseConfig; +diff --git a/src/main/java/net/minecraft/world/level/levelgen/placement/PlacementContext.java b/src/main/java/net/minecraft/world/level/levelgen/placement/PlacementContext.java +index 640c2683c842655bbaee8f293f1c2613ef44844e..c7dfd844c7846281ceff0d443c0160054fd36c5c 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/placement/PlacementContext.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/placement/PlacementContext.java +@@ -18,7 +18,7 @@ public class PlacementContext extends WorldGenerationContext { + private final Optional topFeature; + + public PlacementContext(WorldGenLevel world, ChunkGenerator generator, Optional placedFeature) { +- super(generator, world); ++ super(generator, world, world.getLevel()); // Paper - Flat bedrock generator settings + this.level = world; + this.generator = generator; + this.topFeature = placedFeature; +diff --git a/src/main/resources/data/minecraft/worldgen/noise_settings/amplified.json b/src/main/resources/data/minecraft/worldgen/noise_settings/amplified.json +index 3f61ea695aa6a91a0cacf1fa8dc0a9bdc6fa36e6..02d32bbdbc795db271205bef95e6987ac34b0136 100644 +--- a/src/main/resources/data/minecraft/worldgen/noise_settings/amplified.json ++++ b/src/main/resources/data/minecraft/worldgen/noise_settings/amplified.json +@@ -389,7 +389,8 @@ + { + "type": "minecraft:condition", + "if_true": { +- "type": "minecraft:vertical_gradient", ++ "type": "paper:optionally_flat_bedrock_condition_source", ++ "is_roof": false, + "false_at_and_above": { + "above_bottom": 5 + }, +diff --git a/src/main/resources/data/minecraft/worldgen/noise_settings/caves.json b/src/main/resources/data/minecraft/worldgen/noise_settings/caves.json +index 1fe9ce904cba21ff4e6efb948a0bc274a22bcb0b..2e8c1aad10a2b0adbdee4180cfc1902984b6565b 100644 +--- a/src/main/resources/data/minecraft/worldgen/noise_settings/caves.json ++++ b/src/main/resources/data/minecraft/worldgen/noise_settings/caves.json +@@ -110,7 +110,8 @@ + "if_true": { + "type": "minecraft:not", + "invert": { +- "type": "minecraft:vertical_gradient", ++ "type": "paper:optionally_flat_bedrock_condition_source", ++ "is_roof": true, + "false_at_and_above": { + "below_top": 0 + }, +@@ -130,7 +131,8 @@ + { + "type": "minecraft:condition", + "if_true": { +- "type": "minecraft:vertical_gradient", ++ "type": "paper:optionally_flat_bedrock_condition_source", ++ "is_roof": false, + "false_at_and_above": { + "above_bottom": 5 + }, +diff --git a/src/main/resources/data/minecraft/worldgen/noise_settings/large_biomes.json b/src/main/resources/data/minecraft/worldgen/noise_settings/large_biomes.json +index f4c34de998d78a80bc026d8e0d423c9bed9bbf8a..fd5bd5474b8f931f2e04706997e71cd5145a5a82 100644 +--- a/src/main/resources/data/minecraft/worldgen/noise_settings/large_biomes.json ++++ b/src/main/resources/data/minecraft/worldgen/noise_settings/large_biomes.json +@@ -389,7 +389,8 @@ + { + "type": "minecraft:condition", + "if_true": { +- "type": "minecraft:vertical_gradient", ++ "type": "paper:optionally_flat_bedrock_condition_source", ++ "is_roof": false, + "false_at_and_above": { + "above_bottom": 5 + }, +diff --git a/src/main/resources/data/minecraft/worldgen/noise_settings/nether.json b/src/main/resources/data/minecraft/worldgen/noise_settings/nether.json +index 6219d25fcdbc761debc1d1e357757d1b977dc0b0..1657179b8c3f7e549e3b8774ecb75f292ae79c38 100644 +--- a/src/main/resources/data/minecraft/worldgen/noise_settings/nether.json ++++ b/src/main/resources/data/minecraft/worldgen/noise_settings/nether.json +@@ -108,7 +108,8 @@ + { + "type": "minecraft:condition", + "if_true": { +- "type": "minecraft:vertical_gradient", ++ "type": "paper:optionally_flat_bedrock_condition_source", ++ "is_roof": false, + "false_at_and_above": { + "above_bottom": 5 + }, +@@ -129,7 +130,8 @@ + "if_true": { + "type": "minecraft:not", + "invert": { +- "type": "minecraft:vertical_gradient", ++ "type": "paper:optionally_flat_bedrock_condition_source", ++ "is_roof": true, + "false_at_and_above": { + "below_top": 0 + }, +diff --git a/src/main/resources/data/minecraft/worldgen/noise_settings/overworld.json b/src/main/resources/data/minecraft/worldgen/noise_settings/overworld.json +index da3bda6167859f4ddf7d76ec2053f40b2139c13e..70beb7665a43a06b4afcb3c77aa77f923cb444cb 100644 +--- a/src/main/resources/data/minecraft/worldgen/noise_settings/overworld.json ++++ b/src/main/resources/data/minecraft/worldgen/noise_settings/overworld.json +@@ -389,7 +389,8 @@ + { + "type": "minecraft:condition", + "if_true": { +- "type": "minecraft:vertical_gradient", ++ "type": "paper:optionally_flat_bedrock_condition_source", ++ "is_roof": false, + "false_at_and_above": { + "above_bottom": 5 + }, diff --git a/patches/server/0994-Entity-Activation-Range-2.0.patch b/patches/server/0994-Entity-Activation-Range-2.0.patch new file mode 100644 index 0000000000..fee8d41d7e --- /dev/null +++ b/patches/server/0994-Entity-Activation-Range-2.0.patch @@ -0,0 +1,812 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Fri, 13 May 2016 01:38:06 -0400 +Subject: [PATCH] Entity Activation Range 2.0 + +Optimizes performance of Activation Range + +Adds many new configurations and a new wake up inactive system + +Fixes and adds new Immunities to improve gameplay behavior + +Adds water Mobs to activation range config and nerfs fish +Adds flying monsters to control ghast and phantoms +Adds villagers as separate config + +== AT == +public net.minecraft.world.entity.Entity isInsidePortal + +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 5285cc76b001a8b5fd5e11764a3aa11e45c9d999..b7ca39623348090c5b85e2d80f67a1747553d7d7 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -2,7 +2,6 @@ package net.minecraft.server.level; + + import com.google.common.annotations.VisibleForTesting; + import co.aikar.timings.TimingHistory; // Paper +-import co.aikar.timings.Timings; // Paper + import com.google.common.collect.Lists; + import com.mojang.datafixers.DataFixer; + import com.mojang.datafixers.util.Pair; +@@ -1234,17 +1233,17 @@ public class ServerLevel extends Level implements WorldGenLevel { + ++TimingHistory.entityTicks; // Paper - timings + // Spigot start + co.aikar.timings.Timing timer; // Paper +- if (!org.spigotmc.ActivationRange.checkIfActive(entity)) { ++ /*if (!org.spigotmc.ActivationRange.checkIfActive(entity)) { // Paper - comment out - EAR 2, reimplement below + entity.tickCount++; + timer = entity.getType().inactiveTickTimer.startTiming(); try { // Paper - timings + entity.inactiveTick(); + } finally { timer.stopTiming(); } // Paper + return; +- } ++ }*/ // Paper - comment out EAR 2 + // Spigot end + // Paper start- timings +- TimingHistory.activatedEntityTicks++; +- timer = entity.getVehicle() != null ? entity.getType().passengerTickTimer.startTiming() : entity.getType().tickTimer.startTiming(); ++ final boolean isActive = org.spigotmc.ActivationRange.checkIfActive(entity); ++ timer = isActive ? entity.getType().tickTimer.startTiming() : entity.getType().inactiveTickTimer.startTiming(); // Paper + try { + // Paper end - timings + entity.setOldPosAndRot(); +@@ -1255,9 +1254,13 @@ public class ServerLevel extends Level implements WorldGenLevel { + return BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString(); + }); + gameprofilerfiller.incrementCounter("tickNonPassenger"); ++ if (isActive) { // Paper - EAR 2 ++ TimingHistory.activatedEntityTicks++; + entity.tick(); + entity.postTick(); // CraftBukkit ++ } else { entity.inactiveTick(); } // Paper - EAR 2 + this.getProfiler().pop(); ++ } finally { timer.stopTiming(); } // Paper - timings + Iterator iterator = entity.getPassengers().iterator(); + + while (iterator.hasNext()) { +@@ -1265,13 +1268,18 @@ public class ServerLevel extends Level implements WorldGenLevel { + + this.tickPassenger(entity, entity1); + } +- } finally { timer.stopTiming(); } // Paper - timings ++ // } finally { timer.stopTiming(); } // Paper - timings - move up + + } + + private void tickPassenger(Entity vehicle, Entity passenger) { + if (!passenger.isRemoved() && passenger.getVehicle() == vehicle) { + if (passenger instanceof Player || this.entityTickList.contains(passenger)) { ++ // Paper - EAR 2 ++ final boolean isActive = org.spigotmc.ActivationRange.checkIfActive(passenger); ++ co.aikar.timings.Timing timer = isActive ? passenger.getType().passengerTickTimer.startTiming() : passenger.getType().passengerInactiveTickTimer.startTiming(); // Paper ++ try { ++ // Paper end + passenger.setOldPosAndRot(); + ++passenger.tickCount; + ProfilerFiller gameprofilerfiller = this.getProfiler(); +@@ -1280,8 +1288,17 @@ public class ServerLevel extends Level implements WorldGenLevel { + return BuiltInRegistries.ENTITY_TYPE.getKey(passenger.getType()).toString(); + }); + gameprofilerfiller.incrementCounter("tickPassenger"); ++ // Paper start - EAR 2 ++ if (isActive) { + passenger.rideTick(); + passenger.postTick(); // CraftBukkit ++ } else { ++ passenger.setDeltaMovement(Vec3.ZERO); ++ passenger.inactiveTick(); ++ // copied from inside of if (isPassenger()) of passengerTick, but that ifPassenger is unnecessary ++ vehicle.positionRider(passenger); ++ } ++ // Paper end - EAR 2 + gameprofilerfiller.pop(); + Iterator iterator = passenger.getPassengers().iterator(); + +@@ -1291,6 +1308,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.tickPassenger(passenger, entity2); + } + ++ } finally { timer.stopTiming(); }// Paper - EAR2 timings + } + } else { + passenger.stopRiding(); +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 03840f520624662d4ce3ac9f3065a01c71b5f299..a13edd1165a5ba4dd3f5c323e454926e7fe75c07 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -414,6 +414,8 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + // Spigot end + protected int numCollisions = 0; // Paper - Cap entity collisions + public boolean fromNetherPortal; // Paper - Add option to nerf pigmen from nether portals ++ public long activatedImmunityTick = Integer.MIN_VALUE; // Paper - EAR ++ public boolean isTemporarilyActive; // Paper - EAR + public boolean spawnedViaMobSpawner; // Paper - Yes this name is similar to above, upstream took the better one + // Paper start - Entity origin API + @javax.annotation.Nullable +@@ -1045,6 +1047,8 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } else { + this.wasOnFire = this.isOnFire(); + if (movementType == MoverType.PISTON) { ++ this.activatedTick = Math.max(this.activatedTick, MinecraftServer.currentTick + 20); // Paper ++ this.activatedImmunityTick = Math.max(this.activatedImmunityTick, MinecraftServer.currentTick + 20); // Paper + movement = this.limitPistonMovement(movement); + if (movement.equals(Vec3.ZERO)) { + return; +@@ -1057,6 +1061,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.stuckSpeedMultiplier = Vec3.ZERO; + this.setDeltaMovement(Vec3.ZERO); + } ++ // Paper start - ignore movement changes while inactive. ++ if (isTemporarilyActive && !(this instanceof ItemEntity) && movement == getDeltaMovement() && movementType == MoverType.SELF) { ++ setDeltaMovement(Vec3.ZERO); ++ this.level.getProfiler().pop(); ++ return; ++ } ++ // Paper end + + movement = this.maybeBackOffFromEdge(movement, movementType); + Vec3 vec3d1 = this.collide(movement); +diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java +index 0fd307e214f544284606ac8aa57f3f6bdca8bca6..eb2fd14f175a4ffca920ca116fb8d0d381286dc5 100644 +--- a/src/main/java/net/minecraft/world/entity/Mob.java ++++ b/src/main/java/net/minecraft/world/entity/Mob.java +@@ -237,6 +237,19 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti + return this.lookControl; + } + ++ // Paper start ++ @Override ++ public void inactiveTick() { ++ super.inactiveTick(); ++ if (this.goalSelector.inactiveTick()) { ++ this.goalSelector.tick(); ++ } ++ if (this.targetSelector.inactiveTick()) { ++ this.targetSelector.tick(); ++ } ++ } ++ // Paper end ++ + public MoveControl getMoveControl() { + Entity entity = this.getControlledVehicle(); + +diff --git a/src/main/java/net/minecraft/world/entity/PathfinderMob.java b/src/main/java/net/minecraft/world/entity/PathfinderMob.java +index cdd07093342521ff9944bf7a342bbf142ba3f0b7..645414720db0231a19c88f681608eb08200b8782 100644 +--- a/src/main/java/net/minecraft/world/entity/PathfinderMob.java ++++ b/src/main/java/net/minecraft/world/entity/PathfinderMob.java +@@ -23,6 +23,7 @@ public abstract class PathfinderMob extends Mob { + } + + public org.bukkit.craftbukkit.entity.CraftCreature getBukkitCreature() { return (org.bukkit.craftbukkit.entity.CraftCreature) super.getBukkitEntity(); } // Paper ++ public BlockPos movingTarget = null; public BlockPos getMovingTarget() { return movingTarget; } // Paper + + public float getWalkTargetValue(BlockPos pos) { + return this.getWalkTargetValue(pos, this.level()); +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java +index 89b8a304fe9fae4b57640afbab04b6764ce9aab8..074ef807258139f818e30494126585262c2f33c0 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java +@@ -26,6 +26,7 @@ public class GoalSelector { + private final Set availableGoals = new ObjectLinkedOpenHashSet<>(); + private final Supplier profiler; + private final EnumSet disabledFlags = EnumSet.noneOf(Goal.Flag.class); ++ private int curRate; + + public GoalSelector(Supplier profiler) { + this.profiler = profiler; +@@ -40,6 +41,20 @@ public class GoalSelector { + this.availableGoals.removeIf(goal -> predicate.test(goal.getGoal())); + } + ++ // Paper start ++ public boolean inactiveTick() { ++ this.curRate++; ++ return this.curRate % 3 == 0; // TODO newGoalRate was already unused in 1.20.4, check if this is correct ++ } ++ public boolean hasTasks() { ++ for (WrappedGoal task : this.availableGoals) { ++ if (task.isRunning()) { ++ return true; ++ } ++ } ++ return false; ++ } ++ // Paper end + public void removeGoal(Goal goal) { + for (WrappedGoal wrappedGoal : this.availableGoals) { + if (wrappedGoal.getGoal() == goal && wrappedGoal.isRunning()) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java +index 6d8ea05e5e86e9f6359b560043bb55a10784e952..aee0147649d458b87d92496eda0c1723ebe570d2 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java +@@ -23,6 +23,14 @@ public abstract class MoveToBlockGoal extends Goal { + public MoveToBlockGoal(PathfinderMob mob, double speed, int range) { + this(mob, speed, range, 1); + } ++ // Paper start - activation range improvements ++ @Override ++ public void stop() { ++ super.stop(); ++ this.blockPos = BlockPos.ZERO; ++ this.mob.movingTarget = null; ++ } ++ // Paper end + + public MoveToBlockGoal(PathfinderMob mob, double speed, int range, int maxYDifference) { + this.mob = mob; +@@ -115,6 +123,7 @@ public abstract class MoveToBlockGoal extends Goal { + mutableBlockPos.setWithOffset(blockPos, m, k - 1, n); + if (this.mob.isWithinRestriction(mutableBlockPos) && this.isValidTarget(this.mob.level(), mutableBlockPos)) { + this.blockPos = mutableBlockPos; ++ this.mob.movingTarget = mutableBlockPos == BlockPos.ZERO ? null : mutableBlockPos.immutable(); // Paper + return true; + } + } +diff --git a/src/main/java/net/minecraft/world/entity/npc/Villager.java b/src/main/java/net/minecraft/world/entity/npc/Villager.java +index 1e8579c994098fb18d9725e3f1604a582be4162f..a7930f9875aa4aca997caaead46ecdc21e5e11d7 100644 +--- a/src/main/java/net/minecraft/world/entity/npc/Villager.java ++++ b/src/main/java/net/minecraft/world/entity/npc/Villager.java +@@ -228,17 +228,34 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + @Override + public void inactiveTick() { + // SPIGOT-3874, SPIGOT-3894, SPIGOT-3846, SPIGOT-5286 :( +- if (this.level().spigotConfig.tickInactiveVillagers && this.isEffectiveAi()) { +- this.customServerAiStep(); ++ // Paper start ++ if (this.getUnhappyCounter() > 0) { ++ this.setUnhappyCounter(this.getUnhappyCounter() - 1); + } ++ if (this.isEffectiveAi()) { ++ if (this.level().spigotConfig.tickInactiveVillagers) { ++ this.customServerAiStep(); ++ } else { ++ this.customServerAiStep(true); ++ } ++ } ++ maybeDecayGossip(); ++ // Paper end ++ + super.inactiveTick(); + } + // Spigot End + + @Override ++ @Deprecated // Paper + protected void customServerAiStep() { ++ // Paper start ++ this.customServerAiStep(false); ++ } ++ protected void customServerAiStep(final boolean inactive) { ++ // Paper end + this.level().getProfiler().push("villagerBrain"); +- this.getBrain().tick((ServerLevel) this.level(), this); ++ if (!inactive) this.getBrain().tick((ServerLevel) this.level(), this); // Paper + this.level().getProfiler().pop(); + if (this.assignProfessionWhenSpawned) { + this.assignProfessionWhenSpawned = false; +@@ -262,7 +279,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + this.lastTradedPlayer = null; + } + +- if (!this.isNoAi() && this.random.nextInt(100) == 0) { ++ if (!inactive && !this.isNoAi() && this.random.nextInt(100) == 0) { // Paper + Raid raid = ((ServerLevel) this.level()).getRaidAt(this.blockPosition()); + + if (raid != null && raid.isActive() && !raid.isOver()) { +@@ -273,6 +290,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + if (this.getVillagerData().getProfession() == VillagerProfession.NONE && this.isTrading()) { + this.stopTrading(); + } ++ if (inactive) return; // Paper + + super.customServerAiStep(); + } +diff --git a/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java +index 0b7f52021441d633c37543e8ae485e81c292b747..d7f8464bf3eed0e42a5fc7f14a5b243d171f8b5e 100644 +--- a/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java ++++ b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java +@@ -52,6 +52,7 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper + if (bl != this.isEnabled()) { + this.setEnabled(bl); + } ++ this.immunize(); // Paper + } + + public boolean isEnabled() { +@@ -92,11 +93,13 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper + + public boolean suckInItems() { + if (HopperBlockEntity.suckInItems(this.level(), this)) { ++ this.immunize(); // Paper + return true; + } else { + for (ItemEntity itemEntity : this.level() + .getEntitiesOfClass(ItemEntity.class, this.getBoundingBox().inflate(0.25, 0.0, 0.25), EntitySelector.ENTITY_STILL_ALIVE)) { + if (HopperBlockEntity.addItem(this, itemEntity)) { ++ this.immunize(); // Paper + return true; + } + } +@@ -126,4 +129,11 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper + public AbstractContainerMenu createMenu(int syncId, Inventory playerInventory) { + return new HopperMenu(syncId, playerInventory, this); + } ++ ++ // Paper start ++ public void immunize() { ++ this.activatedImmunityTick = Math.max(this.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 20); ++ } ++ // Paper end ++ + } +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 12ab7097562d49dc466737378046707dc1b36b3b..2d829eb4d39aec82c23a55f30f22a9a5f7003d27 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -154,6 +154,12 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public Map capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper - Retain block place order when capturing blockstates + public List captureDrops; + public final it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap ticksPerSpawnCategory = new it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<>(); ++ // Paper start ++ public int wakeupInactiveRemainingAnimals; ++ public int wakeupInactiveRemainingFlying; ++ public int wakeupInactiveRemainingMonsters; ++ public int wakeupInactiveRemainingVillagers; ++ // Paper end + public boolean populating; + public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot + // Paper start - add paper world config +diff --git a/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java b/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java +index d7b963571c900f0f68005d6954bcd9ef1d9e0b7c..b35f476e26a020cf75e53a5eb488717d996a6935 100644 +--- a/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java +@@ -148,6 +148,10 @@ public class PistonMovingBlockEntity extends BlockEntity { + } + + entity.setDeltaMovement(e, g, h); ++ // Paper - EAR items stuck in in slime pushed by a piston ++ entity.activatedTick = Math.max(entity.activatedTick, net.minecraft.server.MinecraftServer.currentTick + 10); ++ entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 10); ++ // Paper end + break; + } + } +diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java +index 9fb9fa62c32445ac3c3883a6433759c86dcfc428..3283ed99c35ffed6805567705e0518d9f84feedc 100644 +--- a/src/main/java/org/spigotmc/ActivationRange.java ++++ b/src/main/java/org/spigotmc/ActivationRange.java +@@ -1,33 +1,43 @@ + package org.spigotmc; + ++import net.minecraft.core.BlockPos; + import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerChunkCache; + import net.minecraft.world.entity.Entity; + import net.minecraft.world.entity.ExperienceOrb; ++import net.minecraft.world.entity.FlyingMob; + import net.minecraft.world.entity.LightningBolt; + import net.minecraft.world.entity.LivingEntity; ++import net.minecraft.world.entity.Mob; + import net.minecraft.world.entity.PathfinderMob; ++import net.minecraft.world.entity.ai.Brain; + import net.minecraft.world.entity.ambient.AmbientCreature; + import net.minecraft.world.entity.animal.Animal; ++import net.minecraft.world.entity.animal.Bee; + import net.minecraft.world.entity.animal.Sheep; ++import net.minecraft.world.entity.animal.WaterAnimal; ++import net.minecraft.world.entity.animal.horse.Llama; + import net.minecraft.world.entity.boss.EnderDragonPart; + import net.minecraft.world.entity.boss.enderdragon.EndCrystal; + import net.minecraft.world.entity.boss.enderdragon.EnderDragon; + import net.minecraft.world.entity.boss.wither.WitherBoss; + import net.minecraft.world.entity.item.PrimedTnt; + import net.minecraft.world.entity.monster.Creeper; +-import net.minecraft.world.entity.monster.Monster; +-import net.minecraft.world.entity.monster.Slime; ++import net.minecraft.world.entity.monster.Enemy; ++import net.minecraft.world.entity.monster.Pillager; + import net.minecraft.world.entity.npc.Villager; + import net.minecraft.world.entity.player.Player; + import net.minecraft.world.entity.projectile.AbstractArrow; + import net.minecraft.world.entity.projectile.AbstractHurtingProjectile; ++import net.minecraft.world.entity.projectile.EyeOfEnder; + import net.minecraft.world.entity.projectile.FireworkRocketEntity; + import net.minecraft.world.entity.projectile.ThrowableProjectile; + import net.minecraft.world.entity.projectile.ThrownTrident; + import net.minecraft.world.entity.raid.Raider; ++import co.aikar.timings.MinecraftTimings; ++import net.minecraft.world.entity.schedule.Activity; + import net.minecraft.world.level.Level; + import net.minecraft.world.phys.AABB; +-import co.aikar.timings.MinecraftTimings; + + public class ActivationRange + { +@@ -44,6 +54,43 @@ public class ActivationRange + + AABB boundingBox = new AABB( 0, 0, 0, 0, 0, 0 ); + } ++ // Paper start ++ ++ static Activity[] VILLAGER_PANIC_IMMUNITIES = { ++ Activity.HIDE, ++ Activity.PRE_RAID, ++ Activity.RAID, ++ Activity.PANIC ++ }; ++ ++ private static int checkInactiveWakeup(Entity entity) { ++ Level world = entity.level(); ++ SpigotWorldConfig config = world.spigotConfig; ++ long inactiveFor = MinecraftServer.currentTick - entity.activatedTick; ++ if (entity.activationType == ActivationType.VILLAGER) { ++ if (inactiveFor > config.wakeUpInactiveVillagersEvery && world.wakeupInactiveRemainingVillagers > 0) { ++ world.wakeupInactiveRemainingVillagers--; ++ return config.wakeUpInactiveVillagersFor; ++ } ++ } else if (entity.activationType == ActivationType.ANIMAL) { ++ if (inactiveFor > config.wakeUpInactiveAnimalsEvery && world.wakeupInactiveRemainingAnimals > 0) { ++ world.wakeupInactiveRemainingAnimals--; ++ return config.wakeUpInactiveAnimalsFor; ++ } ++ } else if (entity.activationType == ActivationType.FLYING_MONSTER) { ++ if (inactiveFor > config.wakeUpInactiveFlyingEvery && world.wakeupInactiveRemainingFlying > 0) { ++ world.wakeupInactiveRemainingFlying--; ++ return config.wakeUpInactiveFlyingFor; ++ } ++ } else if (entity.activationType == ActivationType.MONSTER || entity.activationType == ActivationType.RAIDER) { ++ if (inactiveFor > config.wakeUpInactiveMonstersEvery && world.wakeupInactiveRemainingMonsters > 0) { ++ world.wakeupInactiveRemainingMonsters--; ++ return config.wakeUpInactiveMonstersFor; ++ } ++ } ++ return -1; ++ } ++ // Paper end + + static AABB maxBB = new AABB( 0, 0, 0, 0, 0, 0 ); + +@@ -56,10 +103,13 @@ public class ActivationRange + */ + public static ActivationType initializeEntityActivationType(Entity entity) + { ++ if (entity instanceof WaterAnimal) { return ActivationType.WATER; } // Paper ++ else if (entity instanceof Villager) { return ActivationType.VILLAGER; } // Paper ++ else if (entity instanceof FlyingMob && entity instanceof Enemy) { return ActivationType.FLYING_MONSTER; } // Paper - doing & Monster incase Flying no longer includes monster in future + if ( entity instanceof Raider ) + { + return ActivationType.RAIDER; +- } else if ( entity instanceof Monster || entity instanceof Slime ) ++ } else if ( entity instanceof Enemy ) // Paper - correct monster check + { + return ActivationType.MONSTER; + } else if ( entity instanceof PathfinderMob || entity instanceof AmbientCreature ) +@@ -80,10 +130,14 @@ public class ActivationRange + */ + public static boolean initializeEntityActivationState(Entity entity, SpigotWorldConfig config) + { +- if ( ( entity.activationType == ActivationType.MISC && config.miscActivationRange == 0 ) +- || ( entity.activationType == ActivationType.RAIDER && config.raiderActivationRange == 0 ) +- || ( entity.activationType == ActivationType.ANIMAL && config.animalActivationRange == 0 ) +- || ( entity.activationType == ActivationType.MONSTER && config.monsterActivationRange == 0 ) ++ if ( ( entity.activationType == ActivationType.MISC && config.miscActivationRange <= 0 ) ++ || ( entity.activationType == ActivationType.RAIDER && config.raiderActivationRange <= 0 ) ++ || ( entity.activationType == ActivationType.ANIMAL && config.animalActivationRange <= 0 ) ++ || ( entity.activationType == ActivationType.MONSTER && config.monsterActivationRange <= 0 ) ++ || ( entity.activationType == ActivationType.VILLAGER && config.villagerActivationRange <= 0 ) // Paper ++ || ( entity.activationType == ActivationType.WATER && config.waterActivationRange <= 0 ) // Paper ++ || ( entity.activationType == ActivationType.FLYING_MONSTER && config.flyingMonsterActivationRange <= 0 ) // Paper ++ || entity instanceof EyeOfEnder // Paper + || entity instanceof Player + || entity instanceof ThrowableProjectile + || entity instanceof EnderDragon +@@ -118,10 +172,25 @@ public class ActivationRange + final int raiderActivationRange = world.spigotConfig.raiderActivationRange; + final int animalActivationRange = world.spigotConfig.animalActivationRange; + final int monsterActivationRange = world.spigotConfig.monsterActivationRange; ++ // Paper start ++ final int waterActivationRange = world.spigotConfig.waterActivationRange; ++ final int flyingActivationRange = world.spigotConfig.flyingMonsterActivationRange; ++ final int villagerActivationRange = world.spigotConfig.villagerActivationRange; ++ world.wakeupInactiveRemainingAnimals = Math.min(world.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals); ++ world.wakeupInactiveRemainingVillagers = Math.min(world.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers); ++ world.wakeupInactiveRemainingMonsters = Math.min(world.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters); ++ world.wakeupInactiveRemainingFlying = Math.min(world.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying); ++ final ServerChunkCache chunkProvider = (ServerChunkCache) world.getChunkSource(); ++ // Paper end + + int maxRange = Math.max( monsterActivationRange, animalActivationRange ); + maxRange = Math.max( maxRange, raiderActivationRange ); + maxRange = Math.max( maxRange, miscActivationRange ); ++ // Paper start ++ maxRange = Math.max( maxRange, flyingActivationRange ); ++ maxRange = Math.max( maxRange, waterActivationRange ); ++ maxRange = Math.max( maxRange, villagerActivationRange ); ++ // Paper end + maxRange = Math.min( ( world.spigotConfig.simulationDistance << 4 ) - 8, maxRange ); + + for ( Player player : world.players() ) +@@ -132,13 +201,30 @@ public class ActivationRange + continue; + } + +- ActivationRange.maxBB = player.getBoundingBox().inflate( maxRange, 256, maxRange ); +- ActivationType.MISC.boundingBox = player.getBoundingBox().inflate( miscActivationRange, 256, miscActivationRange ); +- ActivationType.RAIDER.boundingBox = player.getBoundingBox().inflate( raiderActivationRange, 256, raiderActivationRange ); +- ActivationType.ANIMAL.boundingBox = player.getBoundingBox().inflate( animalActivationRange, 256, animalActivationRange ); +- ActivationType.MONSTER.boundingBox = player.getBoundingBox().inflate( monsterActivationRange, 256, monsterActivationRange ); ++ // Paper start ++ int worldHeight = world.getHeight(); ++ ActivationRange.maxBB = player.getBoundingBox().inflate( maxRange, worldHeight, maxRange ); ++ ActivationType.MISC.boundingBox = player.getBoundingBox().inflate( miscActivationRange, worldHeight, miscActivationRange ); ++ ActivationType.RAIDER.boundingBox = player.getBoundingBox().inflate( raiderActivationRange, worldHeight, raiderActivationRange ); ++ ActivationType.ANIMAL.boundingBox = player.getBoundingBox().inflate( animalActivationRange, worldHeight, animalActivationRange ); ++ ActivationType.MONSTER.boundingBox = player.getBoundingBox().inflate( monsterActivationRange, worldHeight, monsterActivationRange ); ++ ActivationType.WATER.boundingBox = player.getBoundingBox().inflate( waterActivationRange, worldHeight, waterActivationRange ); ++ ActivationType.FLYING_MONSTER.boundingBox = player.getBoundingBox().inflate( flyingActivationRange, worldHeight, flyingActivationRange ); ++ ActivationType.VILLAGER.boundingBox = player.getBoundingBox().inflate( villagerActivationRange, worldHeight, villagerActivationRange ); ++ // Paper end + +- world.getEntities().get(ActivationRange.maxBB, ActivationRange::activateEntity); ++ // Paper start ++ java.util.List entities = world.getEntities((Entity)null, ActivationRange.maxBB, null); ++ boolean tickMarkers = world.paperConfig().entities.markers.tick; // Paper - Configurable marker ticking ++ for (Entity entity : entities) { ++ // Paper start - Configurable marker ticking ++ if (!tickMarkers && entity instanceof net.minecraft.world.entity.Marker) { ++ continue; ++ } ++ // Paper end - Configurable marker ticking ++ ActivationRange.activateEntity(entity); ++ } ++ // Paper end + } + MinecraftTimings.entityActivationCheckTimer.stopTiming(); + } +@@ -171,60 +257,118 @@ public class ActivationRange + * @param entity + * @return + */ +- public static boolean checkEntityImmunities(Entity entity) ++ public static int checkEntityImmunities(Entity entity) // Paper - return # of ticks to get immunity + { ++ // Paper start ++ SpigotWorldConfig config = entity.level().spigotConfig; ++ int inactiveWakeUpImmunity = checkInactiveWakeup(entity); ++ if (inactiveWakeUpImmunity > -1) { ++ return inactiveWakeUpImmunity; ++ } ++ if (entity.getRemainingFireTicks() > 0) { ++ return 2; ++ } ++ if (entity.activatedImmunityTick >= MinecraftServer.currentTick) { ++ return 1; ++ } ++ long inactiveFor = MinecraftServer.currentTick - entity.activatedTick; ++ // Paper end + // quick checks. +- if ( entity.wasTouchingWater || entity.getRemainingFireTicks() > 0 ) ++ if ( (entity.activationType != ActivationType.WATER && entity.wasTouchingWater && entity.isPushedByFluid()) ) // Paper + { +- return true; ++ return 100; // Paper ++ } ++ // Paper start ++ if ( !entity.onGround() || entity.getDeltaMovement().horizontalDistanceSqr() > 9.999999747378752E-6D ) ++ { ++ return 100; + } ++ // Paper end + if ( !( entity instanceof AbstractArrow ) ) + { +- if ( !entity.onGround() || !entity.passengers.isEmpty() || entity.isPassenger() ) ++ if ( (!entity.onGround() && !(entity instanceof FlyingMob)) ) // Paper - remove passengers logic + { +- return true; ++ return 10; // Paper + } + } else if ( !( (AbstractArrow) entity ).inGround ) + { +- return true; ++ return 1; // Paper + } + // special cases. + if ( entity instanceof LivingEntity ) + { + LivingEntity living = (LivingEntity) entity; +- if ( /*TODO: Missed mapping? living.attackTicks > 0 || */ living.hurtTime > 0 || living.activeEffects.size() > 0 ) ++ if ( living.onClimbable() || living.jumping || living.hurtTime > 0 || living.activeEffects.size() > 0 || living.isFreezing()) // Paper + { +- return true; ++ return 1; // Paper + } +- if ( entity instanceof PathfinderMob && ( (PathfinderMob) entity ).getTarget() != null ) ++ if ( entity instanceof Mob && ((Mob) entity ).getTarget() != null) // Paper + { +- return true; ++ return 20; // Paper ++ } ++ // Paper start ++ if (entity instanceof Bee) { ++ Bee bee = (Bee)entity; ++ BlockPos movingTarget = bee.getMovingTarget(); ++ if (bee.isAngry() || ++ (bee.getHivePos() != null && bee.getHivePos().equals(movingTarget)) || ++ (bee.getSavedFlowerPos() != null && bee.getSavedFlowerPos().equals(movingTarget)) ++ ) { ++ return 20; ++ } + } +- if ( entity instanceof Villager && ( (Villager) entity ).canBreed() ) ++ if ( entity instanceof Villager ) { ++ Brain behaviorController = ((Villager) entity).getBrain(); ++ ++ if (config.villagersActiveForPanic) { ++ for (Activity activity : VILLAGER_PANIC_IMMUNITIES) { ++ if (behaviorController.isActive(activity)) { ++ return 20*5; ++ } ++ } ++ } ++ ++ if (config.villagersWorkImmunityAfter > 0 && inactiveFor >= config.villagersWorkImmunityAfter) { ++ if (behaviorController.isActive(Activity.WORK)) { ++ return config.villagersWorkImmunityFor; ++ } ++ } ++ } ++ if ( entity instanceof Llama && ( (Llama) entity ).inCaravan() ) + { +- return true; ++ return 1; + } ++ // Paper end + if ( entity instanceof Animal ) + { + Animal animal = (Animal) entity; + if ( animal.isBaby() || animal.isInLove() ) + { +- return true; ++ return 5; // Paper + } + if ( entity instanceof Sheep && ( (Sheep) entity ).isSheared() ) + { +- return true; ++ return 1; // Paper + } + } + if (entity instanceof Creeper && ((Creeper) entity).isIgnited()) { // isExplosive +- return true; ++ return 20; // Paper ++ } ++ // Paper start ++ if (entity instanceof Mob && ((Mob) entity).targetSelector.hasTasks() ) { ++ return 0; + } ++ if (entity instanceof Pillager) { ++ Pillager pillager = (Pillager) entity; ++ // TODO:? ++ } ++ // Paper end + } + // SPIGOT-6644: Otherwise the target refresh tick will be missed + if (entity instanceof ExperienceOrb) { +- return true; ++ return 20; // Paper + } +- return false; ++ return -1; // Paper + } + + /** +@@ -239,8 +383,19 @@ public class ActivationRange + if ( entity instanceof FireworkRocketEntity ) { + return true; + } ++ // Paper start - special case always immunities ++ // immunize brand new entities, dead entities, and portal scenarios ++ if (entity.defaultActivationState || entity.tickCount < 20*10 || !entity.isAlive() || entity.isInsidePortal || entity.portalCooldown > 0) { ++ return true; ++ } ++ // immunize leashed entities ++ if (entity instanceof Mob && ((Mob)entity).getLeashHolder() instanceof Player) { ++ return true; ++ } ++ // Paper end + +- boolean isActive = entity.activatedTick >= MinecraftServer.currentTick || entity.defaultActivationState; ++ boolean isActive = entity.activatedTick >= MinecraftServer.currentTick; ++ entity.isTemporarilyActive = false; // Paper + + // Should this entity tick? + if ( !isActive ) +@@ -248,15 +403,19 @@ public class ActivationRange + if ( ( MinecraftServer.currentTick - entity.activatedTick - 1 ) % 20 == 0 ) + { + // Check immunities every 20 ticks. +- if ( ActivationRange.checkEntityImmunities( entity ) ) +- { +- // Triggered some sort of immunity, give 20 full ticks before we check again. +- entity.activatedTick = MinecraftServer.currentTick + 20; ++ // Paper start ++ int immunity = checkEntityImmunities(entity); ++ if (immunity >= 0) { ++ entity.activatedTick = MinecraftServer.currentTick + immunity; ++ } else { ++ entity.isTemporarilyActive = true; + } ++ // Paper end + isActive = true; ++ + } + // Add a little performance juice to active entities. Skip 1/4 if not immune. +- } else if ( !entity.defaultActivationState && (entity.tickCount + entity.getId()) % 4 == 0 && !ActivationRange.checkEntityImmunities( entity ) ) // Paper - Ensure checking item movement is offset from Spigot's entity activation range check ++ } else if ( (entity.tickCount + entity.getId()) % 4 == 0 && ActivationRange.checkEntityImmunities( entity ) < 0 ) // Paper + { + isActive = false; + } +diff --git a/src/main/java/org/spigotmc/SpigotWorldConfig.java b/src/main/java/org/spigotmc/SpigotWorldConfig.java +index 5485df1a1b59e81f4dcedd21dd972e1fd2759573..1cf6d4f854d89c515e48e1fb365eb95ff9340765 100644 +--- a/src/main/java/org/spigotmc/SpigotWorldConfig.java ++++ b/src/main/java/org/spigotmc/SpigotWorldConfig.java +@@ -211,14 +211,60 @@ public class SpigotWorldConfig + public int monsterActivationRange = 32; + public int raiderActivationRange = 48; + public int miscActivationRange = 16; ++ // Paper start ++ public int flyingMonsterActivationRange = 32; ++ public int waterActivationRange = 16; ++ public int villagerActivationRange = 32; ++ public int wakeUpInactiveAnimals = 4; ++ public int wakeUpInactiveAnimalsEvery = 60*20; ++ public int wakeUpInactiveAnimalsFor = 5*20; ++ public int wakeUpInactiveMonsters = 8; ++ public int wakeUpInactiveMonstersEvery = 20*20; ++ public int wakeUpInactiveMonstersFor = 5*20; ++ public int wakeUpInactiveVillagers = 4; ++ public int wakeUpInactiveVillagersEvery = 30*20; ++ public int wakeUpInactiveVillagersFor = 5*20; ++ public int wakeUpInactiveFlying = 8; ++ public int wakeUpInactiveFlyingEvery = 10*20; ++ public int wakeUpInactiveFlyingFor = 5*20; ++ public int villagersWorkImmunityAfter = 5*20; ++ public int villagersWorkImmunityFor = 20; ++ public boolean villagersActiveForPanic = true; ++ // Paper end + public boolean tickInactiveVillagers = true; + public boolean ignoreSpectatorActivation = false; + private void activationRange() + { ++ boolean hasAnimalsConfig = config.getInt("entity-activation-range.animals", this.animalActivationRange) != this.animalActivationRange; // Paper + this.animalActivationRange = this.getInt( "entity-activation-range.animals", this.animalActivationRange ); + this.monsterActivationRange = this.getInt( "entity-activation-range.monsters", this.monsterActivationRange ); + this.raiderActivationRange = this.getInt( "entity-activation-range.raiders", this.raiderActivationRange ); + this.miscActivationRange = this.getInt( "entity-activation-range.misc", this.miscActivationRange ); ++ // Paper start ++ this.waterActivationRange = this.getInt( "entity-activation-range.water", this.waterActivationRange ); ++ this.villagerActivationRange = this.getInt( "entity-activation-range.villagers", hasAnimalsConfig ? this.animalActivationRange : this.villagerActivationRange ); ++ this.flyingMonsterActivationRange = this.getInt( "entity-activation-range.flying-monsters", this.flyingMonsterActivationRange ); ++ ++ this.wakeUpInactiveAnimals = this.getInt("entity-activation-range.wake-up-inactive.animals-max-per-tick", this.wakeUpInactiveAnimals); ++ this.wakeUpInactiveAnimalsEvery = this.getInt("entity-activation-range.wake-up-inactive.animals-every", this.wakeUpInactiveAnimalsEvery); ++ this.wakeUpInactiveAnimalsFor = this.getInt("entity-activation-range.wake-up-inactive.animals-for", this.wakeUpInactiveAnimalsFor); ++ ++ this.wakeUpInactiveMonsters = this.getInt("entity-activation-range.wake-up-inactive.monsters-max-per-tick", this.wakeUpInactiveMonsters); ++ this.wakeUpInactiveMonstersEvery = this.getInt("entity-activation-range.wake-up-inactive.monsters-every", this.wakeUpInactiveMonstersEvery); ++ this.wakeUpInactiveMonstersFor = this.getInt("entity-activation-range.wake-up-inactive.monsters-for", this.wakeUpInactiveMonstersFor); ++ ++ this.wakeUpInactiveVillagers = this.getInt("entity-activation-range.wake-up-inactive.villagers-max-per-tick", this.wakeUpInactiveVillagers); ++ this.wakeUpInactiveVillagersEvery = this.getInt("entity-activation-range.wake-up-inactive.villagers-every", this.wakeUpInactiveVillagersEvery); ++ this.wakeUpInactiveVillagersFor = this.getInt("entity-activation-range.wake-up-inactive.villagers-for", this.wakeUpInactiveVillagersFor); ++ ++ this.wakeUpInactiveFlying = this.getInt("entity-activation-range.wake-up-inactive.flying-monsters-max-per-tick", this.wakeUpInactiveFlying); ++ this.wakeUpInactiveFlyingEvery = this.getInt("entity-activation-range.wake-up-inactive.flying-monsters-every", this.wakeUpInactiveFlyingEvery); ++ this.wakeUpInactiveFlyingFor = this.getInt("entity-activation-range.wake-up-inactive.flying-monsters-for", this.wakeUpInactiveFlyingFor); ++ ++ this.villagersWorkImmunityAfter = this.getInt( "entity-activation-range.villagers-work-immunity-after", this.villagersWorkImmunityAfter ); ++ this.villagersWorkImmunityFor = this.getInt( "entity-activation-range.villagers-work-immunity-for", this.villagersWorkImmunityFor ); ++ this.villagersActiveForPanic = this.getBoolean( "entity-activation-range.villagers-active-for-panic", this.villagersActiveForPanic ); ++ // Paper end + this.tickInactiveVillagers = this.getBoolean( "entity-activation-range.tick-inactive-villagers", this.tickInactiveVillagers ); + this.ignoreSpectatorActivation = this.getBoolean( "entity-activation-range.ignore-spectators", this.ignoreSpectatorActivation ); + this.log( "Entity Activation Range: An " + this.animalActivationRange + " / Mo " + this.monsterActivationRange + " / Ra " + this.raiderActivationRange + " / Mi " + this.miscActivationRange + " / Tiv " + this.tickInactiveVillagers + " / Isa " + this.ignoreSpectatorActivation ); diff --git a/patches/server/0994-Flat-bedrock-generator-settings.patch b/patches/server/0994-Flat-bedrock-generator-settings.patch deleted file mode 100644 index 7103386726..0000000000 --- a/patches/server/0994-Flat-bedrock-generator-settings.patch +++ /dev/null @@ -1,292 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Byteflux -Date: Wed, 2 Mar 2016 02:17:54 -0600 -Subject: [PATCH] Flat bedrock generator settings - -== AT == -public net.minecraft.world.level.levelgen.SurfaceRules$Condition -public net.minecraft.world.level.levelgen.SurfaceRules$Context -public net.minecraft.world.level.levelgen.SurfaceRules$Context blockX -public net.minecraft.world.level.levelgen.SurfaceRules$Context blockY -public net.minecraft.world.level.levelgen.SurfaceRules$Context blockZ -public net.minecraft.world.level.levelgen.SurfaceRules$Context context -public net.minecraft.world.level.levelgen.SurfaceRules$Context randomState -public net.minecraft.world.level.levelgen.SurfaceRules$LazyYCondition -public net.minecraft.world.level.levelgen.SurfaceRules$LazyCondition -public net.minecraft.world.level.levelgen.SurfaceRules$VerticalGradientConditionSource -public net.minecraft.world.level.levelgen.SurfaceRules$SurfaceRule - -Co-authored-by: Noah van der Aa - -diff --git a/src/main/java/io/papermc/paper/world/worldgen/OptionallyFlatBedrockConditionSource.java b/src/main/java/io/papermc/paper/world/worldgen/OptionallyFlatBedrockConditionSource.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e0d73113c937ddbcf8144f88b15a8d3d080b90a0 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/world/worldgen/OptionallyFlatBedrockConditionSource.java -@@ -0,0 +1,80 @@ -+package io.papermc.paper.world.worldgen; -+ -+import com.mojang.serialization.Codec; -+import com.mojang.serialization.MapCodec; -+import com.mojang.serialization.codecs.RecordCodecBuilder; -+import net.minecraft.core.Registry; -+import net.minecraft.core.registries.BuiltInRegistries; -+import net.minecraft.core.registries.Registries; -+import net.minecraft.resources.ResourceKey; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.util.KeyDispatchDataCodec; -+import net.minecraft.util.Mth; -+import net.minecraft.util.RandomSource; -+import net.minecraft.world.level.levelgen.PositionalRandomFactory; -+import net.minecraft.world.level.levelgen.SurfaceRules; -+import net.minecraft.world.level.levelgen.VerticalAnchor; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.framework.qual.DefaultQualifier; -+ -+// Modelled off of SurfaceRules$VerticalGradientConditionSource -+@DefaultQualifier(NonNull.class) -+public record OptionallyFlatBedrockConditionSource(ResourceLocation randomName, VerticalAnchor trueAtAndBelow, VerticalAnchor falseAtAndAbove, boolean isRoof) implements SurfaceRules.ConditionSource { -+ -+ private static final ResourceKey> CODEC_RESOURCE_KEY = ResourceKey.create( -+ Registries.MATERIAL_CONDITION, -+ new ResourceLocation(ResourceLocation.PAPER_NAMESPACE, "optionally_flat_bedrock_condition_source") -+ ); -+ private static final KeyDispatchDataCodec CODEC = KeyDispatchDataCodec.of(RecordCodecBuilder.mapCodec((instance) -> { -+ return instance.group( -+ ResourceLocation.CODEC.fieldOf("random_name").forGetter(OptionallyFlatBedrockConditionSource::randomName), -+ VerticalAnchor.CODEC.fieldOf("true_at_and_below").forGetter(OptionallyFlatBedrockConditionSource::trueAtAndBelow), -+ VerticalAnchor.CODEC.fieldOf("false_at_and_above").forGetter(OptionallyFlatBedrockConditionSource::falseAtAndAbove), -+ Codec.BOOL.fieldOf("is_roof").forGetter(OptionallyFlatBedrockConditionSource::isRoof) -+ ).apply(instance, OptionallyFlatBedrockConditionSource::new); -+ })); -+ -+ public static void bootstrap() { -+ Registry.register(BuiltInRegistries.MATERIAL_CONDITION, CODEC_RESOURCE_KEY, CODEC.codec()); -+ } -+ -+ @Override -+ public KeyDispatchDataCodec codec() { -+ return CODEC; -+ } -+ -+ @Override -+ public SurfaceRules.Condition apply(final SurfaceRules.Context context) { -+ boolean hasFlatBedrock = context.context.getWorld().paperConfig().environment.generateFlatBedrock; -+ int tempTrueAtAndBelowY = this.trueAtAndBelow().resolveY(context.context); -+ int tempFalseAtAndAboveY = this.falseAtAndAbove().resolveY(context.context); -+ -+ int flatYLevel = this.isRoof ? Math.max(tempFalseAtAndAboveY, tempTrueAtAndBelowY) - 1 : Math.min(tempFalseAtAndAboveY, tempTrueAtAndBelowY); -+ final int trueAtAndBelowY = hasFlatBedrock ? flatYLevel : tempTrueAtAndBelowY; -+ final int falseAtAndAboveY = hasFlatBedrock ? flatYLevel : tempFalseAtAndAboveY; -+ -+ final PositionalRandomFactory positionalRandomFactory = context.randomState.getOrCreateRandomFactory(this.randomName()); -+ -+ class VerticalGradientCondition extends SurfaceRules.LazyYCondition { -+ VerticalGradientCondition(SurfaceRules.Context context) { -+ super(context); -+ } -+ -+ @Override -+ protected boolean compute() { -+ int blockY = this.context.blockY; -+ if (blockY <= trueAtAndBelowY) { -+ return true; -+ } else if (blockY >= falseAtAndAboveY) { -+ return false; -+ } else { -+ double d = Mth.map(blockY, trueAtAndBelowY, falseAtAndAboveY, 1.0D, 0.0D); -+ RandomSource randomSource = positionalRandomFactory.at(this.context.blockX, blockY, this.context.blockZ); -+ return (double)randomSource.nextFloat() < d; -+ } -+ } -+ } -+ -+ return new VerticalGradientCondition(context); -+ } -+} -diff --git a/src/main/java/net/minecraft/server/Bootstrap.java b/src/main/java/net/minecraft/server/Bootstrap.java -index 05f4be8dfff37ab6804a2d990b1f8c430d42dc7c..2a49a6a6bccabf588ba67b565d8f16fda04ba0b0 100644 ---- a/src/main/java/net/minecraft/server/Bootstrap.java -+++ b/src/main/java/net/minecraft/server/Bootstrap.java -@@ -76,6 +76,7 @@ public class Bootstrap { - CauldronInteraction.bootStrap(); - // Paper start - BuiltInRegistries.bootStrap(() -> { -+ io.papermc.paper.world.worldgen.OptionallyFlatBedrockConditionSource.bootstrap(); // Paper - Flat bedrock generator settings - io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler.enterBootstrappers(); // Paper - Entrypoint for bootstrapping - }); - // Paper end -diff --git a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java -index dc765b92cc90f5f370254e68bbbdfa5add7935ce..8ce870a5341a61fbbaf42021ef7f7f615a6a3e09 100644 ---- a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java -+++ b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java -@@ -207,7 +207,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator { - @Override - public void buildSurface(WorldGenRegion region, StructureManager structures, RandomState noiseConfig, ChunkAccess chunk) { - if (!SharedConstants.debugVoidTerrain(chunk.getPos())) { -- WorldGenerationContext worldgenerationcontext = new WorldGenerationContext(this, region); -+ WorldGenerationContext worldgenerationcontext = new WorldGenerationContext(this, region, region.getMinecraftWorld()); // Paper - Flat bedrock generator settings - - this.buildSurface(chunk, worldgenerationcontext, noiseConfig, structures, region.getBiomeManager(), region.registryAccess().registryOrThrow(Registries.BIOME), Blender.of(region)); - } -@@ -235,7 +235,7 @@ public final class NoiseBasedChunkGenerator extends ChunkGenerator { - return this.createNoiseChunk(ichunkaccess1, structureAccessor, Blender.of(chunkRegion), noiseConfig); - }); - Aquifer aquifer = noisechunk.aquifer(); -- CarvingContext carvingcontext = new CarvingContext(this, chunkRegion.registryAccess(), chunk.getHeightAccessorForGeneration(), noisechunk, noiseConfig, ((NoiseGeneratorSettings) this.settings.value()).surfaceRule()); -+ CarvingContext carvingcontext = new CarvingContext(this, chunkRegion.registryAccess(), chunk.getHeightAccessorForGeneration(), noisechunk, noiseConfig, ((NoiseGeneratorSettings) this.settings.value()).surfaceRule(), chunkRegion.getMinecraftWorld()); // Paper - Flat bedrock generator settings - CarvingMask carvingmask = ((ProtoChunk) chunk).getOrCreateCarvingMask(carverStep); - - for (int j = -8; j <= 8; ++j) { -diff --git a/src/main/java/net/minecraft/world/level/levelgen/WorldGenerationContext.java b/src/main/java/net/minecraft/world/level/levelgen/WorldGenerationContext.java -index b99283c31193e2110f6e3f39c23dbfc2442bab2b..a34e53249668d917c9d77c6837b91360a2349bbc 100644 ---- a/src/main/java/net/minecraft/world/level/levelgen/WorldGenerationContext.java -+++ b/src/main/java/net/minecraft/world/level/levelgen/WorldGenerationContext.java -@@ -6,10 +6,13 @@ import net.minecraft.world.level.chunk.ChunkGenerator; - public class WorldGenerationContext { - private final int minY; - private final int height; -+ private final @javax.annotation.Nullable net.minecraft.world.level.Level level; // Paper - Flat bedrock generator settings - -- public WorldGenerationContext(ChunkGenerator generator, LevelHeightAccessor world) { -+ public WorldGenerationContext(ChunkGenerator generator, LevelHeightAccessor world) { this(generator, world, null); } // Paper - Flat bedrock generator settings -+ public WorldGenerationContext(ChunkGenerator generator, LevelHeightAccessor world, @org.jetbrains.annotations.Nullable net.minecraft.world.level.Level level) { // Paper - Flat bedrock generator settings - this.minY = Math.max(world.getMinBuildHeight(), generator.getMinY()); - this.height = Math.min(world.getHeight(), generator.getGenDepth()); -+ this.level = level; // Paper - Flat bedrock generator settings - } - - public int getMinGenY() { -@@ -19,4 +22,13 @@ public class WorldGenerationContext { - public int getGenDepth() { - return this.height; - } -+ -+ // Paper start - Flat bedrock generator settings -+ public net.minecraft.world.level.Level getWorld() { -+ if (this.level == null) { -+ throw new NullPointerException("WorldGenerationContext was initialized without a Level, but WorldGenerationContext#getWorld was called"); -+ } -+ return this.level; -+ } -+ // Paper end - Flat bedrock generator settings - } -diff --git a/src/main/java/net/minecraft/world/level/levelgen/carver/CarvingContext.java b/src/main/java/net/minecraft/world/level/levelgen/carver/CarvingContext.java -index 390bcf9c302effd9db42d7a0e65b5433cc2eadd6..b9e919d31e442f49300744395af3cf9431e86c57 100644 ---- a/src/main/java/net/minecraft/world/level/levelgen/carver/CarvingContext.java -+++ b/src/main/java/net/minecraft/world/level/levelgen/carver/CarvingContext.java -@@ -27,9 +27,9 @@ public class CarvingContext extends WorldGenerationContext { - LevelHeightAccessor heightLimitView, - NoiseChunk chunkNoiseSampler, - RandomState noiseConfig, -- SurfaceRules.RuleSource materialRule -+ SurfaceRules.RuleSource materialRule, @javax.annotation.Nullable net.minecraft.world.level.Level level // Paper - Flat bedrock generator settings - ) { -- super(noiseChunkGenerator, heightLimitView); -+ super(noiseChunkGenerator, heightLimitView, level); // Paper - Flat bedrock generator settings - this.registryAccess = registryManager; - this.noiseChunk = chunkNoiseSampler; - this.randomState = noiseConfig; -diff --git a/src/main/java/net/minecraft/world/level/levelgen/placement/PlacementContext.java b/src/main/java/net/minecraft/world/level/levelgen/placement/PlacementContext.java -index 640c2683c842655bbaee8f293f1c2613ef44844e..c7dfd844c7846281ceff0d443c0160054fd36c5c 100644 ---- a/src/main/java/net/minecraft/world/level/levelgen/placement/PlacementContext.java -+++ b/src/main/java/net/minecraft/world/level/levelgen/placement/PlacementContext.java -@@ -18,7 +18,7 @@ public class PlacementContext extends WorldGenerationContext { - private final Optional topFeature; - - public PlacementContext(WorldGenLevel world, ChunkGenerator generator, Optional placedFeature) { -- super(generator, world); -+ super(generator, world, world.getLevel()); // Paper - Flat bedrock generator settings - this.level = world; - this.generator = generator; - this.topFeature = placedFeature; -diff --git a/src/main/resources/data/minecraft/worldgen/noise_settings/amplified.json b/src/main/resources/data/minecraft/worldgen/noise_settings/amplified.json -index 3f61ea695aa6a91a0cacf1fa8dc0a9bdc6fa36e6..02d32bbdbc795db271205bef95e6987ac34b0136 100644 ---- a/src/main/resources/data/minecraft/worldgen/noise_settings/amplified.json -+++ b/src/main/resources/data/minecraft/worldgen/noise_settings/amplified.json -@@ -389,7 +389,8 @@ - { - "type": "minecraft:condition", - "if_true": { -- "type": "minecraft:vertical_gradient", -+ "type": "paper:optionally_flat_bedrock_condition_source", -+ "is_roof": false, - "false_at_and_above": { - "above_bottom": 5 - }, -diff --git a/src/main/resources/data/minecraft/worldgen/noise_settings/caves.json b/src/main/resources/data/minecraft/worldgen/noise_settings/caves.json -index 1fe9ce904cba21ff4e6efb948a0bc274a22bcb0b..2e8c1aad10a2b0adbdee4180cfc1902984b6565b 100644 ---- a/src/main/resources/data/minecraft/worldgen/noise_settings/caves.json -+++ b/src/main/resources/data/minecraft/worldgen/noise_settings/caves.json -@@ -110,7 +110,8 @@ - "if_true": { - "type": "minecraft:not", - "invert": { -- "type": "minecraft:vertical_gradient", -+ "type": "paper:optionally_flat_bedrock_condition_source", -+ "is_roof": true, - "false_at_and_above": { - "below_top": 0 - }, -@@ -130,7 +131,8 @@ - { - "type": "minecraft:condition", - "if_true": { -- "type": "minecraft:vertical_gradient", -+ "type": "paper:optionally_flat_bedrock_condition_source", -+ "is_roof": false, - "false_at_and_above": { - "above_bottom": 5 - }, -diff --git a/src/main/resources/data/minecraft/worldgen/noise_settings/large_biomes.json b/src/main/resources/data/minecraft/worldgen/noise_settings/large_biomes.json -index f4c34de998d78a80bc026d8e0d423c9bed9bbf8a..fd5bd5474b8f931f2e04706997e71cd5145a5a82 100644 ---- a/src/main/resources/data/minecraft/worldgen/noise_settings/large_biomes.json -+++ b/src/main/resources/data/minecraft/worldgen/noise_settings/large_biomes.json -@@ -389,7 +389,8 @@ - { - "type": "minecraft:condition", - "if_true": { -- "type": "minecraft:vertical_gradient", -+ "type": "paper:optionally_flat_bedrock_condition_source", -+ "is_roof": false, - "false_at_and_above": { - "above_bottom": 5 - }, -diff --git a/src/main/resources/data/minecraft/worldgen/noise_settings/nether.json b/src/main/resources/data/minecraft/worldgen/noise_settings/nether.json -index 6219d25fcdbc761debc1d1e357757d1b977dc0b0..1657179b8c3f7e549e3b8774ecb75f292ae79c38 100644 ---- a/src/main/resources/data/minecraft/worldgen/noise_settings/nether.json -+++ b/src/main/resources/data/minecraft/worldgen/noise_settings/nether.json -@@ -108,7 +108,8 @@ - { - "type": "minecraft:condition", - "if_true": { -- "type": "minecraft:vertical_gradient", -+ "type": "paper:optionally_flat_bedrock_condition_source", -+ "is_roof": false, - "false_at_and_above": { - "above_bottom": 5 - }, -@@ -129,7 +130,8 @@ - "if_true": { - "type": "minecraft:not", - "invert": { -- "type": "minecraft:vertical_gradient", -+ "type": "paper:optionally_flat_bedrock_condition_source", -+ "is_roof": true, - "false_at_and_above": { - "below_top": 0 - }, -diff --git a/src/main/resources/data/minecraft/worldgen/noise_settings/overworld.json b/src/main/resources/data/minecraft/worldgen/noise_settings/overworld.json -index da3bda6167859f4ddf7d76ec2053f40b2139c13e..70beb7665a43a06b4afcb3c77aa77f923cb444cb 100644 ---- a/src/main/resources/data/minecraft/worldgen/noise_settings/overworld.json -+++ b/src/main/resources/data/minecraft/worldgen/noise_settings/overworld.json -@@ -389,7 +389,8 @@ - { - "type": "minecraft:condition", - "if_true": { -- "type": "minecraft:vertical_gradient", -+ "type": "paper:optionally_flat_bedrock_condition_source", -+ "is_roof": false, - "false_at_and_above": { - "above_bottom": 5 - }, diff --git a/patches/server/0995-Entity-Activation-Range-2.0.patch b/patches/server/0995-Entity-Activation-Range-2.0.patch deleted file mode 100644 index fee8d41d7e..0000000000 --- a/patches/server/0995-Entity-Activation-Range-2.0.patch +++ /dev/null @@ -1,812 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Fri, 13 May 2016 01:38:06 -0400 -Subject: [PATCH] Entity Activation Range 2.0 - -Optimizes performance of Activation Range - -Adds many new configurations and a new wake up inactive system - -Fixes and adds new Immunities to improve gameplay behavior - -Adds water Mobs to activation range config and nerfs fish -Adds flying monsters to control ghast and phantoms -Adds villagers as separate config - -== AT == -public net.minecraft.world.entity.Entity isInsidePortal - -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 5285cc76b001a8b5fd5e11764a3aa11e45c9d999..b7ca39623348090c5b85e2d80f67a1747553d7d7 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -2,7 +2,6 @@ package net.minecraft.server.level; - - import com.google.common.annotations.VisibleForTesting; - import co.aikar.timings.TimingHistory; // Paper --import co.aikar.timings.Timings; // Paper - import com.google.common.collect.Lists; - import com.mojang.datafixers.DataFixer; - import com.mojang.datafixers.util.Pair; -@@ -1234,17 +1233,17 @@ public class ServerLevel extends Level implements WorldGenLevel { - ++TimingHistory.entityTicks; // Paper - timings - // Spigot start - co.aikar.timings.Timing timer; // Paper -- if (!org.spigotmc.ActivationRange.checkIfActive(entity)) { -+ /*if (!org.spigotmc.ActivationRange.checkIfActive(entity)) { // Paper - comment out - EAR 2, reimplement below - entity.tickCount++; - timer = entity.getType().inactiveTickTimer.startTiming(); try { // Paper - timings - entity.inactiveTick(); - } finally { timer.stopTiming(); } // Paper - return; -- } -+ }*/ // Paper - comment out EAR 2 - // Spigot end - // Paper start- timings -- TimingHistory.activatedEntityTicks++; -- timer = entity.getVehicle() != null ? entity.getType().passengerTickTimer.startTiming() : entity.getType().tickTimer.startTiming(); -+ final boolean isActive = org.spigotmc.ActivationRange.checkIfActive(entity); -+ timer = isActive ? entity.getType().tickTimer.startTiming() : entity.getType().inactiveTickTimer.startTiming(); // Paper - try { - // Paper end - timings - entity.setOldPosAndRot(); -@@ -1255,9 +1254,13 @@ public class ServerLevel extends Level implements WorldGenLevel { - return BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString(); - }); - gameprofilerfiller.incrementCounter("tickNonPassenger"); -+ if (isActive) { // Paper - EAR 2 -+ TimingHistory.activatedEntityTicks++; - entity.tick(); - entity.postTick(); // CraftBukkit -+ } else { entity.inactiveTick(); } // Paper - EAR 2 - this.getProfiler().pop(); -+ } finally { timer.stopTiming(); } // Paper - timings - Iterator iterator = entity.getPassengers().iterator(); - - while (iterator.hasNext()) { -@@ -1265,13 +1268,18 @@ public class ServerLevel extends Level implements WorldGenLevel { - - this.tickPassenger(entity, entity1); - } -- } finally { timer.stopTiming(); } // Paper - timings -+ // } finally { timer.stopTiming(); } // Paper - timings - move up - - } - - private void tickPassenger(Entity vehicle, Entity passenger) { - if (!passenger.isRemoved() && passenger.getVehicle() == vehicle) { - if (passenger instanceof Player || this.entityTickList.contains(passenger)) { -+ // Paper - EAR 2 -+ final boolean isActive = org.spigotmc.ActivationRange.checkIfActive(passenger); -+ co.aikar.timings.Timing timer = isActive ? passenger.getType().passengerTickTimer.startTiming() : passenger.getType().passengerInactiveTickTimer.startTiming(); // Paper -+ try { -+ // Paper end - passenger.setOldPosAndRot(); - ++passenger.tickCount; - ProfilerFiller gameprofilerfiller = this.getProfiler(); -@@ -1280,8 +1288,17 @@ public class ServerLevel extends Level implements WorldGenLevel { - return BuiltInRegistries.ENTITY_TYPE.getKey(passenger.getType()).toString(); - }); - gameprofilerfiller.incrementCounter("tickPassenger"); -+ // Paper start - EAR 2 -+ if (isActive) { - passenger.rideTick(); - passenger.postTick(); // CraftBukkit -+ } else { -+ passenger.setDeltaMovement(Vec3.ZERO); -+ passenger.inactiveTick(); -+ // copied from inside of if (isPassenger()) of passengerTick, but that ifPassenger is unnecessary -+ vehicle.positionRider(passenger); -+ } -+ // Paper end - EAR 2 - gameprofilerfiller.pop(); - Iterator iterator = passenger.getPassengers().iterator(); - -@@ -1291,6 +1308,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - this.tickPassenger(passenger, entity2); - } - -+ } finally { timer.stopTiming(); }// Paper - EAR2 timings - } - } else { - passenger.stopRiding(); -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 03840f520624662d4ce3ac9f3065a01c71b5f299..a13edd1165a5ba4dd3f5c323e454926e7fe75c07 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -414,6 +414,8 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - // Spigot end - protected int numCollisions = 0; // Paper - Cap entity collisions - public boolean fromNetherPortal; // Paper - Add option to nerf pigmen from nether portals -+ public long activatedImmunityTick = Integer.MIN_VALUE; // Paper - EAR -+ public boolean isTemporarilyActive; // Paper - EAR - public boolean spawnedViaMobSpawner; // Paper - Yes this name is similar to above, upstream took the better one - // Paper start - Entity origin API - @javax.annotation.Nullable -@@ -1045,6 +1047,8 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } else { - this.wasOnFire = this.isOnFire(); - if (movementType == MoverType.PISTON) { -+ this.activatedTick = Math.max(this.activatedTick, MinecraftServer.currentTick + 20); // Paper -+ this.activatedImmunityTick = Math.max(this.activatedImmunityTick, MinecraftServer.currentTick + 20); // Paper - movement = this.limitPistonMovement(movement); - if (movement.equals(Vec3.ZERO)) { - return; -@@ -1057,6 +1061,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - this.stuckSpeedMultiplier = Vec3.ZERO; - this.setDeltaMovement(Vec3.ZERO); - } -+ // Paper start - ignore movement changes while inactive. -+ if (isTemporarilyActive && !(this instanceof ItemEntity) && movement == getDeltaMovement() && movementType == MoverType.SELF) { -+ setDeltaMovement(Vec3.ZERO); -+ this.level.getProfiler().pop(); -+ return; -+ } -+ // Paper end - - movement = this.maybeBackOffFromEdge(movement, movementType); - Vec3 vec3d1 = this.collide(movement); -diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java -index 0fd307e214f544284606ac8aa57f3f6bdca8bca6..eb2fd14f175a4ffca920ca116fb8d0d381286dc5 100644 ---- a/src/main/java/net/minecraft/world/entity/Mob.java -+++ b/src/main/java/net/minecraft/world/entity/Mob.java -@@ -237,6 +237,19 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Targeti - return this.lookControl; - } - -+ // Paper start -+ @Override -+ public void inactiveTick() { -+ super.inactiveTick(); -+ if (this.goalSelector.inactiveTick()) { -+ this.goalSelector.tick(); -+ } -+ if (this.targetSelector.inactiveTick()) { -+ this.targetSelector.tick(); -+ } -+ } -+ // Paper end -+ - public MoveControl getMoveControl() { - Entity entity = this.getControlledVehicle(); - -diff --git a/src/main/java/net/minecraft/world/entity/PathfinderMob.java b/src/main/java/net/minecraft/world/entity/PathfinderMob.java -index cdd07093342521ff9944bf7a342bbf142ba3f0b7..645414720db0231a19c88f681608eb08200b8782 100644 ---- a/src/main/java/net/minecraft/world/entity/PathfinderMob.java -+++ b/src/main/java/net/minecraft/world/entity/PathfinderMob.java -@@ -23,6 +23,7 @@ public abstract class PathfinderMob extends Mob { - } - - public org.bukkit.craftbukkit.entity.CraftCreature getBukkitCreature() { return (org.bukkit.craftbukkit.entity.CraftCreature) super.getBukkitEntity(); } // Paper -+ public BlockPos movingTarget = null; public BlockPos getMovingTarget() { return movingTarget; } // Paper - - public float getWalkTargetValue(BlockPos pos) { - return this.getWalkTargetValue(pos, this.level()); -diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java -index 89b8a304fe9fae4b57640afbab04b6764ce9aab8..074ef807258139f818e30494126585262c2f33c0 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java -+++ b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java -@@ -26,6 +26,7 @@ public class GoalSelector { - private final Set availableGoals = new ObjectLinkedOpenHashSet<>(); - private final Supplier profiler; - private final EnumSet disabledFlags = EnumSet.noneOf(Goal.Flag.class); -+ private int curRate; - - public GoalSelector(Supplier profiler) { - this.profiler = profiler; -@@ -40,6 +41,20 @@ public class GoalSelector { - this.availableGoals.removeIf(goal -> predicate.test(goal.getGoal())); - } - -+ // Paper start -+ public boolean inactiveTick() { -+ this.curRate++; -+ return this.curRate % 3 == 0; // TODO newGoalRate was already unused in 1.20.4, check if this is correct -+ } -+ public boolean hasTasks() { -+ for (WrappedGoal task : this.availableGoals) { -+ if (task.isRunning()) { -+ return true; -+ } -+ } -+ return false; -+ } -+ // Paper end - public void removeGoal(Goal goal) { - for (WrappedGoal wrappedGoal : this.availableGoals) { - if (wrappedGoal.getGoal() == goal && wrappedGoal.isRunning()) { -diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java -index 6d8ea05e5e86e9f6359b560043bb55a10784e952..aee0147649d458b87d92496eda0c1723ebe570d2 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java -+++ b/src/main/java/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java -@@ -23,6 +23,14 @@ public abstract class MoveToBlockGoal extends Goal { - public MoveToBlockGoal(PathfinderMob mob, double speed, int range) { - this(mob, speed, range, 1); - } -+ // Paper start - activation range improvements -+ @Override -+ public void stop() { -+ super.stop(); -+ this.blockPos = BlockPos.ZERO; -+ this.mob.movingTarget = null; -+ } -+ // Paper end - - public MoveToBlockGoal(PathfinderMob mob, double speed, int range, int maxYDifference) { - this.mob = mob; -@@ -115,6 +123,7 @@ public abstract class MoveToBlockGoal extends Goal { - mutableBlockPos.setWithOffset(blockPos, m, k - 1, n); - if (this.mob.isWithinRestriction(mutableBlockPos) && this.isValidTarget(this.mob.level(), mutableBlockPos)) { - this.blockPos = mutableBlockPos; -+ this.mob.movingTarget = mutableBlockPos == BlockPos.ZERO ? null : mutableBlockPos.immutable(); // Paper - return true; - } - } -diff --git a/src/main/java/net/minecraft/world/entity/npc/Villager.java b/src/main/java/net/minecraft/world/entity/npc/Villager.java -index 1e8579c994098fb18d9725e3f1604a582be4162f..a7930f9875aa4aca997caaead46ecdc21e5e11d7 100644 ---- a/src/main/java/net/minecraft/world/entity/npc/Villager.java -+++ b/src/main/java/net/minecraft/world/entity/npc/Villager.java -@@ -228,17 +228,34 @@ public class Villager extends AbstractVillager implements ReputationEventHandler - @Override - public void inactiveTick() { - // SPIGOT-3874, SPIGOT-3894, SPIGOT-3846, SPIGOT-5286 :( -- if (this.level().spigotConfig.tickInactiveVillagers && this.isEffectiveAi()) { -- this.customServerAiStep(); -+ // Paper start -+ if (this.getUnhappyCounter() > 0) { -+ this.setUnhappyCounter(this.getUnhappyCounter() - 1); - } -+ if (this.isEffectiveAi()) { -+ if (this.level().spigotConfig.tickInactiveVillagers) { -+ this.customServerAiStep(); -+ } else { -+ this.customServerAiStep(true); -+ } -+ } -+ maybeDecayGossip(); -+ // Paper end -+ - super.inactiveTick(); - } - // Spigot End - - @Override -+ @Deprecated // Paper - protected void customServerAiStep() { -+ // Paper start -+ this.customServerAiStep(false); -+ } -+ protected void customServerAiStep(final boolean inactive) { -+ // Paper end - this.level().getProfiler().push("villagerBrain"); -- this.getBrain().tick((ServerLevel) this.level(), this); -+ if (!inactive) this.getBrain().tick((ServerLevel) this.level(), this); // Paper - this.level().getProfiler().pop(); - if (this.assignProfessionWhenSpawned) { - this.assignProfessionWhenSpawned = false; -@@ -262,7 +279,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler - this.lastTradedPlayer = null; - } - -- if (!this.isNoAi() && this.random.nextInt(100) == 0) { -+ if (!inactive && !this.isNoAi() && this.random.nextInt(100) == 0) { // Paper - Raid raid = ((ServerLevel) this.level()).getRaidAt(this.blockPosition()); - - if (raid != null && raid.isActive() && !raid.isOver()) { -@@ -273,6 +290,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler - if (this.getVillagerData().getProfession() == VillagerProfession.NONE && this.isTrading()) { - this.stopTrading(); - } -+ if (inactive) return; // Paper - - super.customServerAiStep(); - } -diff --git a/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java -index 0b7f52021441d633c37543e8ae485e81c292b747..d7f8464bf3eed0e42a5fc7f14a5b243d171f8b5e 100644 ---- a/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java -+++ b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java -@@ -52,6 +52,7 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper - if (bl != this.isEnabled()) { - this.setEnabled(bl); - } -+ this.immunize(); // Paper - } - - public boolean isEnabled() { -@@ -92,11 +93,13 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper - - public boolean suckInItems() { - if (HopperBlockEntity.suckInItems(this.level(), this)) { -+ this.immunize(); // Paper - return true; - } else { - for (ItemEntity itemEntity : this.level() - .getEntitiesOfClass(ItemEntity.class, this.getBoundingBox().inflate(0.25, 0.0, 0.25), EntitySelector.ENTITY_STILL_ALIVE)) { - if (HopperBlockEntity.addItem(this, itemEntity)) { -+ this.immunize(); // Paper - return true; - } - } -@@ -126,4 +129,11 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper - public AbstractContainerMenu createMenu(int syncId, Inventory playerInventory) { - return new HopperMenu(syncId, playerInventory, this); - } -+ -+ // Paper start -+ public void immunize() { -+ this.activatedImmunityTick = Math.max(this.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 20); -+ } -+ // Paper end -+ - } -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index 12ab7097562d49dc466737378046707dc1b36b3b..2d829eb4d39aec82c23a55f30f22a9a5f7003d27 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -154,6 +154,12 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - public Map capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper - Retain block place order when capturing blockstates - public List captureDrops; - public final it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap ticksPerSpawnCategory = new it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<>(); -+ // Paper start -+ public int wakeupInactiveRemainingAnimals; -+ public int wakeupInactiveRemainingFlying; -+ public int wakeupInactiveRemainingMonsters; -+ public int wakeupInactiveRemainingVillagers; -+ // Paper end - public boolean populating; - public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot - // Paper start - add paper world config -diff --git a/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java b/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java -index d7b963571c900f0f68005d6954bcd9ef1d9e0b7c..b35f476e26a020cf75e53a5eb488717d996a6935 100644 ---- a/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java -@@ -148,6 +148,10 @@ public class PistonMovingBlockEntity extends BlockEntity { - } - - entity.setDeltaMovement(e, g, h); -+ // Paper - EAR items stuck in in slime pushed by a piston -+ entity.activatedTick = Math.max(entity.activatedTick, net.minecraft.server.MinecraftServer.currentTick + 10); -+ entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 10); -+ // Paper end - break; - } - } -diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java -index 9fb9fa62c32445ac3c3883a6433759c86dcfc428..3283ed99c35ffed6805567705e0518d9f84feedc 100644 ---- a/src/main/java/org/spigotmc/ActivationRange.java -+++ b/src/main/java/org/spigotmc/ActivationRange.java -@@ -1,33 +1,43 @@ - package org.spigotmc; - -+import net.minecraft.core.BlockPos; - import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ServerChunkCache; - import net.minecraft.world.entity.Entity; - import net.minecraft.world.entity.ExperienceOrb; -+import net.minecraft.world.entity.FlyingMob; - import net.minecraft.world.entity.LightningBolt; - import net.minecraft.world.entity.LivingEntity; -+import net.minecraft.world.entity.Mob; - import net.minecraft.world.entity.PathfinderMob; -+import net.minecraft.world.entity.ai.Brain; - import net.minecraft.world.entity.ambient.AmbientCreature; - import net.minecraft.world.entity.animal.Animal; -+import net.minecraft.world.entity.animal.Bee; - import net.minecraft.world.entity.animal.Sheep; -+import net.minecraft.world.entity.animal.WaterAnimal; -+import net.minecraft.world.entity.animal.horse.Llama; - import net.minecraft.world.entity.boss.EnderDragonPart; - import net.minecraft.world.entity.boss.enderdragon.EndCrystal; - import net.minecraft.world.entity.boss.enderdragon.EnderDragon; - import net.minecraft.world.entity.boss.wither.WitherBoss; - import net.minecraft.world.entity.item.PrimedTnt; - import net.minecraft.world.entity.monster.Creeper; --import net.minecraft.world.entity.monster.Monster; --import net.minecraft.world.entity.monster.Slime; -+import net.minecraft.world.entity.monster.Enemy; -+import net.minecraft.world.entity.monster.Pillager; - import net.minecraft.world.entity.npc.Villager; - import net.minecraft.world.entity.player.Player; - import net.minecraft.world.entity.projectile.AbstractArrow; - import net.minecraft.world.entity.projectile.AbstractHurtingProjectile; -+import net.minecraft.world.entity.projectile.EyeOfEnder; - import net.minecraft.world.entity.projectile.FireworkRocketEntity; - import net.minecraft.world.entity.projectile.ThrowableProjectile; - import net.minecraft.world.entity.projectile.ThrownTrident; - import net.minecraft.world.entity.raid.Raider; -+import co.aikar.timings.MinecraftTimings; -+import net.minecraft.world.entity.schedule.Activity; - import net.minecraft.world.level.Level; - import net.minecraft.world.phys.AABB; --import co.aikar.timings.MinecraftTimings; - - public class ActivationRange - { -@@ -44,6 +54,43 @@ public class ActivationRange - - AABB boundingBox = new AABB( 0, 0, 0, 0, 0, 0 ); - } -+ // Paper start -+ -+ static Activity[] VILLAGER_PANIC_IMMUNITIES = { -+ Activity.HIDE, -+ Activity.PRE_RAID, -+ Activity.RAID, -+ Activity.PANIC -+ }; -+ -+ private static int checkInactiveWakeup(Entity entity) { -+ Level world = entity.level(); -+ SpigotWorldConfig config = world.spigotConfig; -+ long inactiveFor = MinecraftServer.currentTick - entity.activatedTick; -+ if (entity.activationType == ActivationType.VILLAGER) { -+ if (inactiveFor > config.wakeUpInactiveVillagersEvery && world.wakeupInactiveRemainingVillagers > 0) { -+ world.wakeupInactiveRemainingVillagers--; -+ return config.wakeUpInactiveVillagersFor; -+ } -+ } else if (entity.activationType == ActivationType.ANIMAL) { -+ if (inactiveFor > config.wakeUpInactiveAnimalsEvery && world.wakeupInactiveRemainingAnimals > 0) { -+ world.wakeupInactiveRemainingAnimals--; -+ return config.wakeUpInactiveAnimalsFor; -+ } -+ } else if (entity.activationType == ActivationType.FLYING_MONSTER) { -+ if (inactiveFor > config.wakeUpInactiveFlyingEvery && world.wakeupInactiveRemainingFlying > 0) { -+ world.wakeupInactiveRemainingFlying--; -+ return config.wakeUpInactiveFlyingFor; -+ } -+ } else if (entity.activationType == ActivationType.MONSTER || entity.activationType == ActivationType.RAIDER) { -+ if (inactiveFor > config.wakeUpInactiveMonstersEvery && world.wakeupInactiveRemainingMonsters > 0) { -+ world.wakeupInactiveRemainingMonsters--; -+ return config.wakeUpInactiveMonstersFor; -+ } -+ } -+ return -1; -+ } -+ // Paper end - - static AABB maxBB = new AABB( 0, 0, 0, 0, 0, 0 ); - -@@ -56,10 +103,13 @@ public class ActivationRange - */ - public static ActivationType initializeEntityActivationType(Entity entity) - { -+ if (entity instanceof WaterAnimal) { return ActivationType.WATER; } // Paper -+ else if (entity instanceof Villager) { return ActivationType.VILLAGER; } // Paper -+ else if (entity instanceof FlyingMob && entity instanceof Enemy) { return ActivationType.FLYING_MONSTER; } // Paper - doing & Monster incase Flying no longer includes monster in future - if ( entity instanceof Raider ) - { - return ActivationType.RAIDER; -- } else if ( entity instanceof Monster || entity instanceof Slime ) -+ } else if ( entity instanceof Enemy ) // Paper - correct monster check - { - return ActivationType.MONSTER; - } else if ( entity instanceof PathfinderMob || entity instanceof AmbientCreature ) -@@ -80,10 +130,14 @@ public class ActivationRange - */ - public static boolean initializeEntityActivationState(Entity entity, SpigotWorldConfig config) - { -- if ( ( entity.activationType == ActivationType.MISC && config.miscActivationRange == 0 ) -- || ( entity.activationType == ActivationType.RAIDER && config.raiderActivationRange == 0 ) -- || ( entity.activationType == ActivationType.ANIMAL && config.animalActivationRange == 0 ) -- || ( entity.activationType == ActivationType.MONSTER && config.monsterActivationRange == 0 ) -+ if ( ( entity.activationType == ActivationType.MISC && config.miscActivationRange <= 0 ) -+ || ( entity.activationType == ActivationType.RAIDER && config.raiderActivationRange <= 0 ) -+ || ( entity.activationType == ActivationType.ANIMAL && config.animalActivationRange <= 0 ) -+ || ( entity.activationType == ActivationType.MONSTER && config.monsterActivationRange <= 0 ) -+ || ( entity.activationType == ActivationType.VILLAGER && config.villagerActivationRange <= 0 ) // Paper -+ || ( entity.activationType == ActivationType.WATER && config.waterActivationRange <= 0 ) // Paper -+ || ( entity.activationType == ActivationType.FLYING_MONSTER && config.flyingMonsterActivationRange <= 0 ) // Paper -+ || entity instanceof EyeOfEnder // Paper - || entity instanceof Player - || entity instanceof ThrowableProjectile - || entity instanceof EnderDragon -@@ -118,10 +172,25 @@ public class ActivationRange - final int raiderActivationRange = world.spigotConfig.raiderActivationRange; - final int animalActivationRange = world.spigotConfig.animalActivationRange; - final int monsterActivationRange = world.spigotConfig.monsterActivationRange; -+ // Paper start -+ final int waterActivationRange = world.spigotConfig.waterActivationRange; -+ final int flyingActivationRange = world.spigotConfig.flyingMonsterActivationRange; -+ final int villagerActivationRange = world.spigotConfig.villagerActivationRange; -+ world.wakeupInactiveRemainingAnimals = Math.min(world.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals); -+ world.wakeupInactiveRemainingVillagers = Math.min(world.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers); -+ world.wakeupInactiveRemainingMonsters = Math.min(world.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters); -+ world.wakeupInactiveRemainingFlying = Math.min(world.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying); -+ final ServerChunkCache chunkProvider = (ServerChunkCache) world.getChunkSource(); -+ // Paper end - - int maxRange = Math.max( monsterActivationRange, animalActivationRange ); - maxRange = Math.max( maxRange, raiderActivationRange ); - maxRange = Math.max( maxRange, miscActivationRange ); -+ // Paper start -+ maxRange = Math.max( maxRange, flyingActivationRange ); -+ maxRange = Math.max( maxRange, waterActivationRange ); -+ maxRange = Math.max( maxRange, villagerActivationRange ); -+ // Paper end - maxRange = Math.min( ( world.spigotConfig.simulationDistance << 4 ) - 8, maxRange ); - - for ( Player player : world.players() ) -@@ -132,13 +201,30 @@ public class ActivationRange - continue; - } - -- ActivationRange.maxBB = player.getBoundingBox().inflate( maxRange, 256, maxRange ); -- ActivationType.MISC.boundingBox = player.getBoundingBox().inflate( miscActivationRange, 256, miscActivationRange ); -- ActivationType.RAIDER.boundingBox = player.getBoundingBox().inflate( raiderActivationRange, 256, raiderActivationRange ); -- ActivationType.ANIMAL.boundingBox = player.getBoundingBox().inflate( animalActivationRange, 256, animalActivationRange ); -- ActivationType.MONSTER.boundingBox = player.getBoundingBox().inflate( monsterActivationRange, 256, monsterActivationRange ); -+ // Paper start -+ int worldHeight = world.getHeight(); -+ ActivationRange.maxBB = player.getBoundingBox().inflate( maxRange, worldHeight, maxRange ); -+ ActivationType.MISC.boundingBox = player.getBoundingBox().inflate( miscActivationRange, worldHeight, miscActivationRange ); -+ ActivationType.RAIDER.boundingBox = player.getBoundingBox().inflate( raiderActivationRange, worldHeight, raiderActivationRange ); -+ ActivationType.ANIMAL.boundingBox = player.getBoundingBox().inflate( animalActivationRange, worldHeight, animalActivationRange ); -+ ActivationType.MONSTER.boundingBox = player.getBoundingBox().inflate( monsterActivationRange, worldHeight, monsterActivationRange ); -+ ActivationType.WATER.boundingBox = player.getBoundingBox().inflate( waterActivationRange, worldHeight, waterActivationRange ); -+ ActivationType.FLYING_MONSTER.boundingBox = player.getBoundingBox().inflate( flyingActivationRange, worldHeight, flyingActivationRange ); -+ ActivationType.VILLAGER.boundingBox = player.getBoundingBox().inflate( villagerActivationRange, worldHeight, villagerActivationRange ); -+ // Paper end - -- world.getEntities().get(ActivationRange.maxBB, ActivationRange::activateEntity); -+ // Paper start -+ java.util.List entities = world.getEntities((Entity)null, ActivationRange.maxBB, null); -+ boolean tickMarkers = world.paperConfig().entities.markers.tick; // Paper - Configurable marker ticking -+ for (Entity entity : entities) { -+ // Paper start - Configurable marker ticking -+ if (!tickMarkers && entity instanceof net.minecraft.world.entity.Marker) { -+ continue; -+ } -+ // Paper end - Configurable marker ticking -+ ActivationRange.activateEntity(entity); -+ } -+ // Paper end - } - MinecraftTimings.entityActivationCheckTimer.stopTiming(); - } -@@ -171,60 +257,118 @@ public class ActivationRange - * @param entity - * @return - */ -- public static boolean checkEntityImmunities(Entity entity) -+ public static int checkEntityImmunities(Entity entity) // Paper - return # of ticks to get immunity - { -+ // Paper start -+ SpigotWorldConfig config = entity.level().spigotConfig; -+ int inactiveWakeUpImmunity = checkInactiveWakeup(entity); -+ if (inactiveWakeUpImmunity > -1) { -+ return inactiveWakeUpImmunity; -+ } -+ if (entity.getRemainingFireTicks() > 0) { -+ return 2; -+ } -+ if (entity.activatedImmunityTick >= MinecraftServer.currentTick) { -+ return 1; -+ } -+ long inactiveFor = MinecraftServer.currentTick - entity.activatedTick; -+ // Paper end - // quick checks. -- if ( entity.wasTouchingWater || entity.getRemainingFireTicks() > 0 ) -+ if ( (entity.activationType != ActivationType.WATER && entity.wasTouchingWater && entity.isPushedByFluid()) ) // Paper - { -- return true; -+ return 100; // Paper -+ } -+ // Paper start -+ if ( !entity.onGround() || entity.getDeltaMovement().horizontalDistanceSqr() > 9.999999747378752E-6D ) -+ { -+ return 100; - } -+ // Paper end - if ( !( entity instanceof AbstractArrow ) ) - { -- if ( !entity.onGround() || !entity.passengers.isEmpty() || entity.isPassenger() ) -+ if ( (!entity.onGround() && !(entity instanceof FlyingMob)) ) // Paper - remove passengers logic - { -- return true; -+ return 10; // Paper - } - } else if ( !( (AbstractArrow) entity ).inGround ) - { -- return true; -+ return 1; // Paper - } - // special cases. - if ( entity instanceof LivingEntity ) - { - LivingEntity living = (LivingEntity) entity; -- if ( /*TODO: Missed mapping? living.attackTicks > 0 || */ living.hurtTime > 0 || living.activeEffects.size() > 0 ) -+ if ( living.onClimbable() || living.jumping || living.hurtTime > 0 || living.activeEffects.size() > 0 || living.isFreezing()) // Paper - { -- return true; -+ return 1; // Paper - } -- if ( entity instanceof PathfinderMob && ( (PathfinderMob) entity ).getTarget() != null ) -+ if ( entity instanceof Mob && ((Mob) entity ).getTarget() != null) // Paper - { -- return true; -+ return 20; // Paper -+ } -+ // Paper start -+ if (entity instanceof Bee) { -+ Bee bee = (Bee)entity; -+ BlockPos movingTarget = bee.getMovingTarget(); -+ if (bee.isAngry() || -+ (bee.getHivePos() != null && bee.getHivePos().equals(movingTarget)) || -+ (bee.getSavedFlowerPos() != null && bee.getSavedFlowerPos().equals(movingTarget)) -+ ) { -+ return 20; -+ } - } -- if ( entity instanceof Villager && ( (Villager) entity ).canBreed() ) -+ if ( entity instanceof Villager ) { -+ Brain behaviorController = ((Villager) entity).getBrain(); -+ -+ if (config.villagersActiveForPanic) { -+ for (Activity activity : VILLAGER_PANIC_IMMUNITIES) { -+ if (behaviorController.isActive(activity)) { -+ return 20*5; -+ } -+ } -+ } -+ -+ if (config.villagersWorkImmunityAfter > 0 && inactiveFor >= config.villagersWorkImmunityAfter) { -+ if (behaviorController.isActive(Activity.WORK)) { -+ return config.villagersWorkImmunityFor; -+ } -+ } -+ } -+ if ( entity instanceof Llama && ( (Llama) entity ).inCaravan() ) - { -- return true; -+ return 1; - } -+ // Paper end - if ( entity instanceof Animal ) - { - Animal animal = (Animal) entity; - if ( animal.isBaby() || animal.isInLove() ) - { -- return true; -+ return 5; // Paper - } - if ( entity instanceof Sheep && ( (Sheep) entity ).isSheared() ) - { -- return true; -+ return 1; // Paper - } - } - if (entity instanceof Creeper && ((Creeper) entity).isIgnited()) { // isExplosive -- return true; -+ return 20; // Paper -+ } -+ // Paper start -+ if (entity instanceof Mob && ((Mob) entity).targetSelector.hasTasks() ) { -+ return 0; - } -+ if (entity instanceof Pillager) { -+ Pillager pillager = (Pillager) entity; -+ // TODO:? -+ } -+ // Paper end - } - // SPIGOT-6644: Otherwise the target refresh tick will be missed - if (entity instanceof ExperienceOrb) { -- return true; -+ return 20; // Paper - } -- return false; -+ return -1; // Paper - } - - /** -@@ -239,8 +383,19 @@ public class ActivationRange - if ( entity instanceof FireworkRocketEntity ) { - return true; - } -+ // Paper start - special case always immunities -+ // immunize brand new entities, dead entities, and portal scenarios -+ if (entity.defaultActivationState || entity.tickCount < 20*10 || !entity.isAlive() || entity.isInsidePortal || entity.portalCooldown > 0) { -+ return true; -+ } -+ // immunize leashed entities -+ if (entity instanceof Mob && ((Mob)entity).getLeashHolder() instanceof Player) { -+ return true; -+ } -+ // Paper end - -- boolean isActive = entity.activatedTick >= MinecraftServer.currentTick || entity.defaultActivationState; -+ boolean isActive = entity.activatedTick >= MinecraftServer.currentTick; -+ entity.isTemporarilyActive = false; // Paper - - // Should this entity tick? - if ( !isActive ) -@@ -248,15 +403,19 @@ public class ActivationRange - if ( ( MinecraftServer.currentTick - entity.activatedTick - 1 ) % 20 == 0 ) - { - // Check immunities every 20 ticks. -- if ( ActivationRange.checkEntityImmunities( entity ) ) -- { -- // Triggered some sort of immunity, give 20 full ticks before we check again. -- entity.activatedTick = MinecraftServer.currentTick + 20; -+ // Paper start -+ int immunity = checkEntityImmunities(entity); -+ if (immunity >= 0) { -+ entity.activatedTick = MinecraftServer.currentTick + immunity; -+ } else { -+ entity.isTemporarilyActive = true; - } -+ // Paper end - isActive = true; -+ - } - // Add a little performance juice to active entities. Skip 1/4 if not immune. -- } else if ( !entity.defaultActivationState && (entity.tickCount + entity.getId()) % 4 == 0 && !ActivationRange.checkEntityImmunities( entity ) ) // Paper - Ensure checking item movement is offset from Spigot's entity activation range check -+ } else if ( (entity.tickCount + entity.getId()) % 4 == 0 && ActivationRange.checkEntityImmunities( entity ) < 0 ) // Paper - { - isActive = false; - } -diff --git a/src/main/java/org/spigotmc/SpigotWorldConfig.java b/src/main/java/org/spigotmc/SpigotWorldConfig.java -index 5485df1a1b59e81f4dcedd21dd972e1fd2759573..1cf6d4f854d89c515e48e1fb365eb95ff9340765 100644 ---- a/src/main/java/org/spigotmc/SpigotWorldConfig.java -+++ b/src/main/java/org/spigotmc/SpigotWorldConfig.java -@@ -211,14 +211,60 @@ public class SpigotWorldConfig - public int monsterActivationRange = 32; - public int raiderActivationRange = 48; - public int miscActivationRange = 16; -+ // Paper start -+ public int flyingMonsterActivationRange = 32; -+ public int waterActivationRange = 16; -+ public int villagerActivationRange = 32; -+ public int wakeUpInactiveAnimals = 4; -+ public int wakeUpInactiveAnimalsEvery = 60*20; -+ public int wakeUpInactiveAnimalsFor = 5*20; -+ public int wakeUpInactiveMonsters = 8; -+ public int wakeUpInactiveMonstersEvery = 20*20; -+ public int wakeUpInactiveMonstersFor = 5*20; -+ public int wakeUpInactiveVillagers = 4; -+ public int wakeUpInactiveVillagersEvery = 30*20; -+ public int wakeUpInactiveVillagersFor = 5*20; -+ public int wakeUpInactiveFlying = 8; -+ public int wakeUpInactiveFlyingEvery = 10*20; -+ public int wakeUpInactiveFlyingFor = 5*20; -+ public int villagersWorkImmunityAfter = 5*20; -+ public int villagersWorkImmunityFor = 20; -+ public boolean villagersActiveForPanic = true; -+ // Paper end - public boolean tickInactiveVillagers = true; - public boolean ignoreSpectatorActivation = false; - private void activationRange() - { -+ boolean hasAnimalsConfig = config.getInt("entity-activation-range.animals", this.animalActivationRange) != this.animalActivationRange; // Paper - this.animalActivationRange = this.getInt( "entity-activation-range.animals", this.animalActivationRange ); - this.monsterActivationRange = this.getInt( "entity-activation-range.monsters", this.monsterActivationRange ); - this.raiderActivationRange = this.getInt( "entity-activation-range.raiders", this.raiderActivationRange ); - this.miscActivationRange = this.getInt( "entity-activation-range.misc", this.miscActivationRange ); -+ // Paper start -+ this.waterActivationRange = this.getInt( "entity-activation-range.water", this.waterActivationRange ); -+ this.villagerActivationRange = this.getInt( "entity-activation-range.villagers", hasAnimalsConfig ? this.animalActivationRange : this.villagerActivationRange ); -+ this.flyingMonsterActivationRange = this.getInt( "entity-activation-range.flying-monsters", this.flyingMonsterActivationRange ); -+ -+ this.wakeUpInactiveAnimals = this.getInt("entity-activation-range.wake-up-inactive.animals-max-per-tick", this.wakeUpInactiveAnimals); -+ this.wakeUpInactiveAnimalsEvery = this.getInt("entity-activation-range.wake-up-inactive.animals-every", this.wakeUpInactiveAnimalsEvery); -+ this.wakeUpInactiveAnimalsFor = this.getInt("entity-activation-range.wake-up-inactive.animals-for", this.wakeUpInactiveAnimalsFor); -+ -+ this.wakeUpInactiveMonsters = this.getInt("entity-activation-range.wake-up-inactive.monsters-max-per-tick", this.wakeUpInactiveMonsters); -+ this.wakeUpInactiveMonstersEvery = this.getInt("entity-activation-range.wake-up-inactive.monsters-every", this.wakeUpInactiveMonstersEvery); -+ this.wakeUpInactiveMonstersFor = this.getInt("entity-activation-range.wake-up-inactive.monsters-for", this.wakeUpInactiveMonstersFor); -+ -+ this.wakeUpInactiveVillagers = this.getInt("entity-activation-range.wake-up-inactive.villagers-max-per-tick", this.wakeUpInactiveVillagers); -+ this.wakeUpInactiveVillagersEvery = this.getInt("entity-activation-range.wake-up-inactive.villagers-every", this.wakeUpInactiveVillagersEvery); -+ this.wakeUpInactiveVillagersFor = this.getInt("entity-activation-range.wake-up-inactive.villagers-for", this.wakeUpInactiveVillagersFor); -+ -+ this.wakeUpInactiveFlying = this.getInt("entity-activation-range.wake-up-inactive.flying-monsters-max-per-tick", this.wakeUpInactiveFlying); -+ this.wakeUpInactiveFlyingEvery = this.getInt("entity-activation-range.wake-up-inactive.flying-monsters-every", this.wakeUpInactiveFlyingEvery); -+ this.wakeUpInactiveFlyingFor = this.getInt("entity-activation-range.wake-up-inactive.flying-monsters-for", this.wakeUpInactiveFlyingFor); -+ -+ this.villagersWorkImmunityAfter = this.getInt( "entity-activation-range.villagers-work-immunity-after", this.villagersWorkImmunityAfter ); -+ this.villagersWorkImmunityFor = this.getInt( "entity-activation-range.villagers-work-immunity-for", this.villagersWorkImmunityFor ); -+ this.villagersActiveForPanic = this.getBoolean( "entity-activation-range.villagers-active-for-panic", this.villagersActiveForPanic ); -+ // Paper end - this.tickInactiveVillagers = this.getBoolean( "entity-activation-range.tick-inactive-villagers", this.tickInactiveVillagers ); - this.ignoreSpectatorActivation = this.getBoolean( "entity-activation-range.ignore-spectators", this.ignoreSpectatorActivation ); - this.log( "Entity Activation Range: An " + this.animalActivationRange + " / Mo " + this.monsterActivationRange + " / Ra " + this.raiderActivationRange + " / Mi " + this.miscActivationRange + " / Tiv " + this.tickInactiveVillagers + " / Isa " + this.ignoreSpectatorActivation ); diff --git a/patches/server/0995-Optional-per-player-mob-spawns.patch b/patches/server/0995-Optional-per-player-mob-spawns.patch new file mode 100644 index 0000000000..b9fe80f3e4 --- /dev/null +++ b/patches/server/0995-Optional-per-player-mob-spawns.patch @@ -0,0 +1,255 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: kickash32 +Date: Mon, 19 Aug 2019 01:27:58 +0500 +Subject: [PATCH] Optional per player mob spawns + + +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index 1d2d4d38ff3414896d07f7f4e40d0edb9a8c3993..56f855ed9dbbb9a7025bef6f9e98a8cdcf2ad4fc 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -291,9 +291,28 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + }); + } + ++ // Paper start - Optional per player mob spawns ++ public void updatePlayerMobTypeMap(final Entity entity) { ++ if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { ++ return; ++ } ++ int index = entity.getType().getCategory().ordinal(); ++ ++ final com.destroystokyo.paper.util.maplist.ReferenceList inRange = ++ this.getNearbyPlayers().getPlayers(entity.chunkPosition(), io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE); ++ if (inRange == null) { ++ return; ++ } ++ final Object[] backingSet = inRange.getRawData(); ++ for (int i = 0, len = inRange.size(); i < len; i++) { ++ ++((ServerPlayer)backingSet[i]).mobCounts[index]; ++ } ++ } ++ + public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) { +- return -1; ++ return player.mobCounts[mobCategory.ordinal()]; + } ++ // Paper end - Optional per player mob spawns + + private static double euclideanDistanceSquared(ChunkPos pos, Entity entity) { + double d0 = (double) SectionPos.sectionToBlockCoord(pos.x, 8); +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index d1728e13a7b649f308bde90ab633c79d86c10822..002aad174fb1781ee963414037f84e5120488592 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -516,7 +516,19 @@ public class ServerChunkCache extends ChunkSource { + gameprofilerfiller.popPush("naturalSpawnCount"); + this.level.timings.countNaturalMobs.startTiming(); // Paper - timings + int k = this.distanceManager.getNaturalSpawnChunkCount(); +- NaturalSpawner.SpawnState spawnercreature_d = NaturalSpawner.createState(k, this.level.getAllEntities(), this::getFullChunk, new LocalMobCapCalculator(this.chunkMap)); ++ // Paper start - Optional per player mob spawns ++ int naturalSpawnChunkCount = k; ++ NaturalSpawner.SpawnState spawnercreature_d; // moved down ++ if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled ++ // re-set mob counts ++ for (ServerPlayer player : this.level.players) { ++ Arrays.fill(player.mobCounts, 0); ++ } ++ spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true); ++ } else { ++ spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); ++ } ++ // Paper end - Optional per player mob spawns + this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings + + this.lastSpawnState = spawnercreature_d; +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index fc8e9cd699bdefb6ddadc6aa30e6459417c73194..61e256224c6d7c944bee0e11d0edcf05b8a5ecd3 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -271,6 +271,10 @@ public class ServerPlayer extends Player { + public boolean queueHealthUpdatePacket; + public net.minecraft.network.protocol.game.ClientboundSetHealthPacket queuedHealthUpdatePacket; + // Paper end - cancellable death event ++ // Paper start - Optional per player mob spawns ++ public static final int MOBCATEGORY_TOTAL_ENUMS = net.minecraft.world.entity.MobCategory.values().length; ++ public final int[] mobCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper ++ // Paper end - Optional per player mob spawns + + // CraftBukkit start + public CraftPlayer.TransferCookieConnection transferCookieConnection; +diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +index ea6533c1ac218aa075da3401807a06fcb7892321..0679636530ca1afd077c43b0fa605a2ac74e0522 100644 +--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java ++++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +@@ -67,6 +67,12 @@ public final class NaturalSpawner { + private NaturalSpawner() {} + + public static NaturalSpawner.SpawnState createState(int spawningChunkCount, Iterable entities, NaturalSpawner.ChunkGetter chunkSource, LocalMobCapCalculator densityCapper) { ++ // Paper start - Optional per player mob spawns ++ return createState(spawningChunkCount, entities, chunkSource, densityCapper, false); ++ } ++ ++ public static NaturalSpawner.SpawnState createState(int spawningChunkCount, Iterable entities, NaturalSpawner.ChunkGetter chunkSource, LocalMobCapCalculator densityCapper, boolean countMobs) { ++ // Paper end - Optional per player mob spawns + PotentialCalculator spawnercreatureprobabilities = new PotentialCalculator(); + Object2IntOpenHashMap object2intopenhashmap = new Object2IntOpenHashMap(); + Iterator iterator = entities.iterator(); +@@ -99,11 +105,16 @@ public final class NaturalSpawner { + spawnercreatureprobabilities.addCharge(entity.blockPosition(), biomesettingsmobs_b.charge()); + } + +- if (entity instanceof Mob) { ++ if (densityCapper != null && entity instanceof Mob) { // Paper - Optional per player mob spawns + densityCapper.addMob(chunk.getPos(), enumcreaturetype); + } + + object2intopenhashmap.addTo(enumcreaturetype, 1); ++ // Paper start - Optional per player mob spawns ++ if (countMobs) { ++ chunk.level.getChunkSource().chunkMap.updatePlayerMobTypeMap(entity); ++ } ++ // Paper end - Optional per player mob spawns + }); + } + } +@@ -138,13 +149,35 @@ public final class NaturalSpawner { + continue; + } + +- if ((spawnAnimals || !enumcreaturetype.isFriendly()) && (spawnMonsters || enumcreaturetype.isFriendly()) && (rareSpawn || !enumcreaturetype.isPersistent()) && info.canSpawnForCategory(enumcreaturetype, chunk.getPos(), limit)) { ++ // Paper start - Optional per player mob spawns; only allow spawns upto the limit per chunk and update count afterwards ++ int currEntityCount = info.mobCategoryCounts.getInt(enumcreaturetype); ++ int k1 = limit * info.getSpawnableChunkCount() / NaturalSpawner.MAGIC_NUMBER; ++ int difference = k1 - currEntityCount; ++ ++ if (world.paperConfig().entities.spawning.perPlayerMobSpawns) { ++ int minDiff = Integer.MAX_VALUE; ++ final com.destroystokyo.paper.util.maplist.ReferenceList inRange = ++ world.chunkSource.chunkMap.getNearbyPlayers().getPlayers(chunk.getPos(), io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE); ++ if (inRange != null) { ++ final Object[] backingSet = inRange.getRawData(); ++ for (int k = 0, len = inRange.size(); k < len; k++) { ++ minDiff = Math.min(limit - world.getChunkSource().chunkMap.getMobCountNear((net.minecraft.server.level.ServerPlayer)backingSet[k], enumcreaturetype), minDiff); ++ } ++ } ++ difference = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff; ++ } ++ if ((spawnAnimals || !enumcreaturetype.isFriendly()) && (spawnMonsters || enumcreaturetype.isFriendly()) && (rareSpawn || !enumcreaturetype.isPersistent()) && difference > 0) { ++ // Paper end - Optional per player mob spawns + // CraftBukkit end + Objects.requireNonNull(info); + NaturalSpawner.SpawnPredicate spawnercreature_c = info::canSpawn; + + Objects.requireNonNull(info); +- NaturalSpawner.spawnCategoryForChunk(enumcreaturetype, world, chunk, spawnercreature_c, info::afterSpawn); ++ // Paper start - Optional per player mob spawns ++ int spawnCount = NaturalSpawner.spawnCategoryForChunk(enumcreaturetype, world, chunk, spawnercreature_c, info::afterSpawn, ++ difference, world.paperConfig().entities.spawning.perPlayerMobSpawns ? world.getChunkSource().chunkMap::updatePlayerMobTypeMap : null); ++ info.mobCategoryCounts.mergeInt(enumcreaturetype, spawnCount, Integer::sum); ++ // Paper end - Optional per player mob spawns + } + } + +@@ -163,11 +196,17 @@ public final class NaturalSpawner { + // Paper end - Add mobcaps commands + + public static void spawnCategoryForChunk(MobCategory group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) { ++ // Paper start - Optional per player mob spawns ++ spawnCategoryForChunk(group, world, chunk, checker, runner, Integer.MAX_VALUE, null); ++ } ++ public static int spawnCategoryForChunk(MobCategory group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner, int maxSpawns, Consumer trackEntity) { ++ // Paper end - Optional per player mob spawns + BlockPos blockposition = NaturalSpawner.getRandomPosWithin(world, chunk); + + if (blockposition.getY() >= world.getMinBuildHeight() + 1) { +- NaturalSpawner.spawnCategoryForPosition(group, world, chunk, blockposition, checker, runner); ++ return NaturalSpawner.spawnCategoryForPosition(group, world, chunk, blockposition, checker, runner, maxSpawns, trackEntity); // Paper - Optional per player mob spawns + } ++ return 0; // Paper - Optional per player mob spawns + } + + @VisibleForDebug +@@ -178,15 +217,21 @@ public final class NaturalSpawner { + }); + } + ++ // Paper start - Optional per player mob spawns + public static void spawnCategoryForPosition(MobCategory group, ServerLevel world, ChunkAccess chunk, BlockPos pos, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) { ++ spawnCategoryForPosition(group, world,chunk, pos, checker, runner, Integer.MAX_VALUE, null); ++ } ++ public static int spawnCategoryForPosition(MobCategory group, ServerLevel world, ChunkAccess chunk, BlockPos pos, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner, int maxSpawns, Consumer trackEntity) { ++ // Paper end - Optional per player mob spawns + StructureManager structuremanager = world.structureManager(); + ChunkGenerator chunkgenerator = world.getChunkSource().getGenerator(); + int i = pos.getY(); + BlockState iblockdata = world.getBlockStateIfLoadedAndInBounds(pos); // Paper - don't load chunks for mob spawn ++ int j = 0; // Paper - Optional per player mob spawns; moved up + + if (iblockdata != null && !iblockdata.isRedstoneConductor(chunk, pos)) { // Paper - don't load chunks for mob spawn + BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos(); +- int j = 0; ++ //int j = 0; // Paper - Optional per player mob spawns; moved up + int k = 0; + + while (k < 3) { +@@ -228,14 +273,14 @@ public final class NaturalSpawner { + // Paper start - PreCreatureSpawnEvent + PreSpawnStatus doSpawning = isValidSpawnPostitionForType(world, group, structuremanager, chunkgenerator, biomesettingsmobs_c, blockposition_mutableblockposition, d2); + if (doSpawning == PreSpawnStatus.ABORT) { +- return; ++ return j; // Paper - Optional per player mob spawns + } + if (doSpawning == PreSpawnStatus.SUCCESS && checker.test(biomesettingsmobs_c.type, blockposition_mutableblockposition, chunk)) { + // Paper end - PreCreatureSpawnEvent + Mob entityinsentient = NaturalSpawner.getMobForSpawn(world, biomesettingsmobs_c.type); + + if (entityinsentient == null) { +- return; ++ return j; // Paper - Optional per player mob spawns + } + + entityinsentient.moveTo(d0, (double) i, d1, world.random.nextFloat() * 360.0F, 0.0F); +@@ -248,10 +293,15 @@ public final class NaturalSpawner { + ++j; + ++k1; + runner.run(entityinsentient, chunk); ++ // Paper start - Optional per player mob spawns ++ if (trackEntity != null) { ++ trackEntity.accept(entityinsentient); ++ } ++ // Paper end - Optional per player mob spawns + } + // CraftBukkit end +- if (j >= entityinsentient.getMaxSpawnClusterSize()) { +- return; ++ if (j >= entityinsentient.getMaxSpawnClusterSize() || j >= maxSpawns) { // Paper - Optional per player mob spawns ++ return j; // Paper - Optional per player mob spawns + } + + if (entityinsentient.isMaxGroupSizeReached(k1)) { +@@ -273,6 +323,7 @@ public final class NaturalSpawner { + } + + } ++ return j; // Paper - Optional per player mob spawns + } + + private static boolean isRightDistanceToPlayerAndSpawnPoint(ServerLevel world, ChunkAccess chunk, BlockPos.MutableBlockPos pos, double squaredDistance) { +@@ -523,7 +574,7 @@ public final class NaturalSpawner { + MobCategory enumcreaturetype = entitytypes.getCategory(); + + this.mobCategoryCounts.addTo(enumcreaturetype, 1); +- this.localMobCapCalculator.addMob(new ChunkPos(blockposition), enumcreaturetype); ++ if (this.localMobCapCalculator != null) this.localMobCapCalculator.addMob(new ChunkPos(blockposition), enumcreaturetype); // Paper - Optional per player mob spawns + } + + public int getSpawnableChunkCount() { +@@ -539,6 +590,7 @@ public final class NaturalSpawner { + int i = limit * this.spawnableChunkCount / NaturalSpawner.MAGIC_NUMBER; + // CraftBukkit end + ++ if (this.localMobCapCalculator == null) return this.mobCategoryCounts.getInt(enumcreaturetype) < i; // Paper - Optional per player mob spawns + return this.mobCategoryCounts.getInt(enumcreaturetype) >= i ? false : this.localMobCapCalculator.canSpawn(enumcreaturetype, chunkcoordintpair); + } + } diff --git a/patches/server/0996-Anti-Xray.patch b/patches/server/0996-Anti-Xray.patch new file mode 100644 index 0000000000..058f4c5c6a --- /dev/null +++ b/patches/server/0996-Anti-Xray.patch @@ -0,0 +1,1638 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: stonar96 +Date: Thu, 25 Nov 2021 13:27:51 +0100 +Subject: [PATCH] Anti-Xray + + +diff --git a/src/main/java/com/destroystokyo/paper/antixray/BitStorageReader.java b/src/main/java/com/destroystokyo/paper/antixray/BitStorageReader.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e448c26327b5f6189c3c52e698cff66c8f9ad81a +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/antixray/BitStorageReader.java +@@ -0,0 +1,51 @@ ++package com.destroystokyo.paper.antixray; ++ ++public final class BitStorageReader { ++ ++ private byte[] buffer; ++ private int bits; ++ private int mask; ++ private int longInBufferIndex; ++ private int bitInLongIndex; ++ private long current; ++ ++ public void setBuffer(byte[] buffer) { ++ this.buffer = buffer; ++ } ++ ++ public void setBits(int bits) { ++ this.bits = bits; ++ mask = (1 << bits) - 1; ++ } ++ ++ public void setIndex(int index) { ++ longInBufferIndex = index; ++ bitInLongIndex = 0; ++ init(); ++ } ++ ++ private void init() { ++ if (buffer.length > longInBufferIndex + 7) { ++ current = ((((long) buffer[longInBufferIndex]) << 56) ++ | (((long) buffer[longInBufferIndex + 1] & 0xff) << 48) ++ | (((long) buffer[longInBufferIndex + 2] & 0xff) << 40) ++ | (((long) buffer[longInBufferIndex + 3] & 0xff) << 32) ++ | (((long) buffer[longInBufferIndex + 4] & 0xff) << 24) ++ | (((long) buffer[longInBufferIndex + 5] & 0xff) << 16) ++ | (((long) buffer[longInBufferIndex + 6] & 0xff) << 8) ++ | (((long) buffer[longInBufferIndex + 7] & 0xff))); ++ } ++ } ++ ++ public int read() { ++ if (bitInLongIndex + bits > 64) { ++ bitInLongIndex = 0; ++ longInBufferIndex += 8; ++ init(); ++ } ++ ++ int value = (int) (current >>> bitInLongIndex) & mask; ++ bitInLongIndex += bits; ++ return value; ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/antixray/BitStorageWriter.java b/src/main/java/com/destroystokyo/paper/antixray/BitStorageWriter.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e4540ea278f2dc871cb6a3cb8897559bfd65e134 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/antixray/BitStorageWriter.java +@@ -0,0 +1,79 @@ ++package com.destroystokyo.paper.antixray; ++ ++public final class BitStorageWriter { ++ ++ private byte[] buffer; ++ private int bits; ++ private long mask; ++ private int longInBufferIndex; ++ private int bitInLongIndex; ++ private long current; ++ private boolean dirty; ++ ++ public void setBuffer(byte[] buffer) { ++ this.buffer = buffer; ++ } ++ ++ public void setBits(int bits) { ++ this.bits = bits; ++ mask = (1L << bits) - 1; ++ } ++ ++ public void setIndex(int index) { ++ longInBufferIndex = index; ++ bitInLongIndex = 0; ++ init(); ++ } ++ ++ private void init() { ++ if (buffer.length > longInBufferIndex + 7) { ++ current = ((((long) buffer[longInBufferIndex]) << 56) ++ | (((long) buffer[longInBufferIndex + 1] & 0xff) << 48) ++ | (((long) buffer[longInBufferIndex + 2] & 0xff) << 40) ++ | (((long) buffer[longInBufferIndex + 3] & 0xff) << 32) ++ | (((long) buffer[longInBufferIndex + 4] & 0xff) << 24) ++ | (((long) buffer[longInBufferIndex + 5] & 0xff) << 16) ++ | (((long) buffer[longInBufferIndex + 6] & 0xff) << 8) ++ | (((long) buffer[longInBufferIndex + 7] & 0xff))); ++ } ++ ++ dirty = false; ++ } ++ ++ public void flush() { ++ if (dirty && buffer.length > longInBufferIndex + 7) { ++ buffer[longInBufferIndex] = (byte) (current >> 56 & 0xff); ++ buffer[longInBufferIndex + 1] = (byte) (current >> 48 & 0xff); ++ buffer[longInBufferIndex + 2] = (byte) (current >> 40 & 0xff); ++ buffer[longInBufferIndex + 3] = (byte) (current >> 32 & 0xff); ++ buffer[longInBufferIndex + 4] = (byte) (current >> 24 & 0xff); ++ buffer[longInBufferIndex + 5] = (byte) (current >> 16 & 0xff); ++ buffer[longInBufferIndex + 6] = (byte) (current >> 8 & 0xff); ++ buffer[longInBufferIndex + 7] = (byte) (current & 0xff); ++ } ++ } ++ ++ public void write(int value) { ++ if (bitInLongIndex + bits > 64) { ++ flush(); ++ bitInLongIndex = 0; ++ longInBufferIndex += 8; ++ init(); ++ } ++ ++ current = current & ~(mask << bitInLongIndex) | (value & mask) << bitInLongIndex; ++ dirty = true; ++ bitInLongIndex += bits; ++ } ++ ++ public void skip() { ++ bitInLongIndex += bits; ++ ++ if (bitInLongIndex > 64) { ++ flush(); ++ bitInLongIndex = bits; ++ longInBufferIndex += 8; ++ init(); ++ } ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java +new file mode 100644 +index 0000000000000000000000000000000000000000..52d2e2b744f91914802506e52a07161729bbcf3a +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java +@@ -0,0 +1,45 @@ ++package com.destroystokyo.paper.antixray; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.Direction; ++import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; ++import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.server.level.ServerPlayerGameMode; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.chunk.LevelChunk; ++ ++public class ChunkPacketBlockController { ++ ++ public static final ChunkPacketBlockController NO_OPERATION_INSTANCE = new ChunkPacketBlockController(); ++ ++ protected ChunkPacketBlockController() { ++ ++ } ++ ++ public BlockState[] getPresetBlockStates(Level level, ChunkPos chunkPos, int chunkSectionY) { ++ return null; ++ } ++ ++ public boolean shouldModify(ServerPlayer player, LevelChunk chunk) { ++ return false; ++ } ++ ++ public ChunkPacketInfo getChunkPacketInfo(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk) { ++ return null; ++ } ++ ++ public void modifyBlocks(ClientboundLevelChunkWithLightPacket chunkPacket, ChunkPacketInfo chunkPacketInfo) { ++ chunkPacket.setReady(true); ++ } ++ ++ public void onBlockChange(Level level, BlockPos blockPos, BlockState newBlockState, BlockState oldBlockState, int flags, int maxUpdateDepth) { ++ ++ } ++ ++ public void onPlayerLeftClickBlock(ServerPlayerGameMode serverPlayerGameMode, BlockPos blockPos, ServerboundPlayerActionPacket.Action action, Direction direction, int worldHeight, int sequence) { ++ ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e7fe98ea30ae6d0baea3ec1f9f98a89502a49a12 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java +@@ -0,0 +1,676 @@ ++package com.destroystokyo.paper.antixray; ++ ++import io.papermc.paper.configuration.WorldConfiguration; ++import io.papermc.paper.configuration.type.EngineMode; ++import java.util.ArrayList; ++import java.util.LinkedHashSet; ++import java.util.LinkedList; ++import java.util.List; ++import java.util.Set; ++import java.util.concurrent.Executor; ++import java.util.concurrent.ThreadLocalRandom; ++import java.util.function.IntSupplier; ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.Direction; ++import net.minecraft.core.registries.Registries; ++import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; ++import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.server.level.ServerPlayerGameMode; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.biome.Biomes; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.block.EntityBlock; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.chunk.EmptyLevelChunk; ++import net.minecraft.world.level.chunk.GlobalPalette; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.LevelChunkSection; ++import net.minecraft.world.level.chunk.MissingPaletteEntryException; ++import net.minecraft.world.level.chunk.Palette; ++import org.bukkit.Bukkit; ++ ++public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockController { ++ ++ private static final Palette GLOBAL_BLOCKSTATE_PALETTE = new GlobalPalette<>(Block.BLOCK_STATE_REGISTRY); ++ private static final LevelChunkSection EMPTY_SECTION = null; ++ private final Executor executor; ++ private final EngineMode engineMode; ++ private final int maxBlockHeight; ++ private final int updateRadius; ++ private final boolean usePermission; ++ private final BlockState[] presetBlockStates; ++ private final BlockState[] presetBlockStatesFull; ++ private final BlockState[] presetBlockStatesStone; ++ private final BlockState[] presetBlockStatesDeepslate; ++ private final BlockState[] presetBlockStatesNetherrack; ++ private final BlockState[] presetBlockStatesEndStone; ++ private final int[] presetBlockStateBitsGlobal; ++ private final int[] presetBlockStateBitsStoneGlobal; ++ private final int[] presetBlockStateBitsDeepslateGlobal; ++ private final int[] presetBlockStateBitsNetherrackGlobal; ++ private final int[] presetBlockStateBitsEndStoneGlobal; ++ private final boolean[] solidGlobal = new boolean[Block.BLOCK_STATE_REGISTRY.size()]; ++ private final boolean[] obfuscateGlobal = new boolean[Block.BLOCK_STATE_REGISTRY.size()]; ++ private final LevelChunkSection[] emptyNearbyChunkSections = {EMPTY_SECTION, EMPTY_SECTION, EMPTY_SECTION, EMPTY_SECTION}; ++ private final int maxBlockHeightUpdatePosition; ++ ++ public ChunkPacketBlockControllerAntiXray(Level level, Executor executor) { ++ this.executor = executor; ++ WorldConfiguration.Anticheat.AntiXray paperWorldConfig = level.paperConfig().anticheat.antiXray; ++ engineMode = paperWorldConfig.engineMode; ++ maxBlockHeight = paperWorldConfig.maxBlockHeight >> 4 << 4; ++ updateRadius = paperWorldConfig.updateRadius; ++ usePermission = paperWorldConfig.usePermission; ++ List toObfuscate; ++ ++ if (engineMode == EngineMode.HIDE) { ++ toObfuscate = paperWorldConfig.hiddenBlocks; ++ presetBlockStates = null; ++ presetBlockStatesFull = null; ++ presetBlockStatesStone = new BlockState[]{Blocks.STONE.defaultBlockState()}; ++ presetBlockStatesDeepslate = new BlockState[]{Blocks.DEEPSLATE.defaultBlockState()}; ++ presetBlockStatesNetherrack = new BlockState[]{Blocks.NETHERRACK.defaultBlockState()}; ++ presetBlockStatesEndStone = new BlockState[]{Blocks.END_STONE.defaultBlockState()}; ++ presetBlockStateBitsGlobal = null; ++ presetBlockStateBitsStoneGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.STONE.defaultBlockState())}; ++ presetBlockStateBitsDeepslateGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.DEEPSLATE.defaultBlockState())}; ++ presetBlockStateBitsNetherrackGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.NETHERRACK.defaultBlockState())}; ++ presetBlockStateBitsEndStoneGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.END_STONE.defaultBlockState())}; ++ } else { ++ toObfuscate = new ArrayList<>(paperWorldConfig.replacementBlocks); ++ List presetBlockStateList = new LinkedList<>(); ++ ++ for (Block block : paperWorldConfig.hiddenBlocks) { ++ ++ if (!(block instanceof EntityBlock)) { ++ toObfuscate.add(block); ++ presetBlockStateList.add(block.defaultBlockState()); ++ } ++ } ++ ++ // The doc of the LinkedHashSet(Collection) constructor doesn't specify that the insertion order is the predictable iteration order of the specified Collection, although it is in the implementation ++ Set presetBlockStateSet = new LinkedHashSet<>(); ++ // Therefore addAll(Collection) is used, which guarantees this order in the doc ++ presetBlockStateSet.addAll(presetBlockStateList); ++ presetBlockStates = presetBlockStateSet.isEmpty() ? new BlockState[]{Blocks.DIAMOND_ORE.defaultBlockState()} : presetBlockStateSet.toArray(new BlockState[0]); ++ presetBlockStatesFull = presetBlockStateSet.isEmpty() ? new BlockState[]{Blocks.DIAMOND_ORE.defaultBlockState()} : presetBlockStateList.toArray(new BlockState[0]); ++ presetBlockStatesStone = null; ++ presetBlockStatesDeepslate = null; ++ presetBlockStatesNetherrack = null; ++ presetBlockStatesEndStone = null; ++ presetBlockStateBitsGlobal = new int[presetBlockStatesFull.length]; ++ ++ for (int i = 0; i < presetBlockStatesFull.length; i++) { ++ presetBlockStateBitsGlobal[i] = GLOBAL_BLOCKSTATE_PALETTE.idFor(presetBlockStatesFull[i]); ++ } ++ ++ presetBlockStateBitsStoneGlobal = null; ++ presetBlockStateBitsDeepslateGlobal = null; ++ presetBlockStateBitsNetherrackGlobal = null; ++ presetBlockStateBitsEndStoneGlobal = null; ++ } ++ ++ for (Block block : toObfuscate) { ++ ++ // Don't obfuscate air because air causes unnecessary block updates and causes block updates to fail in the void ++ if (block != null && !block.defaultBlockState().isAir()) { ++ // Replace all block states of a specified block ++ for (BlockState blockState : block.getStateDefinition().getPossibleStates()) { ++ obfuscateGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)] = true; ++ } ++ } ++ } ++ ++ EmptyLevelChunk emptyChunk = new EmptyLevelChunk(level, new ChunkPos(0, 0), MinecraftServer.getServer().registryAccess().registryOrThrow(Registries.BIOME).getHolderOrThrow(Biomes.PLAINS)); ++ BlockPos zeroPos = new BlockPos(0, 0, 0); ++ ++ for (int i = 0; i < solidGlobal.length; i++) { ++ BlockState blockState = GLOBAL_BLOCKSTATE_PALETTE.valueFor(i); ++ ++ if (blockState != null) { ++ solidGlobal[i] = blockState.isRedstoneConductor(emptyChunk, zeroPos) ++ && blockState.getBlock() != Blocks.SPAWNER && blockState.getBlock() != Blocks.BARRIER && blockState.getBlock() != Blocks.SHULKER_BOX && blockState.getBlock() != Blocks.SLIME_BLOCK && blockState.getBlock() != Blocks.MANGROVE_ROOTS || paperWorldConfig.lavaObscures && blockState == Blocks.LAVA.defaultBlockState(); ++ // Comparing blockState == Blocks.LAVA.defaultBlockState() instead of blockState.getBlock() == Blocks.LAVA ensures that only "stationary lava" is used ++ // shulker box checks TE. ++ } ++ } ++ ++ maxBlockHeightUpdatePosition = maxBlockHeight + updateRadius - 1; ++ } ++ ++ private int getPresetBlockStatesFullLength() { ++ return engineMode == EngineMode.HIDE ? 1 : presetBlockStatesFull.length; ++ } ++ ++ @Override ++ public BlockState[] getPresetBlockStates(Level level, ChunkPos chunkPos, int chunkSectionY) { ++ // Return the block states to be added to the paletted containers so that they can be used for obfuscation ++ int bottomBlockY = chunkSectionY << 4; ++ ++ if (bottomBlockY < maxBlockHeight) { ++ if (engineMode == EngineMode.HIDE) { ++ return switch (level.getWorld().getEnvironment()) { ++ case NETHER -> presetBlockStatesNetherrack; ++ case THE_END -> presetBlockStatesEndStone; ++ default -> bottomBlockY < 0 ? presetBlockStatesDeepslate : presetBlockStatesStone; ++ }; ++ } ++ ++ return presetBlockStates; ++ } ++ ++ return null; ++ } ++ ++ @Override ++ public boolean shouldModify(ServerPlayer player, LevelChunk chunk) { ++ return !usePermission || !player.getBukkitEntity().hasPermission("paper.antixray.bypass"); ++ } ++ ++ @Override ++ public ChunkPacketInfoAntiXray getChunkPacketInfo(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk) { ++ // Return a new instance to collect data and objects in the right state while creating the chunk packet for thread safe access later ++ return new ChunkPacketInfoAntiXray(chunkPacket, chunk, this); ++ } ++ ++ @Override ++ public void modifyBlocks(ClientboundLevelChunkWithLightPacket chunkPacket, ChunkPacketInfo chunkPacketInfo) { ++ if (!(chunkPacketInfo instanceof ChunkPacketInfoAntiXray)) { ++ chunkPacket.setReady(true); ++ return; ++ } ++ ++ if (!Bukkit.isPrimaryThread()) { ++ // Plugins? ++ MinecraftServer.getServer().scheduleOnMain(() -> modifyBlocks(chunkPacket, chunkPacketInfo)); ++ return; ++ } ++ ++ LevelChunk chunk = chunkPacketInfo.getChunk(); ++ int x = chunk.getPos().x; ++ int z = chunk.getPos().z; ++ Level level = chunk.getLevel(); ++ ((ChunkPacketInfoAntiXray) chunkPacketInfo).setNearbyChunks(level.getChunkIfLoaded(x - 1, z), level.getChunkIfLoaded(x + 1, z), level.getChunkIfLoaded(x, z - 1), level.getChunkIfLoaded(x, z + 1)); ++ executor.execute((Runnable) chunkPacketInfo); ++ } ++ ++ // Actually these fields should be variables inside the obfuscate method but in sync mode or with SingleThreadExecutor in async mode it's okay (even without ThreadLocal) ++ // If an ExecutorService with multiple threads is used, ThreadLocal must be used here ++ private final ThreadLocal presetBlockStateBits = ThreadLocal.withInitial(() -> new int[getPresetBlockStatesFullLength()]); ++ private static final ThreadLocal SOLID = ThreadLocal.withInitial(() -> new boolean[Block.BLOCK_STATE_REGISTRY.size()]); ++ private static final ThreadLocal OBFUSCATE = ThreadLocal.withInitial(() -> new boolean[Block.BLOCK_STATE_REGISTRY.size()]); ++ // These boolean arrays represent chunk layers, true means don't obfuscate, false means obfuscate ++ private static final ThreadLocal CURRENT = ThreadLocal.withInitial(() -> new boolean[16][16]); ++ private static final ThreadLocal NEXT = ThreadLocal.withInitial(() -> new boolean[16][16]); ++ private static final ThreadLocal NEXT_NEXT = ThreadLocal.withInitial(() -> new boolean[16][16]); ++ ++ public void obfuscate(ChunkPacketInfoAntiXray chunkPacketInfoAntiXray) { ++ int[] presetBlockStateBits = this.presetBlockStateBits.get(); ++ boolean[] solid = SOLID.get(); ++ boolean[] obfuscate = OBFUSCATE.get(); ++ boolean[][] current = CURRENT.get(); ++ boolean[][] next = NEXT.get(); ++ boolean[][] nextNext = NEXT_NEXT.get(); ++ // bitStorageReader, bitStorageWriter and nearbyChunkSections could also be reused (with ThreadLocal if necessary) but it's not worth it ++ BitStorageReader bitStorageReader = new BitStorageReader(); ++ BitStorageWriter bitStorageWriter = new BitStorageWriter(); ++ LevelChunkSection[] nearbyChunkSections = new LevelChunkSection[4]; ++ LevelChunk chunk = chunkPacketInfoAntiXray.getChunk(); ++ Level level = chunk.getLevel(); ++ int maxChunkSectionIndex = Math.min((maxBlockHeight >> 4) - chunk.getMinSection(), chunk.getSectionsCount()) - 1; ++ boolean[] solidTemp = null; ++ boolean[] obfuscateTemp = null; ++ bitStorageReader.setBuffer(chunkPacketInfoAntiXray.getBuffer()); ++ bitStorageWriter.setBuffer(chunkPacketInfoAntiXray.getBuffer()); ++ int numberOfBlocks = presetBlockStateBits.length; ++ // Keep the lambda expressions as simple as possible. They are used very frequently. ++ LayeredIntSupplier random = numberOfBlocks == 1 ? (() -> 0) : engineMode == EngineMode.OBFUSCATE_LAYER ? new LayeredIntSupplier() { ++ // engine-mode: 3 ++ private int state; ++ private int next; ++ ++ { ++ while ((state = ThreadLocalRandom.current().nextInt()) == 0) ; ++ } ++ ++ @Override ++ public void nextLayer() { ++ // https://en.wikipedia.org/wiki/Xorshift ++ state ^= state << 13; ++ state ^= state >>> 17; ++ state ^= state << 5; ++ // https://www.pcg-random.org/posts/bounded-rands.html ++ next = (int) ((Integer.toUnsignedLong(state) * numberOfBlocks) >>> 32); ++ } ++ ++ @Override ++ public int getAsInt() { ++ return next; ++ } ++ } : new LayeredIntSupplier() { ++ // engine-mode: 2 ++ private int state; ++ ++ { ++ while ((state = ThreadLocalRandom.current().nextInt()) == 0) ; ++ } ++ ++ @Override ++ public int getAsInt() { ++ // https://en.wikipedia.org/wiki/Xorshift ++ state ^= state << 13; ++ state ^= state >>> 17; ++ state ^= state << 5; ++ // https://www.pcg-random.org/posts/bounded-rands.html ++ return (int) ((Integer.toUnsignedLong(state) * numberOfBlocks) >>> 32); ++ } ++ }; ++ ++ for (int chunkSectionIndex = 0; chunkSectionIndex <= maxChunkSectionIndex; chunkSectionIndex++) { ++ if (chunkPacketInfoAntiXray.isWritten(chunkSectionIndex) && chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex) != null) { ++ int[] presetBlockStateBitsTemp; ++ ++ if (chunkPacketInfoAntiXray.getPalette(chunkSectionIndex) instanceof GlobalPalette) { ++ if (engineMode == EngineMode.HIDE) { ++ presetBlockStateBitsTemp = switch (level.getWorld().getEnvironment()) { ++ case NETHER -> presetBlockStateBitsNetherrackGlobal; ++ case THE_END -> presetBlockStateBitsEndStoneGlobal; ++ default -> chunkSectionIndex + chunk.getMinSection() < 0 ? presetBlockStateBitsDeepslateGlobal : presetBlockStateBitsStoneGlobal; ++ }; ++ } else { ++ presetBlockStateBitsTemp = presetBlockStateBitsGlobal; ++ } ++ } else { ++ // If it's presetBlockStates, use this.presetBlockStatesFull instead ++ BlockState[] presetBlockStatesFull = chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex) == presetBlockStates ? this.presetBlockStatesFull : chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex); ++ presetBlockStateBitsTemp = presetBlockStateBits; ++ ++ for (int i = 0; i < presetBlockStateBitsTemp.length; i++) { ++ // This is thread safe because we only request IDs that are guaranteed to be in the palette and are visible ++ // For more details see the comments in the readPalette method ++ presetBlockStateBitsTemp[i] = chunkPacketInfoAntiXray.getPalette(chunkSectionIndex).idFor(presetBlockStatesFull[i]); ++ } ++ } ++ ++ bitStorageWriter.setIndex(chunkPacketInfoAntiXray.getIndex(chunkSectionIndex)); ++ ++ // Check if the chunk section below was not obfuscated ++ if (chunkSectionIndex == 0 || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex - 1) || chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex - 1) == null) { ++ // If so, initialize some stuff ++ bitStorageReader.setBits(chunkPacketInfoAntiXray.getBits(chunkSectionIndex)); ++ bitStorageReader.setIndex(chunkPacketInfoAntiXray.getIndex(chunkSectionIndex)); ++ solidTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex), solid, solidGlobal); ++ obfuscateTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex), obfuscate, obfuscateGlobal); ++ // Read the blocks of the upper layer of the chunk section below if it exists ++ LevelChunkSection belowChunkSection = null; ++ boolean skipFirstLayer = chunkSectionIndex == 0 || (belowChunkSection = chunk.getSections()[chunkSectionIndex - 1]) == EMPTY_SECTION; ++ ++ for (int z = 0; z < 16; z++) { ++ for (int x = 0; x < 16; x++) { ++ current[z][x] = true; ++ next[z][x] = skipFirstLayer || isTransparent(belowChunkSection, x, 15, z); ++ } ++ } ++ ++ // Abuse the obfuscateLayer method to read the blocks of the first layer of the current chunk section ++ bitStorageWriter.setBits(0); ++ obfuscateLayer(-1, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, emptyNearbyChunkSections, random); ++ } ++ ++ bitStorageWriter.setBits(chunkPacketInfoAntiXray.getBits(chunkSectionIndex)); ++ nearbyChunkSections[0] = chunkPacketInfoAntiXray.getNearbyChunks()[0] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[0].getSections()[chunkSectionIndex]; ++ nearbyChunkSections[1] = chunkPacketInfoAntiXray.getNearbyChunks()[1] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[1].getSections()[chunkSectionIndex]; ++ nearbyChunkSections[2] = chunkPacketInfoAntiXray.getNearbyChunks()[2] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[2].getSections()[chunkSectionIndex]; ++ nearbyChunkSections[3] = chunkPacketInfoAntiXray.getNearbyChunks()[3] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[3].getSections()[chunkSectionIndex]; ++ ++ // Obfuscate all layers of the current chunk section except the upper one ++ for (int y = 0; y < 15; y++) { ++ boolean[][] temp = current; ++ current = next; ++ next = nextNext; ++ nextNext = temp; ++ random.nextLayer(); ++ obfuscateLayer(y, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, nearbyChunkSections, random); ++ } ++ ++ // Check if the chunk section above doesn't need obfuscation ++ if (chunkSectionIndex == maxChunkSectionIndex || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex + 1) || chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex + 1) == null) { ++ // If so, obfuscate the upper layer of the current chunk section by reading blocks of the first layer from the chunk section above if it exists ++ LevelChunkSection aboveChunkSection; ++ ++ if (chunkSectionIndex != chunk.getSectionsCount() - 1 && (aboveChunkSection = chunk.getSections()[chunkSectionIndex + 1]) != EMPTY_SECTION) { ++ boolean[][] temp = current; ++ current = next; ++ next = nextNext; ++ nextNext = temp; ++ ++ for (int z = 0; z < 16; z++) { ++ for (int x = 0; x < 16; x++) { ++ if (isTransparent(aboveChunkSection, x, 0, z)) { ++ current[z][x] = true; ++ } ++ } ++ } ++ ++ // There is nothing to read anymore ++ bitStorageReader.setBits(0); ++ solid[0] = true; ++ random.nextLayer(); ++ obfuscateLayer(15, bitStorageReader, bitStorageWriter, solid, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, nearbyChunkSections, random); ++ } ++ } else { ++ // If not, initialize the reader and other stuff for the chunk section above to obfuscate the upper layer of the current chunk section ++ bitStorageReader.setBits(chunkPacketInfoAntiXray.getBits(chunkSectionIndex + 1)); ++ bitStorageReader.setIndex(chunkPacketInfoAntiXray.getIndex(chunkSectionIndex + 1)); ++ solidTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex + 1), solid, solidGlobal); ++ obfuscateTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex + 1), obfuscate, obfuscateGlobal); ++ boolean[][] temp = current; ++ current = next; ++ next = nextNext; ++ nextNext = temp; ++ random.nextLayer(); ++ obfuscateLayer(15, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, nearbyChunkSections, random); ++ } ++ ++ bitStorageWriter.flush(); ++ } ++ } ++ ++ chunkPacketInfoAntiXray.getChunkPacket().setReady(true); ++ } ++ ++ private void obfuscateLayer(int y, BitStorageReader bitStorageReader, BitStorageWriter bitStorageWriter, boolean[] solid, boolean[] obfuscate, int[] presetBlockStateBits, boolean[][] current, boolean[][] next, boolean[][] nextNext, LevelChunkSection[] nearbyChunkSections, IntSupplier random) { ++ // First block of first line ++ int bits = bitStorageReader.read(); ++ ++ if (nextNext[0][0] = !solid[bits]) { ++ bitStorageWriter.skip(); ++ next[0][1] = true; ++ next[1][0] = true; ++ } else { ++ if (current[0][0] || isTransparent(nearbyChunkSections[2], 0, y, 15) || isTransparent(nearbyChunkSections[0], 15, y, 0)) { ++ bitStorageWriter.skip(); ++ } else { ++ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); ++ } ++ } ++ ++ if (!obfuscate[bits]) { ++ next[0][0] = true; ++ } ++ ++ // First line ++ for (int x = 1; x < 15; x++) { ++ bits = bitStorageReader.read(); ++ ++ if (nextNext[0][x] = !solid[bits]) { ++ bitStorageWriter.skip(); ++ next[0][x - 1] = true; ++ next[0][x + 1] = true; ++ next[1][x] = true; ++ } else { ++ if (current[0][x] || isTransparent(nearbyChunkSections[2], x, y, 15)) { ++ bitStorageWriter.skip(); ++ } else { ++ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); ++ } ++ } ++ ++ if (!obfuscate[bits]) { ++ next[0][x] = true; ++ } ++ } ++ ++ // Last block of first line ++ bits = bitStorageReader.read(); ++ ++ if (nextNext[0][15] = !solid[bits]) { ++ bitStorageWriter.skip(); ++ next[0][14] = true; ++ next[1][15] = true; ++ } else { ++ if (current[0][15] || isTransparent(nearbyChunkSections[2], 15, y, 15) || isTransparent(nearbyChunkSections[1], 0, y, 0)) { ++ bitStorageWriter.skip(); ++ } else { ++ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); ++ } ++ } ++ ++ if (!obfuscate[bits]) { ++ next[0][15] = true; ++ } ++ ++ // All inner lines ++ for (int z = 1; z < 15; z++) { ++ // First block ++ bits = bitStorageReader.read(); ++ ++ if (nextNext[z][0] = !solid[bits]) { ++ bitStorageWriter.skip(); ++ next[z][1] = true; ++ next[z - 1][0] = true; ++ next[z + 1][0] = true; ++ } else { ++ if (current[z][0] || isTransparent(nearbyChunkSections[0], 15, y, z)) { ++ bitStorageWriter.skip(); ++ } else { ++ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); ++ } ++ } ++ ++ if (!obfuscate[bits]) { ++ next[z][0] = true; ++ } ++ ++ // All inner blocks ++ for (int x = 1; x < 15; x++) { ++ bits = bitStorageReader.read(); ++ ++ if (nextNext[z][x] = !solid[bits]) { ++ bitStorageWriter.skip(); ++ next[z][x - 1] = true; ++ next[z][x + 1] = true; ++ next[z - 1][x] = true; ++ next[z + 1][x] = true; ++ } else { ++ if (current[z][x]) { ++ bitStorageWriter.skip(); ++ } else { ++ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); ++ } ++ } ++ ++ if (!obfuscate[bits]) { ++ next[z][x] = true; ++ } ++ } ++ ++ // Last block ++ bits = bitStorageReader.read(); ++ ++ if (nextNext[z][15] = !solid[bits]) { ++ bitStorageWriter.skip(); ++ next[z][14] = true; ++ next[z - 1][15] = true; ++ next[z + 1][15] = true; ++ } else { ++ if (current[z][15] || isTransparent(nearbyChunkSections[1], 0, y, z)) { ++ bitStorageWriter.skip(); ++ } else { ++ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); ++ } ++ } ++ ++ if (!obfuscate[bits]) { ++ next[z][15] = true; ++ } ++ } ++ ++ // First block of last line ++ bits = bitStorageReader.read(); ++ ++ if (nextNext[15][0] = !solid[bits]) { ++ bitStorageWriter.skip(); ++ next[15][1] = true; ++ next[14][0] = true; ++ } else { ++ if (current[15][0] || isTransparent(nearbyChunkSections[3], 0, y, 0) || isTransparent(nearbyChunkSections[0], 15, y, 15)) { ++ bitStorageWriter.skip(); ++ } else { ++ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); ++ } ++ } ++ ++ if (!obfuscate[bits]) { ++ next[15][0] = true; ++ } ++ ++ // Last line ++ for (int x = 1; x < 15; x++) { ++ bits = bitStorageReader.read(); ++ ++ if (nextNext[15][x] = !solid[bits]) { ++ bitStorageWriter.skip(); ++ next[15][x - 1] = true; ++ next[15][x + 1] = true; ++ next[14][x] = true; ++ } else { ++ if (current[15][x] || isTransparent(nearbyChunkSections[3], x, y, 0)) { ++ bitStorageWriter.skip(); ++ } else { ++ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); ++ } ++ } ++ ++ if (!obfuscate[bits]) { ++ next[15][x] = true; ++ } ++ } ++ ++ // Last block of last line ++ bits = bitStorageReader.read(); ++ ++ if (nextNext[15][15] = !solid[bits]) { ++ bitStorageWriter.skip(); ++ next[15][14] = true; ++ next[14][15] = true; ++ } else { ++ if (current[15][15] || isTransparent(nearbyChunkSections[3], 15, y, 0) || isTransparent(nearbyChunkSections[1], 0, y, 15)) { ++ bitStorageWriter.skip(); ++ } else { ++ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); ++ } ++ } ++ ++ if (!obfuscate[bits]) { ++ next[15][15] = true; ++ } ++ } ++ ++ private boolean isTransparent(LevelChunkSection chunkSection, int x, int y, int z) { ++ if (chunkSection == EMPTY_SECTION) { ++ return true; ++ } ++ ++ try { ++ return !solidGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(chunkSection.getBlockState(x, y, z))]; ++ } catch (MissingPaletteEntryException e) { ++ // Race condition / visibility issue / no happens-before relationship ++ // We don't care and treat the block as transparent ++ // Internal implementation details of PalettedContainer, LinearPalette, HashMapPalette, CrudeIncrementalIntIdentityHashBiMap, ... guarantee us that no (other) exceptions will occur ++ return true; ++ } ++ } ++ ++ private boolean[] readPalette(Palette palette, boolean[] temp, boolean[] global) { ++ if (palette instanceof GlobalPalette) { ++ return global; ++ } ++ ++ try { ++ for (int i = 0; i < palette.getSize(); i++) { ++ temp[i] = global[GLOBAL_BLOCKSTATE_PALETTE.idFor(palette.valueFor(i))]; ++ } ++ } catch (MissingPaletteEntryException e) { ++ // Race condition / visibility issue / no happens-before relationship ++ // We don't care because we at least see the state as it was when the chunk packet was created ++ // Internal implementation details of PalettedContainer, LinearPalette, HashMapPalette, CrudeIncrementalIntIdentityHashBiMap, ... guarantee us that no (other) exceptions will occur until we have all the data that we need here ++ // Since all palettes have a fixed initial maximum size and there is no internal restructuring and no values are removed from palettes, we are also guaranteed to see the data ++ } ++ ++ return temp; ++ } ++ ++ @Override ++ public void onBlockChange(Level level, BlockPos blockPos, BlockState newBlockState, BlockState oldBlockState, int flags, int maxUpdateDepth) { ++ if (oldBlockState != null && solidGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(oldBlockState)] && !solidGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(newBlockState)] && blockPos.getY() <= maxBlockHeightUpdatePosition) { ++ updateNearbyBlocks(level, blockPos); ++ } ++ } ++ ++ @Override ++ public void onPlayerLeftClickBlock(ServerPlayerGameMode serverPlayerGameMode, BlockPos blockPos, ServerboundPlayerActionPacket.Action action, Direction direction, int worldHeight, int sequence) { ++ if (blockPos.getY() <= maxBlockHeightUpdatePosition) { ++ updateNearbyBlocks(serverPlayerGameMode.level, blockPos); ++ } ++ } ++ ++ private void updateNearbyBlocks(Level level, BlockPos blockPos) { ++ if (updateRadius >= 2) { ++ BlockPos temp = blockPos.west(); ++ updateBlock(level, temp); ++ updateBlock(level, temp.west()); ++ updateBlock(level, temp.below()); ++ updateBlock(level, temp.above()); ++ updateBlock(level, temp.north()); ++ updateBlock(level, temp.south()); ++ updateBlock(level, temp = blockPos.east()); ++ updateBlock(level, temp.east()); ++ updateBlock(level, temp.below()); ++ updateBlock(level, temp.above()); ++ updateBlock(level, temp.north()); ++ updateBlock(level, temp.south()); ++ updateBlock(level, temp = blockPos.below()); ++ updateBlock(level, temp.below()); ++ updateBlock(level, temp.north()); ++ updateBlock(level, temp.south()); ++ updateBlock(level, temp = blockPos.above()); ++ updateBlock(level, temp.above()); ++ updateBlock(level, temp.north()); ++ updateBlock(level, temp.south()); ++ updateBlock(level, temp = blockPos.north()); ++ updateBlock(level, temp.north()); ++ updateBlock(level, temp = blockPos.south()); ++ updateBlock(level, temp.south()); ++ } else if (updateRadius == 1) { ++ updateBlock(level, blockPos.west()); ++ updateBlock(level, blockPos.east()); ++ updateBlock(level, blockPos.below()); ++ updateBlock(level, blockPos.above()); ++ updateBlock(level, blockPos.north()); ++ updateBlock(level, blockPos.south()); ++ } else { ++ // Do nothing if updateRadius <= 0 (test mode) ++ } ++ } ++ ++ private void updateBlock(Level level, BlockPos blockPos) { ++ BlockState blockState = level.getBlockStateIfLoaded(blockPos); ++ ++ if (blockState != null && obfuscateGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)]) { ++ ((ServerLevel) level).getChunkSource().blockChanged(blockPos); ++ } ++ } ++ ++ @FunctionalInterface ++ private interface LayeredIntSupplier extends IntSupplier { ++ default void nextLayer() { ++ ++ } ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfo.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfo.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d98a3f5c54c67a673eb7dc456dd039cd78f9c34d +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfo.java +@@ -0,0 +1,80 @@ ++package com.destroystokyo.paper.antixray; ++ ++import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.level.chunk.Palette; ++ ++public class ChunkPacketInfo { ++ ++ private final ClientboundLevelChunkWithLightPacket chunkPacket; ++ private final LevelChunk chunk; ++ private final int[] bits; ++ private final Object[] palettes; ++ private final int[] indexes; ++ private final Object[][] presetValues; ++ private byte[] buffer; ++ ++ public ChunkPacketInfo(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk) { ++ this.chunkPacket = chunkPacket; ++ this.chunk = chunk; ++ int sections = chunk.getSectionsCount(); ++ bits = new int[sections]; ++ palettes = new Object[sections]; ++ indexes = new int[sections]; ++ presetValues = new Object[sections][]; ++ } ++ ++ public ClientboundLevelChunkWithLightPacket getChunkPacket() { ++ return chunkPacket; ++ } ++ ++ public LevelChunk getChunk() { ++ return chunk; ++ } ++ ++ public byte[] getBuffer() { ++ return buffer; ++ } ++ ++ public void setBuffer(byte[] buffer) { ++ this.buffer = buffer; ++ } ++ ++ public int getBits(int chunkSectionIndex) { ++ return bits[chunkSectionIndex]; ++ } ++ ++ public void setBits(int chunkSectionIndex, int bits) { ++ this.bits[chunkSectionIndex] = bits; ++ } ++ ++ @SuppressWarnings("unchecked") ++ public Palette getPalette(int chunkSectionIndex) { ++ return (Palette) palettes[chunkSectionIndex]; ++ } ++ ++ public void setPalette(int chunkSectionIndex, Palette palette) { ++ palettes[chunkSectionIndex] = palette; ++ } ++ ++ public int getIndex(int chunkSectionIndex) { ++ return indexes[chunkSectionIndex]; ++ } ++ ++ public void setIndex(int chunkSectionIndex, int index) { ++ indexes[chunkSectionIndex] = index; ++ } ++ ++ @SuppressWarnings("unchecked") ++ public T[] getPresetValues(int chunkSectionIndex) { ++ return (T[]) presetValues[chunkSectionIndex]; ++ } ++ ++ public void setPresetValues(int chunkSectionIndex, T[] presetValues) { ++ this.presetValues[chunkSectionIndex] = presetValues; ++ } ++ ++ public boolean isWritten(int chunkSectionIndex) { ++ return bits[chunkSectionIndex] != 0; ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfoAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfoAntiXray.java +new file mode 100644 +index 0000000000000000000000000000000000000000..80a2dfb266ae1221680a7b24fee2f7e2a8330b7d +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfoAntiXray.java +@@ -0,0 +1,29 @@ ++package com.destroystokyo.paper.antixray; ++ ++import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.chunk.LevelChunk; ++ ++public final class ChunkPacketInfoAntiXray extends ChunkPacketInfo implements Runnable { ++ ++ private final ChunkPacketBlockControllerAntiXray chunkPacketBlockControllerAntiXray; ++ private LevelChunk[] nearbyChunks; ++ ++ public ChunkPacketInfoAntiXray(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk, ChunkPacketBlockControllerAntiXray chunkPacketBlockControllerAntiXray) { ++ super(chunkPacket, chunk); ++ this.chunkPacketBlockControllerAntiXray = chunkPacketBlockControllerAntiXray; ++ } ++ ++ public LevelChunk[] getNearbyChunks() { ++ return nearbyChunks; ++ } ++ ++ public void setNearbyChunks(LevelChunk... nearbyChunks) { ++ this.nearbyChunks = nearbyChunks; ++ } ++ ++ @Override ++ public void run() { ++ chunkPacketBlockControllerAntiXray.obfuscate(this); ++ } ++} +diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundChunksBiomesPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundChunksBiomesPacket.java +index ccc8c32c27c19cb0f0b581ca6693cfa737cb1de1..3c1cad5c2b34047cec44734ba4e8348cabec6f80 100644 +--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundChunksBiomesPacket.java ++++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundChunksBiomesPacket.java +@@ -70,8 +70,10 @@ public record ClientboundChunksBiomesPacket(List blockEntitiesData; + +- public ClientboundLevelChunkPacketData(LevelChunk chunk) { ++ // Paper start - Anti-Xray - Add chunk packet info ++ @Deprecated @io.papermc.paper.annotation.DoNotUse public ClientboundLevelChunkPacketData(LevelChunk chunk) { this(chunk, null); } ++ public ClientboundLevelChunkPacketData(LevelChunk chunk, com.destroystokyo.paper.antixray.ChunkPacketInfo chunkPacketInfo) { ++ // Paper end + this.heightmaps = new CompoundTag(); + + for (Entry entry : chunk.getHeightmaps()) { +@@ -38,7 +41,14 @@ public class ClientboundLevelChunkPacketData { + } + + this.buffer = new byte[calculateChunkSize(chunk)]; +- extractChunkData(new FriendlyByteBuf(this.getWriteBuffer()), chunk); ++ ++ // Paper start - Anti-Xray - Add chunk packet info ++ if (chunkPacketInfo != null) { ++ chunkPacketInfo.setBuffer(this.buffer); ++ } ++ ++ extractChunkData(new FriendlyByteBuf(this.getWriteBuffer()), chunk, chunkPacketInfo); ++ // Paper end + this.blockEntitiesData = Lists.newArrayList(); + + for (Entry entry2 : chunk.getBlockEntities().entrySet()) { +@@ -85,9 +95,15 @@ public class ClientboundLevelChunkPacketData { + return byteBuf; + } + +- public static void extractChunkData(FriendlyByteBuf buf, LevelChunk chunk) { ++ // Paper start - Anti-Xray - Add chunk packet info ++ @Deprecated @io.papermc.paper.annotation.DoNotUse public static void extractChunkData(FriendlyByteBuf buf, LevelChunk chunk) { ClientboundLevelChunkPacketData.extractChunkData(buf, chunk, null); } ++ public static void extractChunkData(FriendlyByteBuf buf, LevelChunk chunk, com.destroystokyo.paper.antixray.ChunkPacketInfo chunkPacketInfo) { ++ int chunkSectionIndex = 0; ++ + for (LevelChunkSection levelChunkSection : chunk.getSections()) { +- levelChunkSection.write(buf); ++ levelChunkSection.write(buf, chunkPacketInfo, chunkSectionIndex); ++ chunkSectionIndex++; ++ // Paper end + } + } + +diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java +index 183b2191fa1c1b27adedf39593e1b5a223fb1279..8ead66c134688b11dca15f6509147e726f182e6a 100644 +--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java ++++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java +@@ -18,13 +18,30 @@ public class ClientboundLevelChunkWithLightPacket implements Packet chunkPacketInfo = modifyBlocks ? chunk.getLevel().chunkPacketBlockController.getChunkPacketInfo(this, chunk) : null; ++ this.chunkData = new ClientboundLevelChunkPacketData(chunk, chunkPacketInfo); ++ // Paper end + this.lightData = new ClientboundLightUpdatePacketData(chunkPos, lightProvider, skyBits, blockBits); ++ chunk.getLevel().chunkPacketBlockController.modifyBlocks(this, chunkPacketInfo); // Paper - Anti-Xray - Modify blocks + } + + private ClientboundLevelChunkWithLightPacket(RegistryFriendlyByteBuf buf) { +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index b7ca39623348090c5b85e2d80f67a1747553d7d7..bc609da7105293dfa4ed339f553be66b0a632698 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -575,7 +575,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + // Holder holder = worlddimension.type(); // CraftBukkit - decompile error + + // Objects.requireNonNull(minecraftserver); // CraftBukkit - decompile error +- super(iworlddataserver, resourcekey, minecraftserver.registryAccess(), worlddimension.type(), minecraftserver::getProfiler, false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> minecraftserver.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig, minecraftserver.registryAccess()))); // Paper - create paper world configs ++ super(iworlddataserver, resourcekey, minecraftserver.registryAccess(), worlddimension.type(), minecraftserver::getProfiler, false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> minecraftserver.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig, minecraftserver.registryAccess())), executor); // Paper - create paper world configs; Async-Anti-Xray: Pass executor + this.pvpMode = minecraftserver.isPvpAllowed(); + this.convertable = convertable_conversionsession; + this.uuid = WorldUUID.getUUID(convertable_conversionsession.levelDirectory.path().toFile()); +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +index 3dff328c5e4456aae291a8dec82766681f49af06..abb9a86cd42a34cf722a312068134e820ac21956 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +@@ -48,7 +48,7 @@ import org.bukkit.event.player.PlayerInteractEvent; + public class ServerPlayerGameMode { + + private static final Logger LOGGER = LogUtils.getLogger(); +- protected ServerLevel level; ++ public ServerLevel level; // Paper - Anti-Xray - protected -> public + protected final ServerPlayer player; + private GameType gameModeForPlayer; + @Nullable +@@ -329,6 +329,8 @@ public class ServerPlayerGameMode { + } + + } ++ ++ this.level.chunkPacketBlockController.onPlayerLeftClickBlock(this, pos, action, direction, worldHeight, sequence); // Paper - Anti-Xray + } + + public void destroyAndAck(BlockPos pos, int sequence, String reason) { +diff --git a/src/main/java/net/minecraft/server/network/PlayerChunkSender.java b/src/main/java/net/minecraft/server/network/PlayerChunkSender.java +index 27d0f1ed58948039004f8f1eba2f7f9609fdeec0..442bb60eb35002e54dcd6a893aca4a4584a8e017 100644 +--- a/src/main/java/net/minecraft/server/network/PlayerChunkSender.java ++++ b/src/main/java/net/minecraft/server/network/PlayerChunkSender.java +@@ -87,7 +87,10 @@ public class PlayerChunkSender { + + public static void sendChunk(ServerGamePacketListenerImpl handler, ServerLevel world, LevelChunk chunk) { // Paper - rewrite chunk loader - public + handler.player.serverLevel().chunkSource.chunkMap.getVisibleChunkIfPresent(chunk.getPos().toLong()).addPlayer(handler.player); +- handler.send(new ClientboundLevelChunkWithLightPacket(chunk, world.getLightEngine(), null, null)); ++ // Paper start - Anti-Xray ++ final boolean shouldModify = world.chunkPacketBlockController.shouldModify(handler.player, chunk); ++ handler.send(new ClientboundLevelChunkWithLightPacket(chunk, world.getLightEngine(), null, null, shouldModify)); ++ // Paper end - Anti-Xray + // Paper start - PlayerChunkLoadEvent + if (io.papermc.paper.event.packet.PlayerChunkLoadEvent.getHandlerList().getRegisteredListeners().length > 0) { + new io.papermc.paper.event.packet.PlayerChunkLoadEvent(new org.bukkit.craftbukkit.CraftChunk(chunk), handler.getPlayer().getBukkitEntity()).callEvent(); +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 57164b266d23715c224a85660531220477d4dda8..8f1a2de2a5542929f8a2fdd4d38de141eda985b2 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -420,7 +420,7 @@ public abstract class PlayerList { + .getHolderOrThrow(net.minecraft.world.level.biome.Biomes.PLAINS); + player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket( + new net.minecraft.world.level.chunk.EmptyLevelChunk(worldserver1, player.chunkPosition(), plains), +- worldserver1.getLightEngine(), (java.util.BitSet)null, (java.util.BitSet) null) ++ worldserver1.getLightEngine(), (java.util.BitSet)null, (java.util.BitSet) null, true) + ); + } + // Paper end - Send empty chunk +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 2d829eb4d39aec82c23a55f30f22a9a5f7003d27..ff8d947c5a498e097a4c480a5dc9af71b0be5ee9 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -169,6 +169,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + } + // Paper end - add paper world config + ++ public final com.destroystokyo.paper.antixray.ChunkPacketBlockController chunkPacketBlockController; // Paper - Anti-Xray + public final co.aikar.timings.WorldTimingsHandler timings; // Paper + public static BlockPos lastPhysicsProblem; // Spigot + private org.spigotmc.TickLimiter entityLimiter; +@@ -203,7 +204,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + + public abstract ResourceKey getTypeKey(); + +- protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, RegistryAccess iregistrycustom, Holder holder, Supplier supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function paperWorldConfigCreator) { // Paper - create paper world config ++ protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, RegistryAccess iregistrycustom, Holder holder, Supplier supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - create paper world config; Async-Anti-Xray: Pass executor + this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot + this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config + this.generator = gen; +@@ -285,6 +286,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + this.timings = new co.aikar.timings.WorldTimingsHandler(this); // Paper - code below can generate new world and access timings + this.entityLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.entityMaxTickTime); + this.tileLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.tileMaxTickTime); ++ this.chunkPacketBlockController = this.paperConfig().anticheat.antiXray.enabled ? new com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor) : com.destroystokyo.paper.antixray.ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray + } + + // Paper start - Cancel hit for vanished players +@@ -560,6 +562,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + // CraftBukkit end + + BlockState iblockdata1 = chunk.setBlockState(pos, state, (flags & 64) != 0, (flags & 1024) == 0); // CraftBukkit custom NO_PLACE flag ++ this.chunkPacketBlockController.onBlockChange(this, pos, state, iblockdata1, flags, maxUpdateDepth); // Paper - Anti-Xray + + if (iblockdata1 == null) { + // CraftBukkit start - remove blockstate if failed (or the same) +diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java +index 383dc47c81b3f34e8166bce180a51a2ccbfaf6ca..1aac95b03a9e2e37c24f2a30bcb259c1424e1c78 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java +@@ -155,17 +155,17 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom + } + } + +- ChunkAccess.replaceMissingSections(biomeRegistry, this.sections); ++ this.replaceMissingSections(biomeRegistry, this.sections); // Paper - Anti-Xray - make it a non-static method + // CraftBukkit start + this.biomeRegistry = biomeRegistry; + } + public final Registry biomeRegistry; + // CraftBukkit end + +- private static void replaceMissingSections(Registry biomeRegistry, LevelChunkSection[] sectionArray) { ++ private void replaceMissingSections(Registry biomeRegistry, LevelChunkSection[] sectionArray) { // Paper - Anti-Xray - static -> non-static + for (int i = 0; i < sectionArray.length; ++i) { + if (sectionArray[i] == null) { +- sectionArray[i] = new LevelChunkSection(biomeRegistry); ++ sectionArray[i] = new LevelChunkSection(biomeRegistry, this.levelHeightAccessor instanceof net.minecraft.world.level.Level ? (net.minecraft.world.level.Level) this.levelHeightAccessor : null, this.chunkPos, this.levelHeightAccessor.getSectionYFromSectionIndex(i)); // Paper start - Anti-Xray - Add parameters + } + } + +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +index 5d94aee1303d9eca5f1fa9a2e033ad0d12909635..424c4613e202c6ba50fa0de65d2526d400a8e299 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -93,7 +93,7 @@ public class LevelChunk extends ChunkAccess { + } + + public LevelChunk(Level world, ChunkPos pos, UpgradeData upgradeData, LevelChunkTicks blockTickScheduler, LevelChunkTicks fluidTickScheduler, long inhabitedTime, @Nullable LevelChunkSection[] sectionArrayInitializer, @Nullable LevelChunk.PostLoadProcessor entityLoader, @Nullable BlendingData blendingData) { +- super(pos, upgradeData, world, world.registryAccess().registryOrThrow(Registries.BIOME), inhabitedTime, sectionArrayInitializer, blendingData); ++ super(pos, upgradeData, world, net.minecraft.server.MinecraftServer.getServer().registryAccess().registryOrThrow(Registries.BIOME), inhabitedTime, sectionArrayInitializer, blendingData); // Paper - Anti-Xray - The world isn't ready yet, use server singleton for registry + this.tickersInLevel = Maps.newHashMap(); + this.level = (ServerLevel) world; // CraftBukkit - type + this.gameEventListenerRegistrySections = new Int2ObjectOpenHashMap(); +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +index 90d1c3e23e753c29660f7d993b3c90ac022941c3..f2e11bff414b521295bde721e01ae2166a6a3fd6 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +@@ -33,9 +33,12 @@ public class LevelChunkSection { + this.recalcBlockCounts(); + } + +- public LevelChunkSection(Registry biomeRegistry) { +- this.states = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES); +- this.biomes = new PalettedContainer<>(biomeRegistry.asHolderIdMap(), biomeRegistry.getHolderOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES); ++ // Paper start - Anti-Xray - Add parameters ++ @Deprecated @io.papermc.paper.annotation.DoNotUse public LevelChunkSection(Registry biomeRegistry) { this(biomeRegistry, null, null, 0); } ++ public LevelChunkSection(Registry biomeRegistry, net.minecraft.world.level.Level level, net.minecraft.world.level.ChunkPos chunkPos, int chunkSectionY) { ++ // Paper end ++ this.states = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES, level == null || level.chunkPacketBlockController == null ? null : level.chunkPacketBlockController.getPresetBlockStates(level, chunkPos, chunkSectionY)); // Paper - Anti-Xray - Add preset block states ++ this.biomes = new PalettedContainer<>(biomeRegistry.asHolderIdMap(), biomeRegistry.getHolderOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES, null); // Paper - Anti-Xray - Add preset biomes + } + + public BlockState getBlockState(int x, int y, int z) { +@@ -172,10 +175,13 @@ public class LevelChunkSection { + this.biomes = datapaletteblock; + } + +- public void write(FriendlyByteBuf buf) { ++ // Paper start - Anti-Xray - Add chunk packet info ++ @Deprecated @io.papermc.paper.annotation.DoNotUse public void write(FriendlyByteBuf buf) { this.write(buf, null, 0); } ++ public void write(FriendlyByteBuf buf, com.destroystokyo.paper.antixray.ChunkPacketInfo chunkPacketInfo, int chunkSectionIndex) { + buf.writeShort(this.nonEmptyBlockCount); +- this.states.write(buf); +- this.biomes.write(buf); ++ this.states.write(buf, chunkPacketInfo, chunkSectionIndex); ++ this.biomes.write(buf, null, chunkSectionIndex); ++ // Paper end + } + + public int getSerializedSize() { +diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java +index fa9df6ebcd90d4e9e5836a37212b1f60665783b1..926c81a25180d508d662eb3fa35f771636164694 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java ++++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java +@@ -28,6 +28,7 @@ public class PalettedContainer implements PaletteResize, PalettedContainer + private static final int MIN_PALETTE_BITS = 0; + private final PaletteResize dummyPaletteResize = (newSize, added) -> 0; + public final IdMap registry; ++ private final T @org.jetbrains.annotations.Nullable [] presetValues; // Paper - Anti-Xray - Add preset values + private volatile PalettedContainer.Data data; + private final PalettedContainer.Strategy strategy; + // private final ThreadingDetector threadingDetector = new ThreadingDetector("PalettedContainer"); // Paper - unused +@@ -40,14 +41,19 @@ public class PalettedContainer implements PaletteResize, PalettedContainer + // this.threadingDetector.checkAndUnlock(); // Paper - disable this + } + +- public static Codec> codecRW(IdMap idList, Codec entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue) { +- PalettedContainerRO.Unpacker> unpacker = PalettedContainer::unpack; ++ // Paper start - Anti-Xray - Add preset values ++ @Deprecated @io.papermc.paper.annotation.DoNotUse public static Codec> codecRW(IdMap idList, Codec entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue) { return PalettedContainer.codecRW(idList, entryCodec, paletteProvider, defaultValue, null); } ++ public static Codec> codecRW(IdMap idList, Codec entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue, T @org.jetbrains.annotations.Nullable [] presetValues) { ++ PalettedContainerRO.Unpacker> unpacker = (idListx, paletteProviderx, serialized) -> { ++ return unpack(idListx, paletteProviderx, serialized, defaultValue, presetValues); ++ }; ++ // Paper end + return codec(idList, entryCodec, paletteProvider, defaultValue, unpacker); + } + + public static Codec> codecRO(IdMap idList, Codec entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue) { + PalettedContainerRO.Unpacker> unpacker = (idListx, paletteProviderx, serialized) -> unpack( +- idListx, paletteProviderx, serialized ++ idListx, paletteProviderx, serialized, defaultValue, null // Paper - Anti-Xray - Add preset values + ) + .map(result -> (PalettedContainerRO)result); + return codec(idList, entryCodec, paletteProvider, defaultValue, unpacker); +@@ -71,25 +77,58 @@ public class PalettedContainer implements PaletteResize, PalettedContainer + ); + } + ++ // Paper start - Anti-Xray - Add preset values ++ @Deprecated @io.papermc.paper.annotation.DoNotUse public PalettedContainer(IdMap idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Configuration dataProvider, BitStorage storage, List paletteEntries) { this(idList, paletteProvider, dataProvider, storage, paletteEntries, null, null); } + public PalettedContainer( + IdMap idList, + PalettedContainer.Strategy paletteProvider, + PalettedContainer.Configuration dataProvider, + BitStorage storage, +- List paletteEntries ++ List paletteEntries, T defaultValue, T @org.jetbrains.annotations.Nullable [] presetValues + ) { ++ this.presetValues = presetValues; + this.registry = idList; + this.strategy = paletteProvider; + this.data = new PalettedContainer.Data<>(dataProvider, storage, dataProvider.factory().create(dataProvider.bits(), idList, this, paletteEntries)); ++ ++ if (presetValues != null && (dataProvider.factory() == PalettedContainer.Strategy.SINGLE_VALUE_PALETTE_FACTORY ? this.data.palette.valueFor(0) != defaultValue : dataProvider.factory() != PalettedContainer.Strategy.GLOBAL_PALETTE_FACTORY)) { ++ // In 1.18 Mojang unfortunately removed code that already handled possible resize operations on read from disk for us ++ // We readd this here but in a smarter way than it was before ++ int maxSize = 1 << dataProvider.bits(); ++ ++ for (T presetValue : presetValues) { ++ if (this.data.palette.getSize() >= maxSize) { ++ java.util.Set allValues = new java.util.HashSet<>(paletteEntries); ++ allValues.addAll(Arrays.asList(presetValues)); ++ int newBits = Mth.ceillog2(allValues.size()); ++ ++ if (newBits > dataProvider.bits()) { ++ this.onResize(newBits, null); ++ } ++ ++ break; ++ } ++ ++ this.data.palette.idFor(presetValue); ++ } ++ } ++ // Paper end + } + +- private PalettedContainer(IdMap idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Data data) { ++ // Paper start - Anti-Xray - Add preset values ++ private PalettedContainer(IdMap idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Data data, T @org.jetbrains.annotations.Nullable [] presetValues) { ++ this.presetValues = presetValues; ++ // Paper end + this.registry = idList; + this.strategy = paletteProvider; + this.data = data; + } + +- public PalettedContainer(IdMap idList, T object, PalettedContainer.Strategy paletteProvider) { ++ // Paper start - Anti-Xray - Add preset values ++ @Deprecated @io.papermc.paper.annotation.DoNotUse public PalettedContainer(IdMap idList, T object, PalettedContainer.Strategy paletteProvider) { this(idList, object, paletteProvider, null); } ++ public PalettedContainer(IdMap idList, T object, PalettedContainer.Strategy paletteProvider, T @org.jetbrains.annotations.Nullable [] presetValues) { ++ this.presetValues = presetValues; ++ // Paper end + this.strategy = paletteProvider; + this.registry = idList; + this.data = this.createOrReuseData(null, 0); +@@ -106,11 +145,33 @@ public class PalettedContainer implements PaletteResize, PalettedContainer + @Override + public synchronized int onResize(int newBits, T object) { // Paper - synchronize + PalettedContainer.Data data = this.data; ++ ++ // Paper start - Anti-Xray - Add preset values ++ if (this.presetValues != null && object != null && data.configuration().factory() == PalettedContainer.Strategy.SINGLE_VALUE_PALETTE_FACTORY) { ++ int duplicates = 0; ++ List presetValues = Arrays.asList(this.presetValues); ++ duplicates += presetValues.contains(object) ? 1 : 0; ++ duplicates += presetValues.contains(data.palette.valueFor(0)) ? 1 : 0; ++ newBits = Mth.ceillog2((1 << this.strategy.calculateBitsForSerialization(this.registry, 1 << newBits)) + presetValues.size() - duplicates); ++ } ++ + PalettedContainer.Data data2 = this.createOrReuseData(data, newBits); + data2.copyFrom(data.palette, data.storage); + this.data = data2; +- return data2.palette.idFor(object); ++ this.addPresetValues(); ++ return object == null ? -1 : data2.palette.idFor(object); ++ // Paper end ++ } ++ ++ // Paper start - Anti-Xray - Add preset values ++ private void addPresetValues() { ++ if (this.presetValues != null && this.data.configuration().factory() != PalettedContainer.Strategy.GLOBAL_PALETTE_FACTORY) { ++ for (T presetValue : this.presetValues) { ++ this.data.palette.idFor(presetValue); ++ } ++ } + } ++ // Paper end + + public T getAndSet(int x, int y, int z, T value) { + this.acquire(); +@@ -177,24 +238,33 @@ public class PalettedContainer implements PaletteResize, PalettedContainer + data.palette.read(buf); + buf.readLongArray(data.storage.getRaw()); + this.data = data; ++ this.addPresetValues(); // Paper - Anti-Xray - Add preset values (inefficient, but this isn't used by the server) + } finally { + this.release(); + } + } + ++ // Paper start - Anti-Xray; Add chunk packet info ++ @Override ++ @Deprecated @io.papermc.paper.annotation.DoNotUse public void write(FriendlyByteBuf buf) { this.write(buf, null, 0); } + @Override +- public synchronized void write(FriendlyByteBuf buf) { // Paper - synchronize ++ public synchronized void write(FriendlyByteBuf buf, @Nullable com.destroystokyo.paper.antixray.ChunkPacketInfo chunkPacketInfo, int chunkSectionIndex) { // Paper - Synchronize + this.acquire(); + + try { +- this.data.write(buf); ++ this.data.write(buf, chunkPacketInfo, chunkSectionIndex); ++ ++ if (chunkPacketInfo != null) { ++ chunkPacketInfo.setPresetValues(chunkSectionIndex, this.presetValues); ++ } ++ // Paper end + } finally { + this.release(); + } + } + + private static DataResult> unpack( +- IdMap idList, PalettedContainer.Strategy paletteProvider, PalettedContainerRO.PackedData serialized ++ IdMap idList, PalettedContainer.Strategy paletteProvider, PalettedContainerRO.PackedData serialized, T defaultValue, T @org.jetbrains.annotations.Nullable [] presetValues // Paper - Anti-Xray - Add preset values + ) { + List list = serialized.paletteEntries(); + int i = paletteProvider.size(); +@@ -227,7 +297,7 @@ public class PalettedContainer implements PaletteResize, PalettedContainer + } + } + +- return DataResult.success(new PalettedContainer<>(idList, paletteProvider, configuration, bitStorage, list)); ++ return DataResult.success(new PalettedContainer<>(idList, paletteProvider, configuration, bitStorage, list, defaultValue, presetValues)); // Paper - Anti-Xray - Add preset values + } + + @Override +@@ -284,12 +354,12 @@ public class PalettedContainer implements PaletteResize, PalettedContainer + } + + public PalettedContainer copy() { +- return new PalettedContainer<>(this.registry, this.strategy, this.data.copy()); ++ return new PalettedContainer<>(this.registry, this.strategy, this.data.copy(), this.presetValues); // Paper - Anti-Xray - Add preset values + } + + @Override + public PalettedContainer recreate() { +- return new PalettedContainer<>(this.registry, this.data.palette.valueFor(0), this.strategy); ++ return new PalettedContainer<>(this.registry, this.data.palette.valueFor(0), this.strategy, this.presetValues); // Paper - Anti-Xray - Add preset values + } + + @Override +@@ -328,9 +398,18 @@ public class PalettedContainer implements PaletteResize, PalettedContainer + return 1 + this.palette.getSerializedSize() + VarInt.getByteSize(this.storage.getRaw().length) + this.storage.getRaw().length * 8; + } + +- public void write(FriendlyByteBuf buf) { ++ // Paper start - Anti-Xray - Add chunk packet info ++ public void write(FriendlyByteBuf buf, @Nullable com.destroystokyo.paper.antixray.ChunkPacketInfo chunkPacketInfo, int chunkSectionIndex) { + buf.writeByte(this.storage.getBits()); + this.palette.write(buf); ++ ++ if (chunkPacketInfo != null) { ++ chunkPacketInfo.setBits(chunkSectionIndex, this.configuration.bits()); ++ chunkPacketInfo.setPalette(chunkSectionIndex, this.palette); ++ chunkPacketInfo.setIndex(chunkSectionIndex, buf.writerIndex() + VarInt.getByteSize(this.storage.getRaw().length)); ++ } ++ // Paper end ++ + buf.writeLongArray(this.storage.getRaw()); + } + +diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainerRO.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainerRO.java +index 9a2bf744abd8916d492e901be889223591bac3fd..1dd415c96d17eff8e7555c33d3c52e57f2559fa5 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainerRO.java ++++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainerRO.java +@@ -14,7 +14,10 @@ public interface PalettedContainerRO { + + void getAll(Consumer action); + +- void write(FriendlyByteBuf buf); ++ // Paper start - Anti-Xray - Add chunk packet info ++ @Deprecated @io.papermc.paper.annotation.DoNotUse void write(FriendlyByteBuf buf); ++ void write(FriendlyByteBuf buf, @javax.annotation.Nullable com.destroystokyo.paper.antixray.ChunkPacketInfo chunkPacketInfo, int chunkSectionIndex); ++ // Paper end + + int getSerializedSize(); + +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +index b2e8082dda0d0ca32bfc32cbf9d8ae9822febc30..e64fe79b231987e240d7482c0d6fa3c89dc97462 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +@@ -73,7 +73,7 @@ import org.slf4j.Logger; + + public class ChunkSerializer { + +- public static final Codec> BLOCK_STATE_CODEC = PalettedContainer.codecRW(Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState()); ++ public static final Codec> BLOCK_STATE_CODEC = PalettedContainer.codecRW(Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState(), null); // Paper - Anti-Xray - Add preset block states + private static final Logger LOGGER = LogUtils.getLogger(); + private static final String TAG_UPGRADE_DATA = "UpgradeData"; + private static final String BLOCK_TICKS_TAG = "block_ticks"; +@@ -171,13 +171,17 @@ public class ChunkSerializer { + + if (k >= 0 && k < achunksection.length) { + PalettedContainer datapaletteblock; ++ // Paper start - Anti-Xray - Add preset block states ++ BlockState[] presetBlockStates = world.chunkPacketBlockController.getPresetBlockStates(world, chunkPos, b0); + + if (nbttagcompound1.contains("block_states", 10)) { +- datapaletteblock = (PalettedContainer) ChunkSerializer.BLOCK_STATE_CODEC.parse(NbtOps.INSTANCE, nbttagcompound1.getCompound("block_states")).promotePartial((s) -> { ++ Codec> blockStateCodec = presetBlockStates == null ? ChunkSerializer.BLOCK_STATE_CODEC : PalettedContainer.codecRW(Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState(), presetBlockStates); ++ datapaletteblock = blockStateCodec.parse(NbtOps.INSTANCE, nbttagcompound1.getCompound("block_states")).promotePartial((s) -> { + ChunkSerializer.logErrors(chunkPos, b0, s); + }).getOrThrow(ChunkSerializer.ChunkReadException::new); + } else { +- datapaletteblock = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES); ++ datapaletteblock = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES, presetBlockStates); ++ // Paper end + } + + PalettedContainer object; // CraftBukkit - read/write +@@ -187,7 +191,7 @@ public class ChunkSerializer { + ChunkSerializer.logErrors(chunkPos, b0, s); + }).getOrThrow(ChunkSerializer.ChunkReadException::new); + } else { +- object = new PalettedContainer<>(iregistry.asHolderIdMap(), iregistry.getHolderOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES); ++ object = new PalettedContainer<>(iregistry.asHolderIdMap(), iregistry.getHolderOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES, null); // Paper - Anti-Xray - Add preset biomes + } + + LevelChunkSection chunksection = new LevelChunkSection(datapaletteblock, (PalettedContainer) object); // CraftBukkit - read/write +@@ -423,7 +427,7 @@ public class ChunkSerializer { + + // CraftBukkit start - read/write + private static Codec>> makeBiomeCodecRW(Registry iregistry) { +- return PalettedContainer.codecRW(iregistry.asHolderIdMap(), iregistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, iregistry.getHolderOrThrow(Biomes.PLAINS)); ++ return PalettedContainer.codecRW(iregistry.asHolderIdMap(), iregistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, iregistry.getHolderOrThrow(Biomes.PLAINS), null); // Paper - Anti-Xray - Add preset biomes + } + // CraftBukkit end + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +index 1e2530c9e5212b6d2bdbc94817beddb4247dac73..82b4bd669c57b18fb0b443bcd94495023cd5a528 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +@@ -55,7 +55,7 @@ public class CraftChunk implements Chunk { + private final ServerLevel worldServer; + private final int x; + private final int z; +- private static final PalettedContainer emptyBlockIDs = new PalettedContainer<>(net.minecraft.world.level.block.Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES); ++ private static final PalettedContainer emptyBlockIDs = new PalettedContainer<>(net.minecraft.world.level.block.Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES, null); // Paper - Anti-Xray - Add preset block states + private static final byte[] FULL_LIGHT = new byte[2048]; + private static final byte[] EMPTY_LIGHT = new byte[2048]; + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 66fa93a44a348f9cd300ec0c4a2d09a7a2bf47e1..ac9873c430a3ece5c4b5f5305496639a54ce35f5 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -2659,7 +2659,7 @@ public final class CraftServer implements Server { + public ChunkGenerator.ChunkData createChunkData(World world) { + Preconditions.checkArgument(world != null, "World cannot be null"); + ServerLevel handle = ((CraftWorld) world).getHandle(); +- return new OldCraftChunkData(world.getMinHeight(), world.getMaxHeight(), handle.registryAccess().registryOrThrow(Registries.BIOME)); ++ return new OldCraftChunkData(world.getMinHeight(), world.getMaxHeight(), handle.registryAccess().registryOrThrow(Registries.BIOME), world); // Paper - Anti-Xray - Add parameters + } + + // Paper start - Allow delegation to vanilla chunk gen +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 0ce0589204a01051aa1251d6af752eed4ac69c0f..f2b20ed5063a293f0b464548f590d652170cd1d8 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -541,11 +541,16 @@ public class CraftWorld extends CraftRegionAccessor implements World { + List playersInRange = playerChunk.playerProvider.getPlayers(playerChunk.getPos(), false); + if (playersInRange.isEmpty()) return true; // Paper - rewrite player chunk loader + +- ClientboundLevelChunkWithLightPacket refreshPacket = new ClientboundLevelChunkWithLightPacket(chunk, this.world.getLightEngine(), null, null); ++ // Paper start - Anti-Xray - Bypass ++ Map refreshPackets = new HashMap<>(); + for (ServerPlayer player : playersInRange) { + if (player.connection == null) continue; + +- player.connection.send(refreshPacket); ++ Boolean shouldModify = chunk.getLevel().chunkPacketBlockController.shouldModify(player, chunk); ++ player.connection.send(refreshPackets.computeIfAbsent(shouldModify, s -> { // Use connection to prevent creating firing event ++ return new ClientboundLevelChunkWithLightPacket(chunk, this.world.getLightEngine(), null, null, (Boolean) s); ++ })); ++ // Paper end + } + // Paper - rewrite player chunk loader + +diff --git a/src/main/java/org/bukkit/craftbukkit/generator/OldCraftChunkData.java b/src/main/java/org/bukkit/craftbukkit/generator/OldCraftChunkData.java +index 9b640705f2c810160aa7fea5006429ec41d0c858..44a010590e830fd238cf6fdda443e28b72022e66 100644 +--- a/src/main/java/org/bukkit/craftbukkit/generator/OldCraftChunkData.java ++++ b/src/main/java/org/bukkit/craftbukkit/generator/OldCraftChunkData.java +@@ -27,8 +27,13 @@ public final class OldCraftChunkData implements ChunkGenerator.ChunkData { + private final Registry biomes; + private Set tiles; + private final Set lights = new HashSet<>(); ++ // Paper start - Anti-Xray - Add parameters ++ private final org.bukkit.World world; + +- public OldCraftChunkData(int minHeight, int maxHeight, Registry biomes) { ++ @Deprecated @io.papermc.paper.annotation.DoNotUse public OldCraftChunkData(int minHeight, int maxHeight, Registry biomes) { this(minHeight, maxHeight, biomes, null); } ++ public OldCraftChunkData(int minHeight, int maxHeight, Registry biomes, org.bukkit.World world) { ++ this.world = world; ++ // Paper end + this.minHeight = minHeight; + this.maxHeight = maxHeight; + this.biomes = biomes; +@@ -176,7 +181,7 @@ public final class OldCraftChunkData implements ChunkGenerator.ChunkData { + int offset = (y - this.minHeight) >> 4; + LevelChunkSection section = this.sections[offset]; + if (create && section == null) { +- this.sections[offset] = section = new LevelChunkSection(this.biomes); ++ this.sections[offset] = section = new LevelChunkSection(this.biomes, this.world instanceof org.bukkit.craftbukkit.CraftWorld ? ((org.bukkit.craftbukkit.CraftWorld) this.world).getHandle() : null, null, offset + (this.minHeight >> 4)); // Paper - Anti-Xray - Add parameters + } + return section; + } diff --git a/patches/server/0996-Optional-per-player-mob-spawns.patch b/patches/server/0996-Optional-per-player-mob-spawns.patch deleted file mode 100644 index b9fe80f3e4..0000000000 --- a/patches/server/0996-Optional-per-player-mob-spawns.patch +++ /dev/null @@ -1,255 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: kickash32 -Date: Mon, 19 Aug 2019 01:27:58 +0500 -Subject: [PATCH] Optional per player mob spawns - - -diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java -index 1d2d4d38ff3414896d07f7f4e40d0edb9a8c3993..56f855ed9dbbb9a7025bef6f9e98a8cdcf2ad4fc 100644 ---- a/src/main/java/net/minecraft/server/level/ChunkMap.java -+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java -@@ -291,9 +291,28 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - }); - } - -+ // Paper start - Optional per player mob spawns -+ public void updatePlayerMobTypeMap(final Entity entity) { -+ if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { -+ return; -+ } -+ int index = entity.getType().getCategory().ordinal(); -+ -+ final com.destroystokyo.paper.util.maplist.ReferenceList inRange = -+ this.getNearbyPlayers().getPlayers(entity.chunkPosition(), io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE); -+ if (inRange == null) { -+ return; -+ } -+ final Object[] backingSet = inRange.getRawData(); -+ for (int i = 0, len = inRange.size(); i < len; i++) { -+ ++((ServerPlayer)backingSet[i]).mobCounts[index]; -+ } -+ } -+ - public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) { -- return -1; -+ return player.mobCounts[mobCategory.ordinal()]; - } -+ // Paper end - Optional per player mob spawns - - private static double euclideanDistanceSquared(ChunkPos pos, Entity entity) { - double d0 = (double) SectionPos.sectionToBlockCoord(pos.x, 8); -diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -index d1728e13a7b649f308bde90ab633c79d86c10822..002aad174fb1781ee963414037f84e5120488592 100644 ---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java -+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -@@ -516,7 +516,19 @@ public class ServerChunkCache extends ChunkSource { - gameprofilerfiller.popPush("naturalSpawnCount"); - this.level.timings.countNaturalMobs.startTiming(); // Paper - timings - int k = this.distanceManager.getNaturalSpawnChunkCount(); -- NaturalSpawner.SpawnState spawnercreature_d = NaturalSpawner.createState(k, this.level.getAllEntities(), this::getFullChunk, new LocalMobCapCalculator(this.chunkMap)); -+ // Paper start - Optional per player mob spawns -+ int naturalSpawnChunkCount = k; -+ NaturalSpawner.SpawnState spawnercreature_d; // moved down -+ if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled -+ // re-set mob counts -+ for (ServerPlayer player : this.level.players) { -+ Arrays.fill(player.mobCounts, 0); -+ } -+ spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true); -+ } else { -+ spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); -+ } -+ // Paper end - Optional per player mob spawns - this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings - - this.lastSpawnState = spawnercreature_d; -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index fc8e9cd699bdefb6ddadc6aa30e6459417c73194..61e256224c6d7c944bee0e11d0edcf05b8a5ecd3 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -271,6 +271,10 @@ public class ServerPlayer extends Player { - public boolean queueHealthUpdatePacket; - public net.minecraft.network.protocol.game.ClientboundSetHealthPacket queuedHealthUpdatePacket; - // Paper end - cancellable death event -+ // Paper start - Optional per player mob spawns -+ public static final int MOBCATEGORY_TOTAL_ENUMS = net.minecraft.world.entity.MobCategory.values().length; -+ public final int[] mobCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper -+ // Paper end - Optional per player mob spawns - - // CraftBukkit start - public CraftPlayer.TransferCookieConnection transferCookieConnection; -diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java -index ea6533c1ac218aa075da3401807a06fcb7892321..0679636530ca1afd077c43b0fa605a2ac74e0522 100644 ---- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java -+++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java -@@ -67,6 +67,12 @@ public final class NaturalSpawner { - private NaturalSpawner() {} - - public static NaturalSpawner.SpawnState createState(int spawningChunkCount, Iterable entities, NaturalSpawner.ChunkGetter chunkSource, LocalMobCapCalculator densityCapper) { -+ // Paper start - Optional per player mob spawns -+ return createState(spawningChunkCount, entities, chunkSource, densityCapper, false); -+ } -+ -+ public static NaturalSpawner.SpawnState createState(int spawningChunkCount, Iterable entities, NaturalSpawner.ChunkGetter chunkSource, LocalMobCapCalculator densityCapper, boolean countMobs) { -+ // Paper end - Optional per player mob spawns - PotentialCalculator spawnercreatureprobabilities = new PotentialCalculator(); - Object2IntOpenHashMap object2intopenhashmap = new Object2IntOpenHashMap(); - Iterator iterator = entities.iterator(); -@@ -99,11 +105,16 @@ public final class NaturalSpawner { - spawnercreatureprobabilities.addCharge(entity.blockPosition(), biomesettingsmobs_b.charge()); - } - -- if (entity instanceof Mob) { -+ if (densityCapper != null && entity instanceof Mob) { // Paper - Optional per player mob spawns - densityCapper.addMob(chunk.getPos(), enumcreaturetype); - } - - object2intopenhashmap.addTo(enumcreaturetype, 1); -+ // Paper start - Optional per player mob spawns -+ if (countMobs) { -+ chunk.level.getChunkSource().chunkMap.updatePlayerMobTypeMap(entity); -+ } -+ // Paper end - Optional per player mob spawns - }); - } - } -@@ -138,13 +149,35 @@ public final class NaturalSpawner { - continue; - } - -- if ((spawnAnimals || !enumcreaturetype.isFriendly()) && (spawnMonsters || enumcreaturetype.isFriendly()) && (rareSpawn || !enumcreaturetype.isPersistent()) && info.canSpawnForCategory(enumcreaturetype, chunk.getPos(), limit)) { -+ // Paper start - Optional per player mob spawns; only allow spawns upto the limit per chunk and update count afterwards -+ int currEntityCount = info.mobCategoryCounts.getInt(enumcreaturetype); -+ int k1 = limit * info.getSpawnableChunkCount() / NaturalSpawner.MAGIC_NUMBER; -+ int difference = k1 - currEntityCount; -+ -+ if (world.paperConfig().entities.spawning.perPlayerMobSpawns) { -+ int minDiff = Integer.MAX_VALUE; -+ final com.destroystokyo.paper.util.maplist.ReferenceList inRange = -+ world.chunkSource.chunkMap.getNearbyPlayers().getPlayers(chunk.getPos(), io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE); -+ if (inRange != null) { -+ final Object[] backingSet = inRange.getRawData(); -+ for (int k = 0, len = inRange.size(); k < len; k++) { -+ minDiff = Math.min(limit - world.getChunkSource().chunkMap.getMobCountNear((net.minecraft.server.level.ServerPlayer)backingSet[k], enumcreaturetype), minDiff); -+ } -+ } -+ difference = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff; -+ } -+ if ((spawnAnimals || !enumcreaturetype.isFriendly()) && (spawnMonsters || enumcreaturetype.isFriendly()) && (rareSpawn || !enumcreaturetype.isPersistent()) && difference > 0) { -+ // Paper end - Optional per player mob spawns - // CraftBukkit end - Objects.requireNonNull(info); - NaturalSpawner.SpawnPredicate spawnercreature_c = info::canSpawn; - - Objects.requireNonNull(info); -- NaturalSpawner.spawnCategoryForChunk(enumcreaturetype, world, chunk, spawnercreature_c, info::afterSpawn); -+ // Paper start - Optional per player mob spawns -+ int spawnCount = NaturalSpawner.spawnCategoryForChunk(enumcreaturetype, world, chunk, spawnercreature_c, info::afterSpawn, -+ difference, world.paperConfig().entities.spawning.perPlayerMobSpawns ? world.getChunkSource().chunkMap::updatePlayerMobTypeMap : null); -+ info.mobCategoryCounts.mergeInt(enumcreaturetype, spawnCount, Integer::sum); -+ // Paper end - Optional per player mob spawns - } - } - -@@ -163,11 +196,17 @@ public final class NaturalSpawner { - // Paper end - Add mobcaps commands - - public static void spawnCategoryForChunk(MobCategory group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) { -+ // Paper start - Optional per player mob spawns -+ spawnCategoryForChunk(group, world, chunk, checker, runner, Integer.MAX_VALUE, null); -+ } -+ public static int spawnCategoryForChunk(MobCategory group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner, int maxSpawns, Consumer trackEntity) { -+ // Paper end - Optional per player mob spawns - BlockPos blockposition = NaturalSpawner.getRandomPosWithin(world, chunk); - - if (blockposition.getY() >= world.getMinBuildHeight() + 1) { -- NaturalSpawner.spawnCategoryForPosition(group, world, chunk, blockposition, checker, runner); -+ return NaturalSpawner.spawnCategoryForPosition(group, world, chunk, blockposition, checker, runner, maxSpawns, trackEntity); // Paper - Optional per player mob spawns - } -+ return 0; // Paper - Optional per player mob spawns - } - - @VisibleForDebug -@@ -178,15 +217,21 @@ public final class NaturalSpawner { - }); - } - -+ // Paper start - Optional per player mob spawns - public static void spawnCategoryForPosition(MobCategory group, ServerLevel world, ChunkAccess chunk, BlockPos pos, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) { -+ spawnCategoryForPosition(group, world,chunk, pos, checker, runner, Integer.MAX_VALUE, null); -+ } -+ public static int spawnCategoryForPosition(MobCategory group, ServerLevel world, ChunkAccess chunk, BlockPos pos, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner, int maxSpawns, Consumer trackEntity) { -+ // Paper end - Optional per player mob spawns - StructureManager structuremanager = world.structureManager(); - ChunkGenerator chunkgenerator = world.getChunkSource().getGenerator(); - int i = pos.getY(); - BlockState iblockdata = world.getBlockStateIfLoadedAndInBounds(pos); // Paper - don't load chunks for mob spawn -+ int j = 0; // Paper - Optional per player mob spawns; moved up - - if (iblockdata != null && !iblockdata.isRedstoneConductor(chunk, pos)) { // Paper - don't load chunks for mob spawn - BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos(); -- int j = 0; -+ //int j = 0; // Paper - Optional per player mob spawns; moved up - int k = 0; - - while (k < 3) { -@@ -228,14 +273,14 @@ public final class NaturalSpawner { - // Paper start - PreCreatureSpawnEvent - PreSpawnStatus doSpawning = isValidSpawnPostitionForType(world, group, structuremanager, chunkgenerator, biomesettingsmobs_c, blockposition_mutableblockposition, d2); - if (doSpawning == PreSpawnStatus.ABORT) { -- return; -+ return j; // Paper - Optional per player mob spawns - } - if (doSpawning == PreSpawnStatus.SUCCESS && checker.test(biomesettingsmobs_c.type, blockposition_mutableblockposition, chunk)) { - // Paper end - PreCreatureSpawnEvent - Mob entityinsentient = NaturalSpawner.getMobForSpawn(world, biomesettingsmobs_c.type); - - if (entityinsentient == null) { -- return; -+ return j; // Paper - Optional per player mob spawns - } - - entityinsentient.moveTo(d0, (double) i, d1, world.random.nextFloat() * 360.0F, 0.0F); -@@ -248,10 +293,15 @@ public final class NaturalSpawner { - ++j; - ++k1; - runner.run(entityinsentient, chunk); -+ // Paper start - Optional per player mob spawns -+ if (trackEntity != null) { -+ trackEntity.accept(entityinsentient); -+ } -+ // Paper end - Optional per player mob spawns - } - // CraftBukkit end -- if (j >= entityinsentient.getMaxSpawnClusterSize()) { -- return; -+ if (j >= entityinsentient.getMaxSpawnClusterSize() || j >= maxSpawns) { // Paper - Optional per player mob spawns -+ return j; // Paper - Optional per player mob spawns - } - - if (entityinsentient.isMaxGroupSizeReached(k1)) { -@@ -273,6 +323,7 @@ public final class NaturalSpawner { - } - - } -+ return j; // Paper - Optional per player mob spawns - } - - private static boolean isRightDistanceToPlayerAndSpawnPoint(ServerLevel world, ChunkAccess chunk, BlockPos.MutableBlockPos pos, double squaredDistance) { -@@ -523,7 +574,7 @@ public final class NaturalSpawner { - MobCategory enumcreaturetype = entitytypes.getCategory(); - - this.mobCategoryCounts.addTo(enumcreaturetype, 1); -- this.localMobCapCalculator.addMob(new ChunkPos(blockposition), enumcreaturetype); -+ if (this.localMobCapCalculator != null) this.localMobCapCalculator.addMob(new ChunkPos(blockposition), enumcreaturetype); // Paper - Optional per player mob spawns - } - - public int getSpawnableChunkCount() { -@@ -539,6 +590,7 @@ public final class NaturalSpawner { - int i = limit * this.spawnableChunkCount / NaturalSpawner.MAGIC_NUMBER; - // CraftBukkit end - -+ if (this.localMobCapCalculator == null) return this.mobCategoryCounts.getInt(enumcreaturetype) < i; // Paper - Optional per player mob spawns - return this.mobCategoryCounts.getInt(enumcreaturetype) >= i ? false : this.localMobCapCalculator.canSpawn(enumcreaturetype, chunkcoordintpair); - } - } diff --git a/patches/server/0997-Anti-Xray.patch b/patches/server/0997-Anti-Xray.patch deleted file mode 100644 index 058f4c5c6a..0000000000 --- a/patches/server/0997-Anti-Xray.patch +++ /dev/null @@ -1,1638 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: stonar96 -Date: Thu, 25 Nov 2021 13:27:51 +0100 -Subject: [PATCH] Anti-Xray - - -diff --git a/src/main/java/com/destroystokyo/paper/antixray/BitStorageReader.java b/src/main/java/com/destroystokyo/paper/antixray/BitStorageReader.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e448c26327b5f6189c3c52e698cff66c8f9ad81a ---- /dev/null -+++ b/src/main/java/com/destroystokyo/paper/antixray/BitStorageReader.java -@@ -0,0 +1,51 @@ -+package com.destroystokyo.paper.antixray; -+ -+public final class BitStorageReader { -+ -+ private byte[] buffer; -+ private int bits; -+ private int mask; -+ private int longInBufferIndex; -+ private int bitInLongIndex; -+ private long current; -+ -+ public void setBuffer(byte[] buffer) { -+ this.buffer = buffer; -+ } -+ -+ public void setBits(int bits) { -+ this.bits = bits; -+ mask = (1 << bits) - 1; -+ } -+ -+ public void setIndex(int index) { -+ longInBufferIndex = index; -+ bitInLongIndex = 0; -+ init(); -+ } -+ -+ private void init() { -+ if (buffer.length > longInBufferIndex + 7) { -+ current = ((((long) buffer[longInBufferIndex]) << 56) -+ | (((long) buffer[longInBufferIndex + 1] & 0xff) << 48) -+ | (((long) buffer[longInBufferIndex + 2] & 0xff) << 40) -+ | (((long) buffer[longInBufferIndex + 3] & 0xff) << 32) -+ | (((long) buffer[longInBufferIndex + 4] & 0xff) << 24) -+ | (((long) buffer[longInBufferIndex + 5] & 0xff) << 16) -+ | (((long) buffer[longInBufferIndex + 6] & 0xff) << 8) -+ | (((long) buffer[longInBufferIndex + 7] & 0xff))); -+ } -+ } -+ -+ public int read() { -+ if (bitInLongIndex + bits > 64) { -+ bitInLongIndex = 0; -+ longInBufferIndex += 8; -+ init(); -+ } -+ -+ int value = (int) (current >>> bitInLongIndex) & mask; -+ bitInLongIndex += bits; -+ return value; -+ } -+} -diff --git a/src/main/java/com/destroystokyo/paper/antixray/BitStorageWriter.java b/src/main/java/com/destroystokyo/paper/antixray/BitStorageWriter.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e4540ea278f2dc871cb6a3cb8897559bfd65e134 ---- /dev/null -+++ b/src/main/java/com/destroystokyo/paper/antixray/BitStorageWriter.java -@@ -0,0 +1,79 @@ -+package com.destroystokyo.paper.antixray; -+ -+public final class BitStorageWriter { -+ -+ private byte[] buffer; -+ private int bits; -+ private long mask; -+ private int longInBufferIndex; -+ private int bitInLongIndex; -+ private long current; -+ private boolean dirty; -+ -+ public void setBuffer(byte[] buffer) { -+ this.buffer = buffer; -+ } -+ -+ public void setBits(int bits) { -+ this.bits = bits; -+ mask = (1L << bits) - 1; -+ } -+ -+ public void setIndex(int index) { -+ longInBufferIndex = index; -+ bitInLongIndex = 0; -+ init(); -+ } -+ -+ private void init() { -+ if (buffer.length > longInBufferIndex + 7) { -+ current = ((((long) buffer[longInBufferIndex]) << 56) -+ | (((long) buffer[longInBufferIndex + 1] & 0xff) << 48) -+ | (((long) buffer[longInBufferIndex + 2] & 0xff) << 40) -+ | (((long) buffer[longInBufferIndex + 3] & 0xff) << 32) -+ | (((long) buffer[longInBufferIndex + 4] & 0xff) << 24) -+ | (((long) buffer[longInBufferIndex + 5] & 0xff) << 16) -+ | (((long) buffer[longInBufferIndex + 6] & 0xff) << 8) -+ | (((long) buffer[longInBufferIndex + 7] & 0xff))); -+ } -+ -+ dirty = false; -+ } -+ -+ public void flush() { -+ if (dirty && buffer.length > longInBufferIndex + 7) { -+ buffer[longInBufferIndex] = (byte) (current >> 56 & 0xff); -+ buffer[longInBufferIndex + 1] = (byte) (current >> 48 & 0xff); -+ buffer[longInBufferIndex + 2] = (byte) (current >> 40 & 0xff); -+ buffer[longInBufferIndex + 3] = (byte) (current >> 32 & 0xff); -+ buffer[longInBufferIndex + 4] = (byte) (current >> 24 & 0xff); -+ buffer[longInBufferIndex + 5] = (byte) (current >> 16 & 0xff); -+ buffer[longInBufferIndex + 6] = (byte) (current >> 8 & 0xff); -+ buffer[longInBufferIndex + 7] = (byte) (current & 0xff); -+ } -+ } -+ -+ public void write(int value) { -+ if (bitInLongIndex + bits > 64) { -+ flush(); -+ bitInLongIndex = 0; -+ longInBufferIndex += 8; -+ init(); -+ } -+ -+ current = current & ~(mask << bitInLongIndex) | (value & mask) << bitInLongIndex; -+ dirty = true; -+ bitInLongIndex += bits; -+ } -+ -+ public void skip() { -+ bitInLongIndex += bits; -+ -+ if (bitInLongIndex > 64) { -+ flush(); -+ bitInLongIndex = bits; -+ longInBufferIndex += 8; -+ init(); -+ } -+ } -+} -diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java -new file mode 100644 -index 0000000000000000000000000000000000000000..52d2e2b744f91914802506e52a07161729bbcf3a ---- /dev/null -+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockController.java -@@ -0,0 +1,45 @@ -+package com.destroystokyo.paper.antixray; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.Direction; -+import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; -+import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.server.level.ServerPlayerGameMode; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.level.chunk.LevelChunk; -+ -+public class ChunkPacketBlockController { -+ -+ public static final ChunkPacketBlockController NO_OPERATION_INSTANCE = new ChunkPacketBlockController(); -+ -+ protected ChunkPacketBlockController() { -+ -+ } -+ -+ public BlockState[] getPresetBlockStates(Level level, ChunkPos chunkPos, int chunkSectionY) { -+ return null; -+ } -+ -+ public boolean shouldModify(ServerPlayer player, LevelChunk chunk) { -+ return false; -+ } -+ -+ public ChunkPacketInfo getChunkPacketInfo(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk) { -+ return null; -+ } -+ -+ public void modifyBlocks(ClientboundLevelChunkWithLightPacket chunkPacket, ChunkPacketInfo chunkPacketInfo) { -+ chunkPacket.setReady(true); -+ } -+ -+ public void onBlockChange(Level level, BlockPos blockPos, BlockState newBlockState, BlockState oldBlockState, int flags, int maxUpdateDepth) { -+ -+ } -+ -+ public void onPlayerLeftClickBlock(ServerPlayerGameMode serverPlayerGameMode, BlockPos blockPos, ServerboundPlayerActionPacket.Action action, Direction direction, int worldHeight, int sequence) { -+ -+ } -+} -diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e7fe98ea30ae6d0baea3ec1f9f98a89502a49a12 ---- /dev/null -+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketBlockControllerAntiXray.java -@@ -0,0 +1,676 @@ -+package com.destroystokyo.paper.antixray; -+ -+import io.papermc.paper.configuration.WorldConfiguration; -+import io.papermc.paper.configuration.type.EngineMode; -+import java.util.ArrayList; -+import java.util.LinkedHashSet; -+import java.util.LinkedList; -+import java.util.List; -+import java.util.Set; -+import java.util.concurrent.Executor; -+import java.util.concurrent.ThreadLocalRandom; -+import java.util.function.IntSupplier; -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.Direction; -+import net.minecraft.core.registries.Registries; -+import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; -+import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.server.level.ServerPlayerGameMode; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.biome.Biomes; -+import net.minecraft.world.level.block.Block; -+import net.minecraft.world.level.block.Blocks; -+import net.minecraft.world.level.block.EntityBlock; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.level.chunk.EmptyLevelChunk; -+import net.minecraft.world.level.chunk.GlobalPalette; -+import net.minecraft.world.level.chunk.LevelChunk; -+import net.minecraft.world.level.chunk.LevelChunkSection; -+import net.minecraft.world.level.chunk.MissingPaletteEntryException; -+import net.minecraft.world.level.chunk.Palette; -+import org.bukkit.Bukkit; -+ -+public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockController { -+ -+ private static final Palette GLOBAL_BLOCKSTATE_PALETTE = new GlobalPalette<>(Block.BLOCK_STATE_REGISTRY); -+ private static final LevelChunkSection EMPTY_SECTION = null; -+ private final Executor executor; -+ private final EngineMode engineMode; -+ private final int maxBlockHeight; -+ private final int updateRadius; -+ private final boolean usePermission; -+ private final BlockState[] presetBlockStates; -+ private final BlockState[] presetBlockStatesFull; -+ private final BlockState[] presetBlockStatesStone; -+ private final BlockState[] presetBlockStatesDeepslate; -+ private final BlockState[] presetBlockStatesNetherrack; -+ private final BlockState[] presetBlockStatesEndStone; -+ private final int[] presetBlockStateBitsGlobal; -+ private final int[] presetBlockStateBitsStoneGlobal; -+ private final int[] presetBlockStateBitsDeepslateGlobal; -+ private final int[] presetBlockStateBitsNetherrackGlobal; -+ private final int[] presetBlockStateBitsEndStoneGlobal; -+ private final boolean[] solidGlobal = new boolean[Block.BLOCK_STATE_REGISTRY.size()]; -+ private final boolean[] obfuscateGlobal = new boolean[Block.BLOCK_STATE_REGISTRY.size()]; -+ private final LevelChunkSection[] emptyNearbyChunkSections = {EMPTY_SECTION, EMPTY_SECTION, EMPTY_SECTION, EMPTY_SECTION}; -+ private final int maxBlockHeightUpdatePosition; -+ -+ public ChunkPacketBlockControllerAntiXray(Level level, Executor executor) { -+ this.executor = executor; -+ WorldConfiguration.Anticheat.AntiXray paperWorldConfig = level.paperConfig().anticheat.antiXray; -+ engineMode = paperWorldConfig.engineMode; -+ maxBlockHeight = paperWorldConfig.maxBlockHeight >> 4 << 4; -+ updateRadius = paperWorldConfig.updateRadius; -+ usePermission = paperWorldConfig.usePermission; -+ List toObfuscate; -+ -+ if (engineMode == EngineMode.HIDE) { -+ toObfuscate = paperWorldConfig.hiddenBlocks; -+ presetBlockStates = null; -+ presetBlockStatesFull = null; -+ presetBlockStatesStone = new BlockState[]{Blocks.STONE.defaultBlockState()}; -+ presetBlockStatesDeepslate = new BlockState[]{Blocks.DEEPSLATE.defaultBlockState()}; -+ presetBlockStatesNetherrack = new BlockState[]{Blocks.NETHERRACK.defaultBlockState()}; -+ presetBlockStatesEndStone = new BlockState[]{Blocks.END_STONE.defaultBlockState()}; -+ presetBlockStateBitsGlobal = null; -+ presetBlockStateBitsStoneGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.STONE.defaultBlockState())}; -+ presetBlockStateBitsDeepslateGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.DEEPSLATE.defaultBlockState())}; -+ presetBlockStateBitsNetherrackGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.NETHERRACK.defaultBlockState())}; -+ presetBlockStateBitsEndStoneGlobal = new int[]{GLOBAL_BLOCKSTATE_PALETTE.idFor(Blocks.END_STONE.defaultBlockState())}; -+ } else { -+ toObfuscate = new ArrayList<>(paperWorldConfig.replacementBlocks); -+ List presetBlockStateList = new LinkedList<>(); -+ -+ for (Block block : paperWorldConfig.hiddenBlocks) { -+ -+ if (!(block instanceof EntityBlock)) { -+ toObfuscate.add(block); -+ presetBlockStateList.add(block.defaultBlockState()); -+ } -+ } -+ -+ // The doc of the LinkedHashSet(Collection) constructor doesn't specify that the insertion order is the predictable iteration order of the specified Collection, although it is in the implementation -+ Set presetBlockStateSet = new LinkedHashSet<>(); -+ // Therefore addAll(Collection) is used, which guarantees this order in the doc -+ presetBlockStateSet.addAll(presetBlockStateList); -+ presetBlockStates = presetBlockStateSet.isEmpty() ? new BlockState[]{Blocks.DIAMOND_ORE.defaultBlockState()} : presetBlockStateSet.toArray(new BlockState[0]); -+ presetBlockStatesFull = presetBlockStateSet.isEmpty() ? new BlockState[]{Blocks.DIAMOND_ORE.defaultBlockState()} : presetBlockStateList.toArray(new BlockState[0]); -+ presetBlockStatesStone = null; -+ presetBlockStatesDeepslate = null; -+ presetBlockStatesNetherrack = null; -+ presetBlockStatesEndStone = null; -+ presetBlockStateBitsGlobal = new int[presetBlockStatesFull.length]; -+ -+ for (int i = 0; i < presetBlockStatesFull.length; i++) { -+ presetBlockStateBitsGlobal[i] = GLOBAL_BLOCKSTATE_PALETTE.idFor(presetBlockStatesFull[i]); -+ } -+ -+ presetBlockStateBitsStoneGlobal = null; -+ presetBlockStateBitsDeepslateGlobal = null; -+ presetBlockStateBitsNetherrackGlobal = null; -+ presetBlockStateBitsEndStoneGlobal = null; -+ } -+ -+ for (Block block : toObfuscate) { -+ -+ // Don't obfuscate air because air causes unnecessary block updates and causes block updates to fail in the void -+ if (block != null && !block.defaultBlockState().isAir()) { -+ // Replace all block states of a specified block -+ for (BlockState blockState : block.getStateDefinition().getPossibleStates()) { -+ obfuscateGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)] = true; -+ } -+ } -+ } -+ -+ EmptyLevelChunk emptyChunk = new EmptyLevelChunk(level, new ChunkPos(0, 0), MinecraftServer.getServer().registryAccess().registryOrThrow(Registries.BIOME).getHolderOrThrow(Biomes.PLAINS)); -+ BlockPos zeroPos = new BlockPos(0, 0, 0); -+ -+ for (int i = 0; i < solidGlobal.length; i++) { -+ BlockState blockState = GLOBAL_BLOCKSTATE_PALETTE.valueFor(i); -+ -+ if (blockState != null) { -+ solidGlobal[i] = blockState.isRedstoneConductor(emptyChunk, zeroPos) -+ && blockState.getBlock() != Blocks.SPAWNER && blockState.getBlock() != Blocks.BARRIER && blockState.getBlock() != Blocks.SHULKER_BOX && blockState.getBlock() != Blocks.SLIME_BLOCK && blockState.getBlock() != Blocks.MANGROVE_ROOTS || paperWorldConfig.lavaObscures && blockState == Blocks.LAVA.defaultBlockState(); -+ // Comparing blockState == Blocks.LAVA.defaultBlockState() instead of blockState.getBlock() == Blocks.LAVA ensures that only "stationary lava" is used -+ // shulker box checks TE. -+ } -+ } -+ -+ maxBlockHeightUpdatePosition = maxBlockHeight + updateRadius - 1; -+ } -+ -+ private int getPresetBlockStatesFullLength() { -+ return engineMode == EngineMode.HIDE ? 1 : presetBlockStatesFull.length; -+ } -+ -+ @Override -+ public BlockState[] getPresetBlockStates(Level level, ChunkPos chunkPos, int chunkSectionY) { -+ // Return the block states to be added to the paletted containers so that they can be used for obfuscation -+ int bottomBlockY = chunkSectionY << 4; -+ -+ if (bottomBlockY < maxBlockHeight) { -+ if (engineMode == EngineMode.HIDE) { -+ return switch (level.getWorld().getEnvironment()) { -+ case NETHER -> presetBlockStatesNetherrack; -+ case THE_END -> presetBlockStatesEndStone; -+ default -> bottomBlockY < 0 ? presetBlockStatesDeepslate : presetBlockStatesStone; -+ }; -+ } -+ -+ return presetBlockStates; -+ } -+ -+ return null; -+ } -+ -+ @Override -+ public boolean shouldModify(ServerPlayer player, LevelChunk chunk) { -+ return !usePermission || !player.getBukkitEntity().hasPermission("paper.antixray.bypass"); -+ } -+ -+ @Override -+ public ChunkPacketInfoAntiXray getChunkPacketInfo(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk) { -+ // Return a new instance to collect data and objects in the right state while creating the chunk packet for thread safe access later -+ return new ChunkPacketInfoAntiXray(chunkPacket, chunk, this); -+ } -+ -+ @Override -+ public void modifyBlocks(ClientboundLevelChunkWithLightPacket chunkPacket, ChunkPacketInfo chunkPacketInfo) { -+ if (!(chunkPacketInfo instanceof ChunkPacketInfoAntiXray)) { -+ chunkPacket.setReady(true); -+ return; -+ } -+ -+ if (!Bukkit.isPrimaryThread()) { -+ // Plugins? -+ MinecraftServer.getServer().scheduleOnMain(() -> modifyBlocks(chunkPacket, chunkPacketInfo)); -+ return; -+ } -+ -+ LevelChunk chunk = chunkPacketInfo.getChunk(); -+ int x = chunk.getPos().x; -+ int z = chunk.getPos().z; -+ Level level = chunk.getLevel(); -+ ((ChunkPacketInfoAntiXray) chunkPacketInfo).setNearbyChunks(level.getChunkIfLoaded(x - 1, z), level.getChunkIfLoaded(x + 1, z), level.getChunkIfLoaded(x, z - 1), level.getChunkIfLoaded(x, z + 1)); -+ executor.execute((Runnable) chunkPacketInfo); -+ } -+ -+ // Actually these fields should be variables inside the obfuscate method but in sync mode or with SingleThreadExecutor in async mode it's okay (even without ThreadLocal) -+ // If an ExecutorService with multiple threads is used, ThreadLocal must be used here -+ private final ThreadLocal presetBlockStateBits = ThreadLocal.withInitial(() -> new int[getPresetBlockStatesFullLength()]); -+ private static final ThreadLocal SOLID = ThreadLocal.withInitial(() -> new boolean[Block.BLOCK_STATE_REGISTRY.size()]); -+ private static final ThreadLocal OBFUSCATE = ThreadLocal.withInitial(() -> new boolean[Block.BLOCK_STATE_REGISTRY.size()]); -+ // These boolean arrays represent chunk layers, true means don't obfuscate, false means obfuscate -+ private static final ThreadLocal CURRENT = ThreadLocal.withInitial(() -> new boolean[16][16]); -+ private static final ThreadLocal NEXT = ThreadLocal.withInitial(() -> new boolean[16][16]); -+ private static final ThreadLocal NEXT_NEXT = ThreadLocal.withInitial(() -> new boolean[16][16]); -+ -+ public void obfuscate(ChunkPacketInfoAntiXray chunkPacketInfoAntiXray) { -+ int[] presetBlockStateBits = this.presetBlockStateBits.get(); -+ boolean[] solid = SOLID.get(); -+ boolean[] obfuscate = OBFUSCATE.get(); -+ boolean[][] current = CURRENT.get(); -+ boolean[][] next = NEXT.get(); -+ boolean[][] nextNext = NEXT_NEXT.get(); -+ // bitStorageReader, bitStorageWriter and nearbyChunkSections could also be reused (with ThreadLocal if necessary) but it's not worth it -+ BitStorageReader bitStorageReader = new BitStorageReader(); -+ BitStorageWriter bitStorageWriter = new BitStorageWriter(); -+ LevelChunkSection[] nearbyChunkSections = new LevelChunkSection[4]; -+ LevelChunk chunk = chunkPacketInfoAntiXray.getChunk(); -+ Level level = chunk.getLevel(); -+ int maxChunkSectionIndex = Math.min((maxBlockHeight >> 4) - chunk.getMinSection(), chunk.getSectionsCount()) - 1; -+ boolean[] solidTemp = null; -+ boolean[] obfuscateTemp = null; -+ bitStorageReader.setBuffer(chunkPacketInfoAntiXray.getBuffer()); -+ bitStorageWriter.setBuffer(chunkPacketInfoAntiXray.getBuffer()); -+ int numberOfBlocks = presetBlockStateBits.length; -+ // Keep the lambda expressions as simple as possible. They are used very frequently. -+ LayeredIntSupplier random = numberOfBlocks == 1 ? (() -> 0) : engineMode == EngineMode.OBFUSCATE_LAYER ? new LayeredIntSupplier() { -+ // engine-mode: 3 -+ private int state; -+ private int next; -+ -+ { -+ while ((state = ThreadLocalRandom.current().nextInt()) == 0) ; -+ } -+ -+ @Override -+ public void nextLayer() { -+ // https://en.wikipedia.org/wiki/Xorshift -+ state ^= state << 13; -+ state ^= state >>> 17; -+ state ^= state << 5; -+ // https://www.pcg-random.org/posts/bounded-rands.html -+ next = (int) ((Integer.toUnsignedLong(state) * numberOfBlocks) >>> 32); -+ } -+ -+ @Override -+ public int getAsInt() { -+ return next; -+ } -+ } : new LayeredIntSupplier() { -+ // engine-mode: 2 -+ private int state; -+ -+ { -+ while ((state = ThreadLocalRandom.current().nextInt()) == 0) ; -+ } -+ -+ @Override -+ public int getAsInt() { -+ // https://en.wikipedia.org/wiki/Xorshift -+ state ^= state << 13; -+ state ^= state >>> 17; -+ state ^= state << 5; -+ // https://www.pcg-random.org/posts/bounded-rands.html -+ return (int) ((Integer.toUnsignedLong(state) * numberOfBlocks) >>> 32); -+ } -+ }; -+ -+ for (int chunkSectionIndex = 0; chunkSectionIndex <= maxChunkSectionIndex; chunkSectionIndex++) { -+ if (chunkPacketInfoAntiXray.isWritten(chunkSectionIndex) && chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex) != null) { -+ int[] presetBlockStateBitsTemp; -+ -+ if (chunkPacketInfoAntiXray.getPalette(chunkSectionIndex) instanceof GlobalPalette) { -+ if (engineMode == EngineMode.HIDE) { -+ presetBlockStateBitsTemp = switch (level.getWorld().getEnvironment()) { -+ case NETHER -> presetBlockStateBitsNetherrackGlobal; -+ case THE_END -> presetBlockStateBitsEndStoneGlobal; -+ default -> chunkSectionIndex + chunk.getMinSection() < 0 ? presetBlockStateBitsDeepslateGlobal : presetBlockStateBitsStoneGlobal; -+ }; -+ } else { -+ presetBlockStateBitsTemp = presetBlockStateBitsGlobal; -+ } -+ } else { -+ // If it's presetBlockStates, use this.presetBlockStatesFull instead -+ BlockState[] presetBlockStatesFull = chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex) == presetBlockStates ? this.presetBlockStatesFull : chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex); -+ presetBlockStateBitsTemp = presetBlockStateBits; -+ -+ for (int i = 0; i < presetBlockStateBitsTemp.length; i++) { -+ // This is thread safe because we only request IDs that are guaranteed to be in the palette and are visible -+ // For more details see the comments in the readPalette method -+ presetBlockStateBitsTemp[i] = chunkPacketInfoAntiXray.getPalette(chunkSectionIndex).idFor(presetBlockStatesFull[i]); -+ } -+ } -+ -+ bitStorageWriter.setIndex(chunkPacketInfoAntiXray.getIndex(chunkSectionIndex)); -+ -+ // Check if the chunk section below was not obfuscated -+ if (chunkSectionIndex == 0 || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex - 1) || chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex - 1) == null) { -+ // If so, initialize some stuff -+ bitStorageReader.setBits(chunkPacketInfoAntiXray.getBits(chunkSectionIndex)); -+ bitStorageReader.setIndex(chunkPacketInfoAntiXray.getIndex(chunkSectionIndex)); -+ solidTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex), solid, solidGlobal); -+ obfuscateTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex), obfuscate, obfuscateGlobal); -+ // Read the blocks of the upper layer of the chunk section below if it exists -+ LevelChunkSection belowChunkSection = null; -+ boolean skipFirstLayer = chunkSectionIndex == 0 || (belowChunkSection = chunk.getSections()[chunkSectionIndex - 1]) == EMPTY_SECTION; -+ -+ for (int z = 0; z < 16; z++) { -+ for (int x = 0; x < 16; x++) { -+ current[z][x] = true; -+ next[z][x] = skipFirstLayer || isTransparent(belowChunkSection, x, 15, z); -+ } -+ } -+ -+ // Abuse the obfuscateLayer method to read the blocks of the first layer of the current chunk section -+ bitStorageWriter.setBits(0); -+ obfuscateLayer(-1, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, emptyNearbyChunkSections, random); -+ } -+ -+ bitStorageWriter.setBits(chunkPacketInfoAntiXray.getBits(chunkSectionIndex)); -+ nearbyChunkSections[0] = chunkPacketInfoAntiXray.getNearbyChunks()[0] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[0].getSections()[chunkSectionIndex]; -+ nearbyChunkSections[1] = chunkPacketInfoAntiXray.getNearbyChunks()[1] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[1].getSections()[chunkSectionIndex]; -+ nearbyChunkSections[2] = chunkPacketInfoAntiXray.getNearbyChunks()[2] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[2].getSections()[chunkSectionIndex]; -+ nearbyChunkSections[3] = chunkPacketInfoAntiXray.getNearbyChunks()[3] == null ? EMPTY_SECTION : chunkPacketInfoAntiXray.getNearbyChunks()[3].getSections()[chunkSectionIndex]; -+ -+ // Obfuscate all layers of the current chunk section except the upper one -+ for (int y = 0; y < 15; y++) { -+ boolean[][] temp = current; -+ current = next; -+ next = nextNext; -+ nextNext = temp; -+ random.nextLayer(); -+ obfuscateLayer(y, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, nearbyChunkSections, random); -+ } -+ -+ // Check if the chunk section above doesn't need obfuscation -+ if (chunkSectionIndex == maxChunkSectionIndex || !chunkPacketInfoAntiXray.isWritten(chunkSectionIndex + 1) || chunkPacketInfoAntiXray.getPresetValues(chunkSectionIndex + 1) == null) { -+ // If so, obfuscate the upper layer of the current chunk section by reading blocks of the first layer from the chunk section above if it exists -+ LevelChunkSection aboveChunkSection; -+ -+ if (chunkSectionIndex != chunk.getSectionsCount() - 1 && (aboveChunkSection = chunk.getSections()[chunkSectionIndex + 1]) != EMPTY_SECTION) { -+ boolean[][] temp = current; -+ current = next; -+ next = nextNext; -+ nextNext = temp; -+ -+ for (int z = 0; z < 16; z++) { -+ for (int x = 0; x < 16; x++) { -+ if (isTransparent(aboveChunkSection, x, 0, z)) { -+ current[z][x] = true; -+ } -+ } -+ } -+ -+ // There is nothing to read anymore -+ bitStorageReader.setBits(0); -+ solid[0] = true; -+ random.nextLayer(); -+ obfuscateLayer(15, bitStorageReader, bitStorageWriter, solid, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, nearbyChunkSections, random); -+ } -+ } else { -+ // If not, initialize the reader and other stuff for the chunk section above to obfuscate the upper layer of the current chunk section -+ bitStorageReader.setBits(chunkPacketInfoAntiXray.getBits(chunkSectionIndex + 1)); -+ bitStorageReader.setIndex(chunkPacketInfoAntiXray.getIndex(chunkSectionIndex + 1)); -+ solidTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex + 1), solid, solidGlobal); -+ obfuscateTemp = readPalette(chunkPacketInfoAntiXray.getPalette(chunkSectionIndex + 1), obfuscate, obfuscateGlobal); -+ boolean[][] temp = current; -+ current = next; -+ next = nextNext; -+ nextNext = temp; -+ random.nextLayer(); -+ obfuscateLayer(15, bitStorageReader, bitStorageWriter, solidTemp, obfuscateTemp, presetBlockStateBitsTemp, current, next, nextNext, nearbyChunkSections, random); -+ } -+ -+ bitStorageWriter.flush(); -+ } -+ } -+ -+ chunkPacketInfoAntiXray.getChunkPacket().setReady(true); -+ } -+ -+ private void obfuscateLayer(int y, BitStorageReader bitStorageReader, BitStorageWriter bitStorageWriter, boolean[] solid, boolean[] obfuscate, int[] presetBlockStateBits, boolean[][] current, boolean[][] next, boolean[][] nextNext, LevelChunkSection[] nearbyChunkSections, IntSupplier random) { -+ // First block of first line -+ int bits = bitStorageReader.read(); -+ -+ if (nextNext[0][0] = !solid[bits]) { -+ bitStorageWriter.skip(); -+ next[0][1] = true; -+ next[1][0] = true; -+ } else { -+ if (current[0][0] || isTransparent(nearbyChunkSections[2], 0, y, 15) || isTransparent(nearbyChunkSections[0], 15, y, 0)) { -+ bitStorageWriter.skip(); -+ } else { -+ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); -+ } -+ } -+ -+ if (!obfuscate[bits]) { -+ next[0][0] = true; -+ } -+ -+ // First line -+ for (int x = 1; x < 15; x++) { -+ bits = bitStorageReader.read(); -+ -+ if (nextNext[0][x] = !solid[bits]) { -+ bitStorageWriter.skip(); -+ next[0][x - 1] = true; -+ next[0][x + 1] = true; -+ next[1][x] = true; -+ } else { -+ if (current[0][x] || isTransparent(nearbyChunkSections[2], x, y, 15)) { -+ bitStorageWriter.skip(); -+ } else { -+ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); -+ } -+ } -+ -+ if (!obfuscate[bits]) { -+ next[0][x] = true; -+ } -+ } -+ -+ // Last block of first line -+ bits = bitStorageReader.read(); -+ -+ if (nextNext[0][15] = !solid[bits]) { -+ bitStorageWriter.skip(); -+ next[0][14] = true; -+ next[1][15] = true; -+ } else { -+ if (current[0][15] || isTransparent(nearbyChunkSections[2], 15, y, 15) || isTransparent(nearbyChunkSections[1], 0, y, 0)) { -+ bitStorageWriter.skip(); -+ } else { -+ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); -+ } -+ } -+ -+ if (!obfuscate[bits]) { -+ next[0][15] = true; -+ } -+ -+ // All inner lines -+ for (int z = 1; z < 15; z++) { -+ // First block -+ bits = bitStorageReader.read(); -+ -+ if (nextNext[z][0] = !solid[bits]) { -+ bitStorageWriter.skip(); -+ next[z][1] = true; -+ next[z - 1][0] = true; -+ next[z + 1][0] = true; -+ } else { -+ if (current[z][0] || isTransparent(nearbyChunkSections[0], 15, y, z)) { -+ bitStorageWriter.skip(); -+ } else { -+ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); -+ } -+ } -+ -+ if (!obfuscate[bits]) { -+ next[z][0] = true; -+ } -+ -+ // All inner blocks -+ for (int x = 1; x < 15; x++) { -+ bits = bitStorageReader.read(); -+ -+ if (nextNext[z][x] = !solid[bits]) { -+ bitStorageWriter.skip(); -+ next[z][x - 1] = true; -+ next[z][x + 1] = true; -+ next[z - 1][x] = true; -+ next[z + 1][x] = true; -+ } else { -+ if (current[z][x]) { -+ bitStorageWriter.skip(); -+ } else { -+ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); -+ } -+ } -+ -+ if (!obfuscate[bits]) { -+ next[z][x] = true; -+ } -+ } -+ -+ // Last block -+ bits = bitStorageReader.read(); -+ -+ if (nextNext[z][15] = !solid[bits]) { -+ bitStorageWriter.skip(); -+ next[z][14] = true; -+ next[z - 1][15] = true; -+ next[z + 1][15] = true; -+ } else { -+ if (current[z][15] || isTransparent(nearbyChunkSections[1], 0, y, z)) { -+ bitStorageWriter.skip(); -+ } else { -+ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); -+ } -+ } -+ -+ if (!obfuscate[bits]) { -+ next[z][15] = true; -+ } -+ } -+ -+ // First block of last line -+ bits = bitStorageReader.read(); -+ -+ if (nextNext[15][0] = !solid[bits]) { -+ bitStorageWriter.skip(); -+ next[15][1] = true; -+ next[14][0] = true; -+ } else { -+ if (current[15][0] || isTransparent(nearbyChunkSections[3], 0, y, 0) || isTransparent(nearbyChunkSections[0], 15, y, 15)) { -+ bitStorageWriter.skip(); -+ } else { -+ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); -+ } -+ } -+ -+ if (!obfuscate[bits]) { -+ next[15][0] = true; -+ } -+ -+ // Last line -+ for (int x = 1; x < 15; x++) { -+ bits = bitStorageReader.read(); -+ -+ if (nextNext[15][x] = !solid[bits]) { -+ bitStorageWriter.skip(); -+ next[15][x - 1] = true; -+ next[15][x + 1] = true; -+ next[14][x] = true; -+ } else { -+ if (current[15][x] || isTransparent(nearbyChunkSections[3], x, y, 0)) { -+ bitStorageWriter.skip(); -+ } else { -+ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); -+ } -+ } -+ -+ if (!obfuscate[bits]) { -+ next[15][x] = true; -+ } -+ } -+ -+ // Last block of last line -+ bits = bitStorageReader.read(); -+ -+ if (nextNext[15][15] = !solid[bits]) { -+ bitStorageWriter.skip(); -+ next[15][14] = true; -+ next[14][15] = true; -+ } else { -+ if (current[15][15] || isTransparent(nearbyChunkSections[3], 15, y, 0) || isTransparent(nearbyChunkSections[1], 0, y, 15)) { -+ bitStorageWriter.skip(); -+ } else { -+ bitStorageWriter.write(presetBlockStateBits[random.getAsInt()]); -+ } -+ } -+ -+ if (!obfuscate[bits]) { -+ next[15][15] = true; -+ } -+ } -+ -+ private boolean isTransparent(LevelChunkSection chunkSection, int x, int y, int z) { -+ if (chunkSection == EMPTY_SECTION) { -+ return true; -+ } -+ -+ try { -+ return !solidGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(chunkSection.getBlockState(x, y, z))]; -+ } catch (MissingPaletteEntryException e) { -+ // Race condition / visibility issue / no happens-before relationship -+ // We don't care and treat the block as transparent -+ // Internal implementation details of PalettedContainer, LinearPalette, HashMapPalette, CrudeIncrementalIntIdentityHashBiMap, ... guarantee us that no (other) exceptions will occur -+ return true; -+ } -+ } -+ -+ private boolean[] readPalette(Palette palette, boolean[] temp, boolean[] global) { -+ if (palette instanceof GlobalPalette) { -+ return global; -+ } -+ -+ try { -+ for (int i = 0; i < palette.getSize(); i++) { -+ temp[i] = global[GLOBAL_BLOCKSTATE_PALETTE.idFor(palette.valueFor(i))]; -+ } -+ } catch (MissingPaletteEntryException e) { -+ // Race condition / visibility issue / no happens-before relationship -+ // We don't care because we at least see the state as it was when the chunk packet was created -+ // Internal implementation details of PalettedContainer, LinearPalette, HashMapPalette, CrudeIncrementalIntIdentityHashBiMap, ... guarantee us that no (other) exceptions will occur until we have all the data that we need here -+ // Since all palettes have a fixed initial maximum size and there is no internal restructuring and no values are removed from palettes, we are also guaranteed to see the data -+ } -+ -+ return temp; -+ } -+ -+ @Override -+ public void onBlockChange(Level level, BlockPos blockPos, BlockState newBlockState, BlockState oldBlockState, int flags, int maxUpdateDepth) { -+ if (oldBlockState != null && solidGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(oldBlockState)] && !solidGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(newBlockState)] && blockPos.getY() <= maxBlockHeightUpdatePosition) { -+ updateNearbyBlocks(level, blockPos); -+ } -+ } -+ -+ @Override -+ public void onPlayerLeftClickBlock(ServerPlayerGameMode serverPlayerGameMode, BlockPos blockPos, ServerboundPlayerActionPacket.Action action, Direction direction, int worldHeight, int sequence) { -+ if (blockPos.getY() <= maxBlockHeightUpdatePosition) { -+ updateNearbyBlocks(serverPlayerGameMode.level, blockPos); -+ } -+ } -+ -+ private void updateNearbyBlocks(Level level, BlockPos blockPos) { -+ if (updateRadius >= 2) { -+ BlockPos temp = blockPos.west(); -+ updateBlock(level, temp); -+ updateBlock(level, temp.west()); -+ updateBlock(level, temp.below()); -+ updateBlock(level, temp.above()); -+ updateBlock(level, temp.north()); -+ updateBlock(level, temp.south()); -+ updateBlock(level, temp = blockPos.east()); -+ updateBlock(level, temp.east()); -+ updateBlock(level, temp.below()); -+ updateBlock(level, temp.above()); -+ updateBlock(level, temp.north()); -+ updateBlock(level, temp.south()); -+ updateBlock(level, temp = blockPos.below()); -+ updateBlock(level, temp.below()); -+ updateBlock(level, temp.north()); -+ updateBlock(level, temp.south()); -+ updateBlock(level, temp = blockPos.above()); -+ updateBlock(level, temp.above()); -+ updateBlock(level, temp.north()); -+ updateBlock(level, temp.south()); -+ updateBlock(level, temp = blockPos.north()); -+ updateBlock(level, temp.north()); -+ updateBlock(level, temp = blockPos.south()); -+ updateBlock(level, temp.south()); -+ } else if (updateRadius == 1) { -+ updateBlock(level, blockPos.west()); -+ updateBlock(level, blockPos.east()); -+ updateBlock(level, blockPos.below()); -+ updateBlock(level, blockPos.above()); -+ updateBlock(level, blockPos.north()); -+ updateBlock(level, blockPos.south()); -+ } else { -+ // Do nothing if updateRadius <= 0 (test mode) -+ } -+ } -+ -+ private void updateBlock(Level level, BlockPos blockPos) { -+ BlockState blockState = level.getBlockStateIfLoaded(blockPos); -+ -+ if (blockState != null && obfuscateGlobal[GLOBAL_BLOCKSTATE_PALETTE.idFor(blockState)]) { -+ ((ServerLevel) level).getChunkSource().blockChanged(blockPos); -+ } -+ } -+ -+ @FunctionalInterface -+ private interface LayeredIntSupplier extends IntSupplier { -+ default void nextLayer() { -+ -+ } -+ } -+} -diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfo.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfo.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d98a3f5c54c67a673eb7dc456dd039cd78f9c34d ---- /dev/null -+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfo.java -@@ -0,0 +1,80 @@ -+package com.destroystokyo.paper.antixray; -+ -+import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; -+import net.minecraft.world.level.chunk.LevelChunk; -+import net.minecraft.world.level.chunk.Palette; -+ -+public class ChunkPacketInfo { -+ -+ private final ClientboundLevelChunkWithLightPacket chunkPacket; -+ private final LevelChunk chunk; -+ private final int[] bits; -+ private final Object[] palettes; -+ private final int[] indexes; -+ private final Object[][] presetValues; -+ private byte[] buffer; -+ -+ public ChunkPacketInfo(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk) { -+ this.chunkPacket = chunkPacket; -+ this.chunk = chunk; -+ int sections = chunk.getSectionsCount(); -+ bits = new int[sections]; -+ palettes = new Object[sections]; -+ indexes = new int[sections]; -+ presetValues = new Object[sections][]; -+ } -+ -+ public ClientboundLevelChunkWithLightPacket getChunkPacket() { -+ return chunkPacket; -+ } -+ -+ public LevelChunk getChunk() { -+ return chunk; -+ } -+ -+ public byte[] getBuffer() { -+ return buffer; -+ } -+ -+ public void setBuffer(byte[] buffer) { -+ this.buffer = buffer; -+ } -+ -+ public int getBits(int chunkSectionIndex) { -+ return bits[chunkSectionIndex]; -+ } -+ -+ public void setBits(int chunkSectionIndex, int bits) { -+ this.bits[chunkSectionIndex] = bits; -+ } -+ -+ @SuppressWarnings("unchecked") -+ public Palette getPalette(int chunkSectionIndex) { -+ return (Palette) palettes[chunkSectionIndex]; -+ } -+ -+ public void setPalette(int chunkSectionIndex, Palette palette) { -+ palettes[chunkSectionIndex] = palette; -+ } -+ -+ public int getIndex(int chunkSectionIndex) { -+ return indexes[chunkSectionIndex]; -+ } -+ -+ public void setIndex(int chunkSectionIndex, int index) { -+ indexes[chunkSectionIndex] = index; -+ } -+ -+ @SuppressWarnings("unchecked") -+ public T[] getPresetValues(int chunkSectionIndex) { -+ return (T[]) presetValues[chunkSectionIndex]; -+ } -+ -+ public void setPresetValues(int chunkSectionIndex, T[] presetValues) { -+ this.presetValues[chunkSectionIndex] = presetValues; -+ } -+ -+ public boolean isWritten(int chunkSectionIndex) { -+ return bits[chunkSectionIndex] != 0; -+ } -+} -diff --git a/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfoAntiXray.java b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfoAntiXray.java -new file mode 100644 -index 0000000000000000000000000000000000000000..80a2dfb266ae1221680a7b24fee2f7e2a8330b7d ---- /dev/null -+++ b/src/main/java/com/destroystokyo/paper/antixray/ChunkPacketInfoAntiXray.java -@@ -0,0 +1,29 @@ -+package com.destroystokyo.paper.antixray; -+ -+import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.level.chunk.LevelChunk; -+ -+public final class ChunkPacketInfoAntiXray extends ChunkPacketInfo implements Runnable { -+ -+ private final ChunkPacketBlockControllerAntiXray chunkPacketBlockControllerAntiXray; -+ private LevelChunk[] nearbyChunks; -+ -+ public ChunkPacketInfoAntiXray(ClientboundLevelChunkWithLightPacket chunkPacket, LevelChunk chunk, ChunkPacketBlockControllerAntiXray chunkPacketBlockControllerAntiXray) { -+ super(chunkPacket, chunk); -+ this.chunkPacketBlockControllerAntiXray = chunkPacketBlockControllerAntiXray; -+ } -+ -+ public LevelChunk[] getNearbyChunks() { -+ return nearbyChunks; -+ } -+ -+ public void setNearbyChunks(LevelChunk... nearbyChunks) { -+ this.nearbyChunks = nearbyChunks; -+ } -+ -+ @Override -+ public void run() { -+ chunkPacketBlockControllerAntiXray.obfuscate(this); -+ } -+} -diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundChunksBiomesPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundChunksBiomesPacket.java -index ccc8c32c27c19cb0f0b581ca6693cfa737cb1de1..3c1cad5c2b34047cec44734ba4e8348cabec6f80 100644 ---- a/src/main/java/net/minecraft/network/protocol/game/ClientboundChunksBiomesPacket.java -+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundChunksBiomesPacket.java -@@ -70,8 +70,10 @@ public record ClientboundChunksBiomesPacket(List blockEntitiesData; - -- public ClientboundLevelChunkPacketData(LevelChunk chunk) { -+ // Paper start - Anti-Xray - Add chunk packet info -+ @Deprecated @io.papermc.paper.annotation.DoNotUse public ClientboundLevelChunkPacketData(LevelChunk chunk) { this(chunk, null); } -+ public ClientboundLevelChunkPacketData(LevelChunk chunk, com.destroystokyo.paper.antixray.ChunkPacketInfo chunkPacketInfo) { -+ // Paper end - this.heightmaps = new CompoundTag(); - - for (Entry entry : chunk.getHeightmaps()) { -@@ -38,7 +41,14 @@ public class ClientboundLevelChunkPacketData { - } - - this.buffer = new byte[calculateChunkSize(chunk)]; -- extractChunkData(new FriendlyByteBuf(this.getWriteBuffer()), chunk); -+ -+ // Paper start - Anti-Xray - Add chunk packet info -+ if (chunkPacketInfo != null) { -+ chunkPacketInfo.setBuffer(this.buffer); -+ } -+ -+ extractChunkData(new FriendlyByteBuf(this.getWriteBuffer()), chunk, chunkPacketInfo); -+ // Paper end - this.blockEntitiesData = Lists.newArrayList(); - - for (Entry entry2 : chunk.getBlockEntities().entrySet()) { -@@ -85,9 +95,15 @@ public class ClientboundLevelChunkPacketData { - return byteBuf; - } - -- public static void extractChunkData(FriendlyByteBuf buf, LevelChunk chunk) { -+ // Paper start - Anti-Xray - Add chunk packet info -+ @Deprecated @io.papermc.paper.annotation.DoNotUse public static void extractChunkData(FriendlyByteBuf buf, LevelChunk chunk) { ClientboundLevelChunkPacketData.extractChunkData(buf, chunk, null); } -+ public static void extractChunkData(FriendlyByteBuf buf, LevelChunk chunk, com.destroystokyo.paper.antixray.ChunkPacketInfo chunkPacketInfo) { -+ int chunkSectionIndex = 0; -+ - for (LevelChunkSection levelChunkSection : chunk.getSections()) { -- levelChunkSection.write(buf); -+ levelChunkSection.write(buf, chunkPacketInfo, chunkSectionIndex); -+ chunkSectionIndex++; -+ // Paper end - } - } - -diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java -index 183b2191fa1c1b27adedf39593e1b5a223fb1279..8ead66c134688b11dca15f6509147e726f182e6a 100644 ---- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java -+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java -@@ -18,13 +18,30 @@ public class ClientboundLevelChunkWithLightPacket implements Packet chunkPacketInfo = modifyBlocks ? chunk.getLevel().chunkPacketBlockController.getChunkPacketInfo(this, chunk) : null; -+ this.chunkData = new ClientboundLevelChunkPacketData(chunk, chunkPacketInfo); -+ // Paper end - this.lightData = new ClientboundLightUpdatePacketData(chunkPos, lightProvider, skyBits, blockBits); -+ chunk.getLevel().chunkPacketBlockController.modifyBlocks(this, chunkPacketInfo); // Paper - Anti-Xray - Modify blocks - } - - private ClientboundLevelChunkWithLightPacket(RegistryFriendlyByteBuf buf) { -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index b7ca39623348090c5b85e2d80f67a1747553d7d7..bc609da7105293dfa4ed339f553be66b0a632698 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -575,7 +575,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - // Holder holder = worlddimension.type(); // CraftBukkit - decompile error - - // Objects.requireNonNull(minecraftserver); // CraftBukkit - decompile error -- super(iworlddataserver, resourcekey, minecraftserver.registryAccess(), worlddimension.type(), minecraftserver::getProfiler, false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> minecraftserver.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig, minecraftserver.registryAccess()))); // Paper - create paper world configs -+ super(iworlddataserver, resourcekey, minecraftserver.registryAccess(), worlddimension.type(), minecraftserver::getProfiler, false, flag, i, minecraftserver.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> minecraftserver.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(convertable_conversionsession.levelDirectory.path(), iworlddataserver.getLevelName(), resourcekey.location(), spigotConfig, minecraftserver.registryAccess())), executor); // Paper - create paper world configs; Async-Anti-Xray: Pass executor - this.pvpMode = minecraftserver.isPvpAllowed(); - this.convertable = convertable_conversionsession; - this.uuid = WorldUUID.getUUID(convertable_conversionsession.levelDirectory.path().toFile()); -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -index 3dff328c5e4456aae291a8dec82766681f49af06..abb9a86cd42a34cf722a312068134e820ac21956 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -@@ -48,7 +48,7 @@ import org.bukkit.event.player.PlayerInteractEvent; - public class ServerPlayerGameMode { - - private static final Logger LOGGER = LogUtils.getLogger(); -- protected ServerLevel level; -+ public ServerLevel level; // Paper - Anti-Xray - protected -> public - protected final ServerPlayer player; - private GameType gameModeForPlayer; - @Nullable -@@ -329,6 +329,8 @@ public class ServerPlayerGameMode { - } - - } -+ -+ this.level.chunkPacketBlockController.onPlayerLeftClickBlock(this, pos, action, direction, worldHeight, sequence); // Paper - Anti-Xray - } - - public void destroyAndAck(BlockPos pos, int sequence, String reason) { -diff --git a/src/main/java/net/minecraft/server/network/PlayerChunkSender.java b/src/main/java/net/minecraft/server/network/PlayerChunkSender.java -index 27d0f1ed58948039004f8f1eba2f7f9609fdeec0..442bb60eb35002e54dcd6a893aca4a4584a8e017 100644 ---- a/src/main/java/net/minecraft/server/network/PlayerChunkSender.java -+++ b/src/main/java/net/minecraft/server/network/PlayerChunkSender.java -@@ -87,7 +87,10 @@ public class PlayerChunkSender { - - public static void sendChunk(ServerGamePacketListenerImpl handler, ServerLevel world, LevelChunk chunk) { // Paper - rewrite chunk loader - public - handler.player.serverLevel().chunkSource.chunkMap.getVisibleChunkIfPresent(chunk.getPos().toLong()).addPlayer(handler.player); -- handler.send(new ClientboundLevelChunkWithLightPacket(chunk, world.getLightEngine(), null, null)); -+ // Paper start - Anti-Xray -+ final boolean shouldModify = world.chunkPacketBlockController.shouldModify(handler.player, chunk); -+ handler.send(new ClientboundLevelChunkWithLightPacket(chunk, world.getLightEngine(), null, null, shouldModify)); -+ // Paper end - Anti-Xray - // Paper start - PlayerChunkLoadEvent - if (io.papermc.paper.event.packet.PlayerChunkLoadEvent.getHandlerList().getRegisteredListeners().length > 0) { - new io.papermc.paper.event.packet.PlayerChunkLoadEvent(new org.bukkit.craftbukkit.CraftChunk(chunk), handler.getPlayer().getBukkitEntity()).callEvent(); -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 57164b266d23715c224a85660531220477d4dda8..8f1a2de2a5542929f8a2fdd4d38de141eda985b2 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -420,7 +420,7 @@ public abstract class PlayerList { - .getHolderOrThrow(net.minecraft.world.level.biome.Biomes.PLAINS); - player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket( - new net.minecraft.world.level.chunk.EmptyLevelChunk(worldserver1, player.chunkPosition(), plains), -- worldserver1.getLightEngine(), (java.util.BitSet)null, (java.util.BitSet) null) -+ worldserver1.getLightEngine(), (java.util.BitSet)null, (java.util.BitSet) null, true) - ); - } - // Paper end - Send empty chunk -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index 2d829eb4d39aec82c23a55f30f22a9a5f7003d27..ff8d947c5a498e097a4c480a5dc9af71b0be5ee9 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -169,6 +169,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - } - // Paper end - add paper world config - -+ public final com.destroystokyo.paper.antixray.ChunkPacketBlockController chunkPacketBlockController; // Paper - Anti-Xray - public final co.aikar.timings.WorldTimingsHandler timings; // Paper - public static BlockPos lastPhysicsProblem; // Spigot - private org.spigotmc.TickLimiter entityLimiter; -@@ -203,7 +204,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - - public abstract ResourceKey getTypeKey(); - -- protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, RegistryAccess iregistrycustom, Holder holder, Supplier supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function paperWorldConfigCreator) { // Paper - create paper world config -+ protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, RegistryAccess iregistrycustom, Holder holder, Supplier supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - create paper world config; Async-Anti-Xray: Pass executor - this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot - this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config - this.generator = gen; -@@ -285,6 +286,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - this.timings = new co.aikar.timings.WorldTimingsHandler(this); // Paper - code below can generate new world and access timings - this.entityLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.entityMaxTickTime); - this.tileLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.tileMaxTickTime); -+ this.chunkPacketBlockController = this.paperConfig().anticheat.antiXray.enabled ? new com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor) : com.destroystokyo.paper.antixray.ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray - } - - // Paper start - Cancel hit for vanished players -@@ -560,6 +562,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - // CraftBukkit end - - BlockState iblockdata1 = chunk.setBlockState(pos, state, (flags & 64) != 0, (flags & 1024) == 0); // CraftBukkit custom NO_PLACE flag -+ this.chunkPacketBlockController.onBlockChange(this, pos, state, iblockdata1, flags, maxUpdateDepth); // Paper - Anti-Xray - - if (iblockdata1 == null) { - // CraftBukkit start - remove blockstate if failed (or the same) -diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java -index 383dc47c81b3f34e8166bce180a51a2ccbfaf6ca..1aac95b03a9e2e37c24f2a30bcb259c1424e1c78 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java -+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java -@@ -155,17 +155,17 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom - } - } - -- ChunkAccess.replaceMissingSections(biomeRegistry, this.sections); -+ this.replaceMissingSections(biomeRegistry, this.sections); // Paper - Anti-Xray - make it a non-static method - // CraftBukkit start - this.biomeRegistry = biomeRegistry; - } - public final Registry biomeRegistry; - // CraftBukkit end - -- private static void replaceMissingSections(Registry biomeRegistry, LevelChunkSection[] sectionArray) { -+ private void replaceMissingSections(Registry biomeRegistry, LevelChunkSection[] sectionArray) { // Paper - Anti-Xray - static -> non-static - for (int i = 0; i < sectionArray.length; ++i) { - if (sectionArray[i] == null) { -- sectionArray[i] = new LevelChunkSection(biomeRegistry); -+ sectionArray[i] = new LevelChunkSection(biomeRegistry, this.levelHeightAccessor instanceof net.minecraft.world.level.Level ? (net.minecraft.world.level.Level) this.levelHeightAccessor : null, this.chunkPos, this.levelHeightAccessor.getSectionYFromSectionIndex(i)); // Paper start - Anti-Xray - Add parameters - } - } - -diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -index 5d94aee1303d9eca5f1fa9a2e033ad0d12909635..424c4613e202c6ba50fa0de65d2526d400a8e299 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -@@ -93,7 +93,7 @@ public class LevelChunk extends ChunkAccess { - } - - public LevelChunk(Level world, ChunkPos pos, UpgradeData upgradeData, LevelChunkTicks blockTickScheduler, LevelChunkTicks fluidTickScheduler, long inhabitedTime, @Nullable LevelChunkSection[] sectionArrayInitializer, @Nullable LevelChunk.PostLoadProcessor entityLoader, @Nullable BlendingData blendingData) { -- super(pos, upgradeData, world, world.registryAccess().registryOrThrow(Registries.BIOME), inhabitedTime, sectionArrayInitializer, blendingData); -+ super(pos, upgradeData, world, net.minecraft.server.MinecraftServer.getServer().registryAccess().registryOrThrow(Registries.BIOME), inhabitedTime, sectionArrayInitializer, blendingData); // Paper - Anti-Xray - The world isn't ready yet, use server singleton for registry - this.tickersInLevel = Maps.newHashMap(); - this.level = (ServerLevel) world; // CraftBukkit - type - this.gameEventListenerRegistrySections = new Int2ObjectOpenHashMap(); -diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java -index 90d1c3e23e753c29660f7d993b3c90ac022941c3..f2e11bff414b521295bde721e01ae2166a6a3fd6 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java -+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java -@@ -33,9 +33,12 @@ public class LevelChunkSection { - this.recalcBlockCounts(); - } - -- public LevelChunkSection(Registry biomeRegistry) { -- this.states = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES); -- this.biomes = new PalettedContainer<>(biomeRegistry.asHolderIdMap(), biomeRegistry.getHolderOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES); -+ // Paper start - Anti-Xray - Add parameters -+ @Deprecated @io.papermc.paper.annotation.DoNotUse public LevelChunkSection(Registry biomeRegistry) { this(biomeRegistry, null, null, 0); } -+ public LevelChunkSection(Registry biomeRegistry, net.minecraft.world.level.Level level, net.minecraft.world.level.ChunkPos chunkPos, int chunkSectionY) { -+ // Paper end -+ this.states = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES, level == null || level.chunkPacketBlockController == null ? null : level.chunkPacketBlockController.getPresetBlockStates(level, chunkPos, chunkSectionY)); // Paper - Anti-Xray - Add preset block states -+ this.biomes = new PalettedContainer<>(biomeRegistry.asHolderIdMap(), biomeRegistry.getHolderOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES, null); // Paper - Anti-Xray - Add preset biomes - } - - public BlockState getBlockState(int x, int y, int z) { -@@ -172,10 +175,13 @@ public class LevelChunkSection { - this.biomes = datapaletteblock; - } - -- public void write(FriendlyByteBuf buf) { -+ // Paper start - Anti-Xray - Add chunk packet info -+ @Deprecated @io.papermc.paper.annotation.DoNotUse public void write(FriendlyByteBuf buf) { this.write(buf, null, 0); } -+ public void write(FriendlyByteBuf buf, com.destroystokyo.paper.antixray.ChunkPacketInfo chunkPacketInfo, int chunkSectionIndex) { - buf.writeShort(this.nonEmptyBlockCount); -- this.states.write(buf); -- this.biomes.write(buf); -+ this.states.write(buf, chunkPacketInfo, chunkSectionIndex); -+ this.biomes.write(buf, null, chunkSectionIndex); -+ // Paper end - } - - public int getSerializedSize() { -diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java -index fa9df6ebcd90d4e9e5836a37212b1f60665783b1..926c81a25180d508d662eb3fa35f771636164694 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java -+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java -@@ -28,6 +28,7 @@ public class PalettedContainer implements PaletteResize, PalettedContainer - private static final int MIN_PALETTE_BITS = 0; - private final PaletteResize dummyPaletteResize = (newSize, added) -> 0; - public final IdMap registry; -+ private final T @org.jetbrains.annotations.Nullable [] presetValues; // Paper - Anti-Xray - Add preset values - private volatile PalettedContainer.Data data; - private final PalettedContainer.Strategy strategy; - // private final ThreadingDetector threadingDetector = new ThreadingDetector("PalettedContainer"); // Paper - unused -@@ -40,14 +41,19 @@ public class PalettedContainer implements PaletteResize, PalettedContainer - // this.threadingDetector.checkAndUnlock(); // Paper - disable this - } - -- public static Codec> codecRW(IdMap idList, Codec entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue) { -- PalettedContainerRO.Unpacker> unpacker = PalettedContainer::unpack; -+ // Paper start - Anti-Xray - Add preset values -+ @Deprecated @io.papermc.paper.annotation.DoNotUse public static Codec> codecRW(IdMap idList, Codec entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue) { return PalettedContainer.codecRW(idList, entryCodec, paletteProvider, defaultValue, null); } -+ public static Codec> codecRW(IdMap idList, Codec entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue, T @org.jetbrains.annotations.Nullable [] presetValues) { -+ PalettedContainerRO.Unpacker> unpacker = (idListx, paletteProviderx, serialized) -> { -+ return unpack(idListx, paletteProviderx, serialized, defaultValue, presetValues); -+ }; -+ // Paper end - return codec(idList, entryCodec, paletteProvider, defaultValue, unpacker); - } - - public static Codec> codecRO(IdMap idList, Codec entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue) { - PalettedContainerRO.Unpacker> unpacker = (idListx, paletteProviderx, serialized) -> unpack( -- idListx, paletteProviderx, serialized -+ idListx, paletteProviderx, serialized, defaultValue, null // Paper - Anti-Xray - Add preset values - ) - .map(result -> (PalettedContainerRO)result); - return codec(idList, entryCodec, paletteProvider, defaultValue, unpacker); -@@ -71,25 +77,58 @@ public class PalettedContainer implements PaletteResize, PalettedContainer - ); - } - -+ // Paper start - Anti-Xray - Add preset values -+ @Deprecated @io.papermc.paper.annotation.DoNotUse public PalettedContainer(IdMap idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Configuration dataProvider, BitStorage storage, List paletteEntries) { this(idList, paletteProvider, dataProvider, storage, paletteEntries, null, null); } - public PalettedContainer( - IdMap idList, - PalettedContainer.Strategy paletteProvider, - PalettedContainer.Configuration dataProvider, - BitStorage storage, -- List paletteEntries -+ List paletteEntries, T defaultValue, T @org.jetbrains.annotations.Nullable [] presetValues - ) { -+ this.presetValues = presetValues; - this.registry = idList; - this.strategy = paletteProvider; - this.data = new PalettedContainer.Data<>(dataProvider, storage, dataProvider.factory().create(dataProvider.bits(), idList, this, paletteEntries)); -+ -+ if (presetValues != null && (dataProvider.factory() == PalettedContainer.Strategy.SINGLE_VALUE_PALETTE_FACTORY ? this.data.palette.valueFor(0) != defaultValue : dataProvider.factory() != PalettedContainer.Strategy.GLOBAL_PALETTE_FACTORY)) { -+ // In 1.18 Mojang unfortunately removed code that already handled possible resize operations on read from disk for us -+ // We readd this here but in a smarter way than it was before -+ int maxSize = 1 << dataProvider.bits(); -+ -+ for (T presetValue : presetValues) { -+ if (this.data.palette.getSize() >= maxSize) { -+ java.util.Set allValues = new java.util.HashSet<>(paletteEntries); -+ allValues.addAll(Arrays.asList(presetValues)); -+ int newBits = Mth.ceillog2(allValues.size()); -+ -+ if (newBits > dataProvider.bits()) { -+ this.onResize(newBits, null); -+ } -+ -+ break; -+ } -+ -+ this.data.palette.idFor(presetValue); -+ } -+ } -+ // Paper end - } - -- private PalettedContainer(IdMap idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Data data) { -+ // Paper start - Anti-Xray - Add preset values -+ private PalettedContainer(IdMap idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Data data, T @org.jetbrains.annotations.Nullable [] presetValues) { -+ this.presetValues = presetValues; -+ // Paper end - this.registry = idList; - this.strategy = paletteProvider; - this.data = data; - } - -- public PalettedContainer(IdMap idList, T object, PalettedContainer.Strategy paletteProvider) { -+ // Paper start - Anti-Xray - Add preset values -+ @Deprecated @io.papermc.paper.annotation.DoNotUse public PalettedContainer(IdMap idList, T object, PalettedContainer.Strategy paletteProvider) { this(idList, object, paletteProvider, null); } -+ public PalettedContainer(IdMap idList, T object, PalettedContainer.Strategy paletteProvider, T @org.jetbrains.annotations.Nullable [] presetValues) { -+ this.presetValues = presetValues; -+ // Paper end - this.strategy = paletteProvider; - this.registry = idList; - this.data = this.createOrReuseData(null, 0); -@@ -106,11 +145,33 @@ public class PalettedContainer implements PaletteResize, PalettedContainer - @Override - public synchronized int onResize(int newBits, T object) { // Paper - synchronize - PalettedContainer.Data data = this.data; -+ -+ // Paper start - Anti-Xray - Add preset values -+ if (this.presetValues != null && object != null && data.configuration().factory() == PalettedContainer.Strategy.SINGLE_VALUE_PALETTE_FACTORY) { -+ int duplicates = 0; -+ List presetValues = Arrays.asList(this.presetValues); -+ duplicates += presetValues.contains(object) ? 1 : 0; -+ duplicates += presetValues.contains(data.palette.valueFor(0)) ? 1 : 0; -+ newBits = Mth.ceillog2((1 << this.strategy.calculateBitsForSerialization(this.registry, 1 << newBits)) + presetValues.size() - duplicates); -+ } -+ - PalettedContainer.Data data2 = this.createOrReuseData(data, newBits); - data2.copyFrom(data.palette, data.storage); - this.data = data2; -- return data2.palette.idFor(object); -+ this.addPresetValues(); -+ return object == null ? -1 : data2.palette.idFor(object); -+ // Paper end -+ } -+ -+ // Paper start - Anti-Xray - Add preset values -+ private void addPresetValues() { -+ if (this.presetValues != null && this.data.configuration().factory() != PalettedContainer.Strategy.GLOBAL_PALETTE_FACTORY) { -+ for (T presetValue : this.presetValues) { -+ this.data.palette.idFor(presetValue); -+ } -+ } - } -+ // Paper end - - public T getAndSet(int x, int y, int z, T value) { - this.acquire(); -@@ -177,24 +238,33 @@ public class PalettedContainer implements PaletteResize, PalettedContainer - data.palette.read(buf); - buf.readLongArray(data.storage.getRaw()); - this.data = data; -+ this.addPresetValues(); // Paper - Anti-Xray - Add preset values (inefficient, but this isn't used by the server) - } finally { - this.release(); - } - } - -+ // Paper start - Anti-Xray; Add chunk packet info -+ @Override -+ @Deprecated @io.papermc.paper.annotation.DoNotUse public void write(FriendlyByteBuf buf) { this.write(buf, null, 0); } - @Override -- public synchronized void write(FriendlyByteBuf buf) { // Paper - synchronize -+ public synchronized void write(FriendlyByteBuf buf, @Nullable com.destroystokyo.paper.antixray.ChunkPacketInfo chunkPacketInfo, int chunkSectionIndex) { // Paper - Synchronize - this.acquire(); - - try { -- this.data.write(buf); -+ this.data.write(buf, chunkPacketInfo, chunkSectionIndex); -+ -+ if (chunkPacketInfo != null) { -+ chunkPacketInfo.setPresetValues(chunkSectionIndex, this.presetValues); -+ } -+ // Paper end - } finally { - this.release(); - } - } - - private static DataResult> unpack( -- IdMap idList, PalettedContainer.Strategy paletteProvider, PalettedContainerRO.PackedData serialized -+ IdMap idList, PalettedContainer.Strategy paletteProvider, PalettedContainerRO.PackedData serialized, T defaultValue, T @org.jetbrains.annotations.Nullable [] presetValues // Paper - Anti-Xray - Add preset values - ) { - List list = serialized.paletteEntries(); - int i = paletteProvider.size(); -@@ -227,7 +297,7 @@ public class PalettedContainer implements PaletteResize, PalettedContainer - } - } - -- return DataResult.success(new PalettedContainer<>(idList, paletteProvider, configuration, bitStorage, list)); -+ return DataResult.success(new PalettedContainer<>(idList, paletteProvider, configuration, bitStorage, list, defaultValue, presetValues)); // Paper - Anti-Xray - Add preset values - } - - @Override -@@ -284,12 +354,12 @@ public class PalettedContainer implements PaletteResize, PalettedContainer - } - - public PalettedContainer copy() { -- return new PalettedContainer<>(this.registry, this.strategy, this.data.copy()); -+ return new PalettedContainer<>(this.registry, this.strategy, this.data.copy(), this.presetValues); // Paper - Anti-Xray - Add preset values - } - - @Override - public PalettedContainer recreate() { -- return new PalettedContainer<>(this.registry, this.data.palette.valueFor(0), this.strategy); -+ return new PalettedContainer<>(this.registry, this.data.palette.valueFor(0), this.strategy, this.presetValues); // Paper - Anti-Xray - Add preset values - } - - @Override -@@ -328,9 +398,18 @@ public class PalettedContainer implements PaletteResize, PalettedContainer - return 1 + this.palette.getSerializedSize() + VarInt.getByteSize(this.storage.getRaw().length) + this.storage.getRaw().length * 8; - } - -- public void write(FriendlyByteBuf buf) { -+ // Paper start - Anti-Xray - Add chunk packet info -+ public void write(FriendlyByteBuf buf, @Nullable com.destroystokyo.paper.antixray.ChunkPacketInfo chunkPacketInfo, int chunkSectionIndex) { - buf.writeByte(this.storage.getBits()); - this.palette.write(buf); -+ -+ if (chunkPacketInfo != null) { -+ chunkPacketInfo.setBits(chunkSectionIndex, this.configuration.bits()); -+ chunkPacketInfo.setPalette(chunkSectionIndex, this.palette); -+ chunkPacketInfo.setIndex(chunkSectionIndex, buf.writerIndex() + VarInt.getByteSize(this.storage.getRaw().length)); -+ } -+ // Paper end -+ - buf.writeLongArray(this.storage.getRaw()); - } - -diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainerRO.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainerRO.java -index 9a2bf744abd8916d492e901be889223591bac3fd..1dd415c96d17eff8e7555c33d3c52e57f2559fa5 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainerRO.java -+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainerRO.java -@@ -14,7 +14,10 @@ public interface PalettedContainerRO { - - void getAll(Consumer action); - -- void write(FriendlyByteBuf buf); -+ // Paper start - Anti-Xray - Add chunk packet info -+ @Deprecated @io.papermc.paper.annotation.DoNotUse void write(FriendlyByteBuf buf); -+ void write(FriendlyByteBuf buf, @javax.annotation.Nullable com.destroystokyo.paper.antixray.ChunkPacketInfo chunkPacketInfo, int chunkSectionIndex); -+ // Paper end - - int getSerializedSize(); - -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java -index b2e8082dda0d0ca32bfc32cbf9d8ae9822febc30..e64fe79b231987e240d7482c0d6fa3c89dc97462 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java -@@ -73,7 +73,7 @@ import org.slf4j.Logger; - - public class ChunkSerializer { - -- public static final Codec> BLOCK_STATE_CODEC = PalettedContainer.codecRW(Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState()); -+ public static final Codec> BLOCK_STATE_CODEC = PalettedContainer.codecRW(Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState(), null); // Paper - Anti-Xray - Add preset block states - private static final Logger LOGGER = LogUtils.getLogger(); - private static final String TAG_UPGRADE_DATA = "UpgradeData"; - private static final String BLOCK_TICKS_TAG = "block_ticks"; -@@ -171,13 +171,17 @@ public class ChunkSerializer { - - if (k >= 0 && k < achunksection.length) { - PalettedContainer datapaletteblock; -+ // Paper start - Anti-Xray - Add preset block states -+ BlockState[] presetBlockStates = world.chunkPacketBlockController.getPresetBlockStates(world, chunkPos, b0); - - if (nbttagcompound1.contains("block_states", 10)) { -- datapaletteblock = (PalettedContainer) ChunkSerializer.BLOCK_STATE_CODEC.parse(NbtOps.INSTANCE, nbttagcompound1.getCompound("block_states")).promotePartial((s) -> { -+ Codec> blockStateCodec = presetBlockStates == null ? ChunkSerializer.BLOCK_STATE_CODEC : PalettedContainer.codecRW(Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState(), presetBlockStates); -+ datapaletteblock = blockStateCodec.parse(NbtOps.INSTANCE, nbttagcompound1.getCompound("block_states")).promotePartial((s) -> { - ChunkSerializer.logErrors(chunkPos, b0, s); - }).getOrThrow(ChunkSerializer.ChunkReadException::new); - } else { -- datapaletteblock = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES); -+ datapaletteblock = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES, presetBlockStates); -+ // Paper end - } - - PalettedContainer object; // CraftBukkit - read/write -@@ -187,7 +191,7 @@ public class ChunkSerializer { - ChunkSerializer.logErrors(chunkPos, b0, s); - }).getOrThrow(ChunkSerializer.ChunkReadException::new); - } else { -- object = new PalettedContainer<>(iregistry.asHolderIdMap(), iregistry.getHolderOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES); -+ object = new PalettedContainer<>(iregistry.asHolderIdMap(), iregistry.getHolderOrThrow(Biomes.PLAINS), PalettedContainer.Strategy.SECTION_BIOMES, null); // Paper - Anti-Xray - Add preset biomes - } - - LevelChunkSection chunksection = new LevelChunkSection(datapaletteblock, (PalettedContainer) object); // CraftBukkit - read/write -@@ -423,7 +427,7 @@ public class ChunkSerializer { - - // CraftBukkit start - read/write - private static Codec>> makeBiomeCodecRW(Registry iregistry) { -- return PalettedContainer.codecRW(iregistry.asHolderIdMap(), iregistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, iregistry.getHolderOrThrow(Biomes.PLAINS)); -+ return PalettedContainer.codecRW(iregistry.asHolderIdMap(), iregistry.holderByNameCodec(), PalettedContainer.Strategy.SECTION_BIOMES, iregistry.getHolderOrThrow(Biomes.PLAINS), null); // Paper - Anti-Xray - Add preset biomes - } - // CraftBukkit end - -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java -index 1e2530c9e5212b6d2bdbc94817beddb4247dac73..82b4bd669c57b18fb0b443bcd94495023cd5a528 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java -@@ -55,7 +55,7 @@ public class CraftChunk implements Chunk { - private final ServerLevel worldServer; - private final int x; - private final int z; -- private static final PalettedContainer emptyBlockIDs = new PalettedContainer<>(net.minecraft.world.level.block.Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES); -+ private static final PalettedContainer emptyBlockIDs = new PalettedContainer<>(net.minecraft.world.level.block.Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES, null); // Paper - Anti-Xray - Add preset block states - private static final byte[] FULL_LIGHT = new byte[2048]; - private static final byte[] EMPTY_LIGHT = new byte[2048]; - -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index 66fa93a44a348f9cd300ec0c4a2d09a7a2bf47e1..ac9873c430a3ece5c4b5f5305496639a54ce35f5 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -@@ -2659,7 +2659,7 @@ public final class CraftServer implements Server { - public ChunkGenerator.ChunkData createChunkData(World world) { - Preconditions.checkArgument(world != null, "World cannot be null"); - ServerLevel handle = ((CraftWorld) world).getHandle(); -- return new OldCraftChunkData(world.getMinHeight(), world.getMaxHeight(), handle.registryAccess().registryOrThrow(Registries.BIOME)); -+ return new OldCraftChunkData(world.getMinHeight(), world.getMaxHeight(), handle.registryAccess().registryOrThrow(Registries.BIOME), world); // Paper - Anti-Xray - Add parameters - } - - // Paper start - Allow delegation to vanilla chunk gen -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index 0ce0589204a01051aa1251d6af752eed4ac69c0f..f2b20ed5063a293f0b464548f590d652170cd1d8 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -@@ -541,11 +541,16 @@ public class CraftWorld extends CraftRegionAccessor implements World { - List playersInRange = playerChunk.playerProvider.getPlayers(playerChunk.getPos(), false); - if (playersInRange.isEmpty()) return true; // Paper - rewrite player chunk loader - -- ClientboundLevelChunkWithLightPacket refreshPacket = new ClientboundLevelChunkWithLightPacket(chunk, this.world.getLightEngine(), null, null); -+ // Paper start - Anti-Xray - Bypass -+ Map refreshPackets = new HashMap<>(); - for (ServerPlayer player : playersInRange) { - if (player.connection == null) continue; - -- player.connection.send(refreshPacket); -+ Boolean shouldModify = chunk.getLevel().chunkPacketBlockController.shouldModify(player, chunk); -+ player.connection.send(refreshPackets.computeIfAbsent(shouldModify, s -> { // Use connection to prevent creating firing event -+ return new ClientboundLevelChunkWithLightPacket(chunk, this.world.getLightEngine(), null, null, (Boolean) s); -+ })); -+ // Paper end - } - // Paper - rewrite player chunk loader - -diff --git a/src/main/java/org/bukkit/craftbukkit/generator/OldCraftChunkData.java b/src/main/java/org/bukkit/craftbukkit/generator/OldCraftChunkData.java -index 9b640705f2c810160aa7fea5006429ec41d0c858..44a010590e830fd238cf6fdda443e28b72022e66 100644 ---- a/src/main/java/org/bukkit/craftbukkit/generator/OldCraftChunkData.java -+++ b/src/main/java/org/bukkit/craftbukkit/generator/OldCraftChunkData.java -@@ -27,8 +27,13 @@ public final class OldCraftChunkData implements ChunkGenerator.ChunkData { - private final Registry biomes; - private Set tiles; - private final Set lights = new HashSet<>(); -+ // Paper start - Anti-Xray - Add parameters -+ private final org.bukkit.World world; - -- public OldCraftChunkData(int minHeight, int maxHeight, Registry biomes) { -+ @Deprecated @io.papermc.paper.annotation.DoNotUse public OldCraftChunkData(int minHeight, int maxHeight, Registry biomes) { this(minHeight, maxHeight, biomes, null); } -+ public OldCraftChunkData(int minHeight, int maxHeight, Registry biomes, org.bukkit.World world) { -+ this.world = world; -+ // Paper end - this.minHeight = minHeight; - this.maxHeight = maxHeight; - this.biomes = biomes; -@@ -176,7 +181,7 @@ public final class OldCraftChunkData implements ChunkGenerator.ChunkData { - int offset = (y - this.minHeight) >> 4; - LevelChunkSection section = this.sections[offset]; - if (create && section == null) { -- this.sections[offset] = section = new LevelChunkSection(this.biomes); -+ this.sections[offset] = section = new LevelChunkSection(this.biomes, this.world instanceof org.bukkit.craftbukkit.CraftWorld ? ((org.bukkit.craftbukkit.CraftWorld) this.world).getHandle() : null, null, offset + (this.minHeight >> 4)); // Paper - Anti-Xray - Add parameters - } - return section; - } diff --git a/patches/server/0997-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch b/patches/server/0997-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch new file mode 100644 index 0000000000..12af227d2a --- /dev/null +++ b/patches/server/0997-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch @@ -0,0 +1,89 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: kickash32 +Date: Mon, 5 Apr 2021 01:42:35 -0400 +Subject: [PATCH] Improve cancelling PreCreatureSpawnEvent with per player mob + spawns + + +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index 56f855ed9dbbb9a7025bef6f9e98a8cdcf2ad4fc..4621c33ed73b0db64e78e7b9be7013a2ba7393c8 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -309,8 +309,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + } + ++ // Paper start - per player mob count backoff ++ public void updateFailurePlayerMobTypeMap(int chunkX, int chunkZ, net.minecraft.world.entity.MobCategory mobCategory) { ++ if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { ++ return; ++ } ++ int idx = mobCategory.ordinal(); ++ final com.destroystokyo.paper.util.maplist.ReferenceList inRange = ++ this.getNearbyPlayers().getPlayersByChunk(chunkX, chunkZ, io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE); ++ if (inRange == null) { ++ return; ++ } ++ final Object[] backingSet = inRange.getRawData(); ++ for (int i = 0, len = inRange.size(); i < len; i++) { ++ ++((ServerPlayer)backingSet[i]).mobBackoffCounts[idx]; ++ } ++ } ++ // Paper end - per player mob count backoff + public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) { +- return player.mobCounts[mobCategory.ordinal()]; ++ return player.mobCounts[mobCategory.ordinal()] + player.mobBackoffCounts[mobCategory.ordinal()]; // Paper - per player mob count backoff + } + // Paper end - Optional per player mob spawns + +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index 002aad174fb1781ee963414037f84e5120488592..c7b7f153895a4b95b2071a31db00c9c4b69fa094 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -522,7 +522,17 @@ public class ServerChunkCache extends ChunkSource { + if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled + // re-set mob counts + for (ServerPlayer player : this.level.players) { +- Arrays.fill(player.mobCounts, 0); ++ // Paper start - per player mob spawning backoff ++ for (int ii = 0; ii < ServerPlayer.MOBCATEGORY_TOTAL_ENUMS; ii++) { ++ player.mobCounts[ii] = 0; ++ ++ int newBackoff = player.mobBackoffCounts[ii] - 1; // TODO make configurable bleed // TODO use nonlinear algorithm? ++ if (newBackoff < 0) { ++ newBackoff = 0; ++ } ++ player.mobBackoffCounts[ii] = newBackoff; ++ } ++ // Paper end - per player mob spawning backoff + } + spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true); + } else { +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index 61e256224c6d7c944bee0e11d0edcf05b8a5ecd3..e06b9b0c7b1c3a200b89819e0e521f1c88dc981b 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -275,6 +275,7 @@ public class ServerPlayer extends Player { + public static final int MOBCATEGORY_TOTAL_ENUMS = net.minecraft.world.entity.MobCategory.values().length; + public final int[] mobCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper + // Paper end - Optional per player mob spawns ++ public final int[] mobBackoffCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper - per player mob count backoff + + // CraftBukkit start + public CraftPlayer.TransferCookieConnection transferCookieConnection; +diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +index 0679636530ca1afd077c43b0fa605a2ac74e0522..ed8032495af9ce9c23419224814b8d27e4a97c17 100644 +--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java ++++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +@@ -272,6 +272,11 @@ public final class NaturalSpawner { + + // Paper start - PreCreatureSpawnEvent + PreSpawnStatus doSpawning = isValidSpawnPostitionForType(world, group, structuremanager, chunkgenerator, biomesettingsmobs_c, blockposition_mutableblockposition, d2); ++ // Paper start - per player mob count backoff ++ if (doSpawning == PreSpawnStatus.ABORT || doSpawning == PreSpawnStatus.CANCELLED) { ++ world.getChunkSource().chunkMap.updateFailurePlayerMobTypeMap(blockposition_mutableblockposition.getX() >> 4, blockposition_mutableblockposition.getZ() >> 4, group); ++ } ++ // Paper end - per player mob count backoff + if (doSpawning == PreSpawnStatus.ABORT) { + return j; // Paper - Optional per player mob spawns + } diff --git a/patches/server/0998-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch b/patches/server/0998-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch deleted file mode 100644 index 12af227d2a..0000000000 --- a/patches/server/0998-Improve-cancelling-PreCreatureSpawnEvent-with-per-pl.patch +++ /dev/null @@ -1,89 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: kickash32 -Date: Mon, 5 Apr 2021 01:42:35 -0400 -Subject: [PATCH] Improve cancelling PreCreatureSpawnEvent with per player mob - spawns - - -diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java -index 56f855ed9dbbb9a7025bef6f9e98a8cdcf2ad4fc..4621c33ed73b0db64e78e7b9be7013a2ba7393c8 100644 ---- a/src/main/java/net/minecraft/server/level/ChunkMap.java -+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java -@@ -309,8 +309,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - } - -+ // Paper start - per player mob count backoff -+ public void updateFailurePlayerMobTypeMap(int chunkX, int chunkZ, net.minecraft.world.entity.MobCategory mobCategory) { -+ if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { -+ return; -+ } -+ int idx = mobCategory.ordinal(); -+ final com.destroystokyo.paper.util.maplist.ReferenceList inRange = -+ this.getNearbyPlayers().getPlayersByChunk(chunkX, chunkZ, io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE); -+ if (inRange == null) { -+ return; -+ } -+ final Object[] backingSet = inRange.getRawData(); -+ for (int i = 0, len = inRange.size(); i < len; i++) { -+ ++((ServerPlayer)backingSet[i]).mobBackoffCounts[idx]; -+ } -+ } -+ // Paper end - per player mob count backoff - public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) { -- return player.mobCounts[mobCategory.ordinal()]; -+ return player.mobCounts[mobCategory.ordinal()] + player.mobBackoffCounts[mobCategory.ordinal()]; // Paper - per player mob count backoff - } - // Paper end - Optional per player mob spawns - -diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -index 002aad174fb1781ee963414037f84e5120488592..c7b7f153895a4b95b2071a31db00c9c4b69fa094 100644 ---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java -+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -@@ -522,7 +522,17 @@ public class ServerChunkCache extends ChunkSource { - if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled - // re-set mob counts - for (ServerPlayer player : this.level.players) { -- Arrays.fill(player.mobCounts, 0); -+ // Paper start - per player mob spawning backoff -+ for (int ii = 0; ii < ServerPlayer.MOBCATEGORY_TOTAL_ENUMS; ii++) { -+ player.mobCounts[ii] = 0; -+ -+ int newBackoff = player.mobBackoffCounts[ii] - 1; // TODO make configurable bleed // TODO use nonlinear algorithm? -+ if (newBackoff < 0) { -+ newBackoff = 0; -+ } -+ player.mobBackoffCounts[ii] = newBackoff; -+ } -+ // Paper end - per player mob spawning backoff - } - spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true); - } else { -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index 61e256224c6d7c944bee0e11d0edcf05b8a5ecd3..e06b9b0c7b1c3a200b89819e0e521f1c88dc981b 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -275,6 +275,7 @@ public class ServerPlayer extends Player { - public static final int MOBCATEGORY_TOTAL_ENUMS = net.minecraft.world.entity.MobCategory.values().length; - public final int[] mobCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper - // Paper end - Optional per player mob spawns -+ public final int[] mobBackoffCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper - per player mob count backoff - - // CraftBukkit start - public CraftPlayer.TransferCookieConnection transferCookieConnection; -diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java -index 0679636530ca1afd077c43b0fa605a2ac74e0522..ed8032495af9ce9c23419224814b8d27e4a97c17 100644 ---- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java -+++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java -@@ -272,6 +272,11 @@ public final class NaturalSpawner { - - // Paper start - PreCreatureSpawnEvent - PreSpawnStatus doSpawning = isValidSpawnPostitionForType(world, group, structuremanager, chunkgenerator, biomesettingsmobs_c, blockposition_mutableblockposition, d2); -+ // Paper start - per player mob count backoff -+ if (doSpawning == PreSpawnStatus.ABORT || doSpawning == PreSpawnStatus.CANCELLED) { -+ world.getChunkSource().chunkMap.updateFailurePlayerMobTypeMap(blockposition_mutableblockposition.getX() >> 4, blockposition_mutableblockposition.getZ() >> 4, group); -+ } -+ // Paper end - per player mob count backoff - if (doSpawning == PreSpawnStatus.ABORT) { - return j; // Paper - Optional per player mob spawns - } diff --git a/patches/server/0998-Optimize-Collision-to-not-load-chunks.patch b/patches/server/0998-Optimize-Collision-to-not-load-chunks.patch new file mode 100644 index 0000000000..17286aaa99 --- /dev/null +++ b/patches/server/0998-Optimize-Collision-to-not-load-chunks.patch @@ -0,0 +1,109 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Thu, 2 Apr 2020 02:37:57 -0400 +Subject: [PATCH] Optimize Collision to not load chunks + +The collision code takes an AABB and generates a cuboid of checks rather +than a cylinder, so at high velocity this can generate a lot of chunk checks. + +Treat an unloaded chunk as a collision for entities, and also for players if +the "prevent moving into unloaded chunks" setting is enabled. + +If that serting is not enabled, collisions will be ignored for players, since +movement will load only the chunk the player enters anyways and avoids loading +massive amounts of surrounding chunks due to large AABB lookups. + +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 8f1a2de2a5542929f8a2fdd4d38de141eda985b2..29b35b8c0a6001d626e327a82eaff26d068660db 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -934,6 +934,7 @@ public abstract class PlayerList { + entityplayer1.setShiftKeyDown(false); + entityplayer1.forceSetPositionRotation(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch()); + ++ worldserver1.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.POST_TELEPORT, new net.minecraft.world.level.ChunkPos(location.getBlockX() >> 4, location.getBlockZ() >> 4), 1, entityplayer.getId()); // Paper + while (avoidSuffocation && !worldserver1.noCollision((Entity) entityplayer1) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) { + // CraftBukkit end + entityplayer1.setPos(entityplayer1.getX(), entityplayer1.getY() + 1.0D, entityplayer1.getZ()); +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index a13edd1165a5ba4dd3f5c323e454926e7fe75c07..b57644317b5085d74d11ac6ba858c3747d703a47 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -245,6 +245,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + public org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason; // Paper - Entity#getEntitySpawnReason + + public com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData; // Paper ++ public boolean collisionLoadChunks = false; // Paper + private CraftEntity bukkitEntity; + + public @org.jetbrains.annotations.Nullable net.minecraft.server.level.ChunkMap.TrackedEntity tracker; // Paper +diff --git a/src/main/java/net/minecraft/world/level/BlockCollisions.java b/src/main/java/net/minecraft/world/level/BlockCollisions.java +index 1c10835b59aaefa3a65ff64f784620bdc54ddcdc..cd89623a44f02d7db77f0d0f87545cf80841f403 100644 +--- a/src/main/java/net/minecraft/world/level/BlockCollisions.java ++++ b/src/main/java/net/minecraft/world/level/BlockCollisions.java +@@ -66,18 +66,37 @@ public class BlockCollisions extends AbstractIterator { + @Override + protected T computeNext() { + while (this.cursor.advance()) { +- int i = this.cursor.nextX(); +- int j = this.cursor.nextY(); +- int k = this.cursor.nextZ(); ++ int i = this.cursor.nextX(); final int x = i; // Paper - OBFHELPER ++ int j = this.cursor.nextY(); final int y = j; // Paper - OBFHELPER ++ int k = this.cursor.nextZ(); final int z = k; // Paper - OBFHELPER + int l = this.cursor.getNextType(); + if (l != 3) { +- BlockGetter blockGetter = this.getChunk(i, k); +- if (blockGetter != null) { +- this.pos.set(i, j, k); +- BlockState blockState = blockGetter.getBlockState(this.pos); +- if ((!this.onlySuffocatingBlocks || blockState.isSuffocating(blockGetter, this.pos)) +- && (l != 1 || blockState.hasLargeCollisionShape()) +- && (l != 2 || blockState.is(Blocks.MOVING_PISTON))) { ++ // Paper start - ensure we don't load chunks ++ // BlockGetter blockGetter = this.getChunk(i, k); ++ if (true) { ++ final @Nullable Entity source = this.context instanceof net.minecraft.world.phys.shapes.EntityCollisionContext entityContext ? entityContext.getEntity() : null; ++ boolean far = source != null && io.papermc.paper.util.MCUtil.distanceSq(source.getX(), y, source.getZ(), x, y, z) > 14; ++ this.pos.set(x, y, z); ++ BlockState blockState; ++ if (this.collisionGetter instanceof net.minecraft.server.level.WorldGenRegion) { ++ BlockGetter blockGetter = this.getChunk(x, z); ++ if (blockGetter == null) { ++ continue; ++ } ++ blockState = blockGetter.getBlockState(this.pos); ++ } else if ((!far && source instanceof net.minecraft.server.level.ServerPlayer) || (source != null && source.collisionLoadChunks)) { ++ blockState = this.collisionGetter.getBlockState(this.pos); ++ } else { ++ blockState = this.collisionGetter.getBlockStateIfLoaded(this.pos); ++ } ++ if (blockState == null) { ++ if (!(source instanceof net.minecraft.server.level.ServerPlayer) || source.level().paperConfig().chunks.preventMovingIntoUnloadedChunks) { ++ return this.resultProvider.apply(new BlockPos.MutableBlockPos(x, y, z), Shapes.create(far ? source.getBoundingBox() : new AABB(new BlockPos(x, y, z)))); ++ } ++ continue; ++ } ++ if (/*(!this.onlySuffocatingBlocks || blockState.isSuffocating(blockGetter, this.pos)) &&*/ (l != 1 || blockState.hasLargeCollisionShape()) && (l != 2 || blockState.is(Blocks.MOVING_PISTON))) { // Paper - onlySuffocatingBlocks is only true on the client, so we don't care about it here ++ // Paper end + VoxelShape voxelShape = blockState.getCollisionShape(this.collisionGetter, this.pos, this.context); + if (voxelShape == Shapes.block()) { + if (this.box.intersects((double)i, (double)j, (double)k, (double)i + 1.0, (double)j + 1.0, (double)k + 1.0)) { +diff --git a/src/main/java/net/minecraft/world/level/CollisionGetter.java b/src/main/java/net/minecraft/world/level/CollisionGetter.java +index e57cb7fe53e915d24246e44c7f49971f5b2ab2cf..1ad0c976c6e2d6d31397dff850a9de7c16d16fba 100644 +--- a/src/main/java/net/minecraft/world/level/CollisionGetter.java ++++ b/src/main/java/net/minecraft/world/level/CollisionGetter.java +@@ -44,11 +44,13 @@ public interface CollisionGetter extends BlockGetter { + } + + default boolean noCollision(@Nullable Entity entity, AABB box) { ++ try { if (entity != null) entity.collisionLoadChunks = true; // Paper + for (VoxelShape voxelShape : this.getBlockCollisions(entity, box)) { + if (!voxelShape.isEmpty()) { + return false; + } + } ++ } finally { if (entity != null) entity.collisionLoadChunks = false; } // Paper + + if (!this.getEntityCollisions(entity, box).isEmpty()) { + return false; diff --git a/patches/server/0999-Optimize-Collision-to-not-load-chunks.patch b/patches/server/0999-Optimize-Collision-to-not-load-chunks.patch deleted file mode 100644 index 17286aaa99..0000000000 --- a/patches/server/0999-Optimize-Collision-to-not-load-chunks.patch +++ /dev/null @@ -1,109 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Thu, 2 Apr 2020 02:37:57 -0400 -Subject: [PATCH] Optimize Collision to not load chunks - -The collision code takes an AABB and generates a cuboid of checks rather -than a cylinder, so at high velocity this can generate a lot of chunk checks. - -Treat an unloaded chunk as a collision for entities, and also for players if -the "prevent moving into unloaded chunks" setting is enabled. - -If that serting is not enabled, collisions will be ignored for players, since -movement will load only the chunk the player enters anyways and avoids loading -massive amounts of surrounding chunks due to large AABB lookups. - -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 8f1a2de2a5542929f8a2fdd4d38de141eda985b2..29b35b8c0a6001d626e327a82eaff26d068660db 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -934,6 +934,7 @@ public abstract class PlayerList { - entityplayer1.setShiftKeyDown(false); - entityplayer1.forceSetPositionRotation(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch()); - -+ worldserver1.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.POST_TELEPORT, new net.minecraft.world.level.ChunkPos(location.getBlockX() >> 4, location.getBlockZ() >> 4), 1, entityplayer.getId()); // Paper - while (avoidSuffocation && !worldserver1.noCollision((Entity) entityplayer1) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) { - // CraftBukkit end - entityplayer1.setPos(entityplayer1.getX(), entityplayer1.getY() + 1.0D, entityplayer1.getZ()); -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index a13edd1165a5ba4dd3f5c323e454926e7fe75c07..b57644317b5085d74d11ac6ba858c3747d703a47 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -245,6 +245,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - public org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason; // Paper - Entity#getEntitySpawnReason - - public com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData; // Paper -+ public boolean collisionLoadChunks = false; // Paper - private CraftEntity bukkitEntity; - - public @org.jetbrains.annotations.Nullable net.minecraft.server.level.ChunkMap.TrackedEntity tracker; // Paper -diff --git a/src/main/java/net/minecraft/world/level/BlockCollisions.java b/src/main/java/net/minecraft/world/level/BlockCollisions.java -index 1c10835b59aaefa3a65ff64f784620bdc54ddcdc..cd89623a44f02d7db77f0d0f87545cf80841f403 100644 ---- a/src/main/java/net/minecraft/world/level/BlockCollisions.java -+++ b/src/main/java/net/minecraft/world/level/BlockCollisions.java -@@ -66,18 +66,37 @@ public class BlockCollisions extends AbstractIterator { - @Override - protected T computeNext() { - while (this.cursor.advance()) { -- int i = this.cursor.nextX(); -- int j = this.cursor.nextY(); -- int k = this.cursor.nextZ(); -+ int i = this.cursor.nextX(); final int x = i; // Paper - OBFHELPER -+ int j = this.cursor.nextY(); final int y = j; // Paper - OBFHELPER -+ int k = this.cursor.nextZ(); final int z = k; // Paper - OBFHELPER - int l = this.cursor.getNextType(); - if (l != 3) { -- BlockGetter blockGetter = this.getChunk(i, k); -- if (blockGetter != null) { -- this.pos.set(i, j, k); -- BlockState blockState = blockGetter.getBlockState(this.pos); -- if ((!this.onlySuffocatingBlocks || blockState.isSuffocating(blockGetter, this.pos)) -- && (l != 1 || blockState.hasLargeCollisionShape()) -- && (l != 2 || blockState.is(Blocks.MOVING_PISTON))) { -+ // Paper start - ensure we don't load chunks -+ // BlockGetter blockGetter = this.getChunk(i, k); -+ if (true) { -+ final @Nullable Entity source = this.context instanceof net.minecraft.world.phys.shapes.EntityCollisionContext entityContext ? entityContext.getEntity() : null; -+ boolean far = source != null && io.papermc.paper.util.MCUtil.distanceSq(source.getX(), y, source.getZ(), x, y, z) > 14; -+ this.pos.set(x, y, z); -+ BlockState blockState; -+ if (this.collisionGetter instanceof net.minecraft.server.level.WorldGenRegion) { -+ BlockGetter blockGetter = this.getChunk(x, z); -+ if (blockGetter == null) { -+ continue; -+ } -+ blockState = blockGetter.getBlockState(this.pos); -+ } else if ((!far && source instanceof net.minecraft.server.level.ServerPlayer) || (source != null && source.collisionLoadChunks)) { -+ blockState = this.collisionGetter.getBlockState(this.pos); -+ } else { -+ blockState = this.collisionGetter.getBlockStateIfLoaded(this.pos); -+ } -+ if (blockState == null) { -+ if (!(source instanceof net.minecraft.server.level.ServerPlayer) || source.level().paperConfig().chunks.preventMovingIntoUnloadedChunks) { -+ return this.resultProvider.apply(new BlockPos.MutableBlockPos(x, y, z), Shapes.create(far ? source.getBoundingBox() : new AABB(new BlockPos(x, y, z)))); -+ } -+ continue; -+ } -+ if (/*(!this.onlySuffocatingBlocks || blockState.isSuffocating(blockGetter, this.pos)) &&*/ (l != 1 || blockState.hasLargeCollisionShape()) && (l != 2 || blockState.is(Blocks.MOVING_PISTON))) { // Paper - onlySuffocatingBlocks is only true on the client, so we don't care about it here -+ // Paper end - VoxelShape voxelShape = blockState.getCollisionShape(this.collisionGetter, this.pos, this.context); - if (voxelShape == Shapes.block()) { - if (this.box.intersects((double)i, (double)j, (double)k, (double)i + 1.0, (double)j + 1.0, (double)k + 1.0)) { -diff --git a/src/main/java/net/minecraft/world/level/CollisionGetter.java b/src/main/java/net/minecraft/world/level/CollisionGetter.java -index e57cb7fe53e915d24246e44c7f49971f5b2ab2cf..1ad0c976c6e2d6d31397dff850a9de7c16d16fba 100644 ---- a/src/main/java/net/minecraft/world/level/CollisionGetter.java -+++ b/src/main/java/net/minecraft/world/level/CollisionGetter.java -@@ -44,11 +44,13 @@ public interface CollisionGetter extends BlockGetter { - } - - default boolean noCollision(@Nullable Entity entity, AABB box) { -+ try { if (entity != null) entity.collisionLoadChunks = true; // Paper - for (VoxelShape voxelShape : this.getBlockCollisions(entity, box)) { - if (!voxelShape.isEmpty()) { - return false; - } - } -+ } finally { if (entity != null) entity.collisionLoadChunks = false; } // Paper - - if (!this.getEntityCollisions(entity, box).isEmpty()) { - return false; diff --git a/patches/server/0999-Optimize-GoalSelector-Goal.Flag-Set-operations.patch b/patches/server/0999-Optimize-GoalSelector-Goal.Flag-Set-operations.patch new file mode 100644 index 0000000000..1f46712639 --- /dev/null +++ b/patches/server/0999-Optimize-GoalSelector-Goal.Flag-Set-operations.patch @@ -0,0 +1,160 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Mon, 6 Apr 2020 17:53:29 -0700 +Subject: [PATCH] Optimize GoalSelector Goal.Flag Set operations + +Optimise the stream.anyMatch statement to move to a bitset +where we can replace the call with a single bitwise operation. + +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/Goal.java b/src/main/java/net/minecraft/world/entity/ai/goal/Goal.java +index 16f9a98b8a939e5ca7e2dc04f87134a7ed66736b..dd423302b1baa64ef86ded87a29659342e28c142 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/Goal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/Goal.java +@@ -4,7 +4,16 @@ import java.util.EnumSet; + import net.minecraft.util.Mth; + + public abstract class Goal { +- private final EnumSet flags = EnumSet.noneOf(Goal.Flag.class); ++ private final EnumSet flags = EnumSet.noneOf(Goal.Flag.class); // Paper unused, but dummy to prevent plugins from crashing as hard. Theyll need to support paper in a special case if this is super important, but really doesn't seem like it would be. ++ private final com.destroystokyo.paper.util.set.OptimizedSmallEnumSet goalTypes = new com.destroystokyo.paper.util.set.OptimizedSmallEnumSet<>(Goal.Flag.class); // Paper - remove streams from pathfindergoalselector ++ ++ // Paper start - remove streams from pathfindergoalselector; make sure types are not empty ++ public Goal() { ++ if (this.goalTypes.size() == 0) { ++ this.goalTypes.add(Flag.UNKNOWN_BEHAVIOR); ++ } ++ } ++ // Paper end - remove streams from pathfindergoalselector + + public abstract boolean canUse(); + +@@ -30,8 +39,13 @@ public abstract class Goal { + } + + public void setFlags(EnumSet controls) { +- this.flags.clear(); +- this.flags.addAll(controls); ++ // Paper start - remove streams from pathfindergoalselector ++ this.goalTypes.clear(); ++ this.goalTypes.addAll(controls); ++ if (this.goalTypes.size() == 0) { ++ this.goalTypes.add(Flag.UNKNOWN_BEHAVIOR); ++ } ++ // Paper end - remove streams from pathfindergoalselector + } + + @Override +@@ -39,8 +53,10 @@ public abstract class Goal { + return this.getClass().getSimpleName(); + } + +- public EnumSet getFlags() { +- return this.flags; ++ // Paper start - remove streams from pathfindergoalselector ++ public com.destroystokyo.paper.util.set.OptimizedSmallEnumSet getFlags() { ++ return this.goalTypes; ++ // Paper end - remove streams from pathfindergoalselector + } + + protected int adjustedTickDelay(int ticks) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java +index 074ef807258139f818e30494126585262c2f33c0..74d4f653d5c7f1923c59019effd78337402f7025 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java +@@ -25,7 +25,8 @@ public class GoalSelector { + private final Map lockedFlags = new EnumMap<>(Goal.Flag.class); + private final Set availableGoals = new ObjectLinkedOpenHashSet<>(); + private final Supplier profiler; +- private final EnumSet disabledFlags = EnumSet.noneOf(Goal.Flag.class); ++ private static final Goal.Flag[] GOAL_FLAG_VALUES = Goal.Flag.values(); // Paper - remove streams from pathfindergoalselector ++ private final com.destroystokyo.paper.util.set.OptimizedSmallEnumSet goalTypes = new com.destroystokyo.paper.util.set.OptimizedSmallEnumSet<>(Goal.Flag.class); // Paper - remove streams from pathfindergoalselector + private int curRate; + + public GoalSelector(Supplier profiler) { +@@ -65,18 +66,18 @@ public class GoalSelector { + this.availableGoals.removeIf(wrappedGoalx -> wrappedGoalx.getGoal() == goal); + } + +- private static boolean goalContainsAnyFlags(WrappedGoal goal, EnumSet controls) { +- for (Goal.Flag flag : goal.getFlags()) { +- if (controls.contains(flag)) { +- return true; +- } +- } +- +- return false; ++ // Paper start ++ private static boolean goalContainsAnyFlags(WrappedGoal goal, com.destroystokyo.paper.util.set.OptimizedSmallEnumSet controls) { ++ return goal.getFlags().hasCommonElements(controls); + } + + private static boolean goalCanBeReplacedForAllFlags(WrappedGoal goal, Map goalsByControl) { +- for (Goal.Flag flag : goal.getFlags()) { ++ long flagIterator = goal.getFlags().getBackingSet(); ++ int wrappedGoalSize = goal.getFlags().size(); ++ for (int i = 0; i < wrappedGoalSize; ++i) { ++ final Goal.Flag flag = GOAL_FLAG_VALUES[Long.numberOfTrailingZeros(flagIterator)]; ++ flagIterator ^= io.papermc.paper.util.IntegerUtil.getTrailingBit(flagIterator); ++ // Paper end + if (!goalsByControl.getOrDefault(flag, NO_GOAL).canBeReplacedBy(goal)) { + return false; + } +@@ -90,7 +91,7 @@ public class GoalSelector { + profilerFiller.push("goalCleanup"); + + for (WrappedGoal wrappedGoal : this.availableGoals) { +- if (wrappedGoal.isRunning() && (goalContainsAnyFlags(wrappedGoal, this.disabledFlags) || !wrappedGoal.canContinueToUse())) { ++ if (wrappedGoal.isRunning() && (goalContainsAnyFlags(wrappedGoal, this.goalTypes) || !wrappedGoal.canContinueToUse())) { // Paper - Perf: optimize goal types by removing streams + wrappedGoal.stop(); + } + } +@@ -100,11 +101,14 @@ public class GoalSelector { + profilerFiller.push("goalUpdate"); + + for (WrappedGoal wrappedGoal2 : this.availableGoals) { +- if (!wrappedGoal2.isRunning() +- && !goalContainsAnyFlags(wrappedGoal2, this.disabledFlags) +- && goalCanBeReplacedForAllFlags(wrappedGoal2, this.lockedFlags) +- && wrappedGoal2.canUse()) { +- for (Goal.Flag flag : wrappedGoal2.getFlags()) { ++ // Paper start ++ if (!wrappedGoal2.isRunning() && !goalContainsAnyFlags(wrappedGoal2, this.goalTypes) && goalCanBeReplacedForAllFlags(wrappedGoal2, this.lockedFlags) && wrappedGoal2.canUse()) { ++ long flagIterator = wrappedGoal2.getFlags().getBackingSet(); ++ int wrappedGoalSize = wrappedGoal2.getFlags().size(); ++ for (int i = 0; i < wrappedGoalSize; ++i) { ++ final Goal.Flag flag = GOAL_FLAG_VALUES[Long.numberOfTrailingZeros(flagIterator)]; ++ flagIterator ^= io.papermc.paper.util.IntegerUtil.getTrailingBit(flagIterator); ++ // Paper end + WrappedGoal wrappedGoal3 = this.lockedFlags.getOrDefault(flag, NO_GOAL); + wrappedGoal3.stop(); + this.lockedFlags.put(flag, wrappedGoal2); +@@ -136,11 +140,11 @@ public class GoalSelector { + } + + public void disableControlFlag(Goal.Flag control) { +- this.disabledFlags.add(control); ++ this.goalTypes.add(control); // Paper - remove streams from pathfindergoalselector + } + + public void enableControlFlag(Goal.Flag control) { +- this.disabledFlags.remove(control); ++ this.goalTypes.remove(control); // Paper - remove streams from pathfindergoalselector + } + + public void setControlFlag(Goal.Flag control, boolean enabled) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/WrappedGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/WrappedGoal.java +index b02d3deb550830245c8945ef17d3073ea930fdda..65ccdbaa5230c02d44a5959bca0f6fc30237a6fd 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/goal/WrappedGoal.java ++++ b/src/main/java/net/minecraft/world/entity/ai/goal/WrappedGoal.java +@@ -69,8 +69,10 @@ public class WrappedGoal extends Goal { + } + + @Override +- public EnumSet getFlags() { ++ // Paper start - remove streams from pathfindergoalselector ++ public com.destroystokyo.paper.util.set.OptimizedSmallEnumSet getFlags() { + return this.goal.getFlags(); ++ // Paper end - remove streams from pathfindergoalselector + } + + public boolean isRunning() { diff --git a/patches/server/1000-Entity-load-save-limit-per-chunk.patch b/patches/server/1000-Entity-load-save-limit-per-chunk.patch new file mode 100644 index 0000000000..1dbf729bda --- /dev/null +++ b/patches/server/1000-Entity-load-save-limit-per-chunk.patch @@ -0,0 +1,58 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> +Date: Wed, 18 Nov 2020 20:52:25 -0800 +Subject: [PATCH] Entity load/save limit per chunk + +Adds a config option to limit the number of entities saved and loaded +to a chunk. The default values of -1 disable the limit. Although +defaults are only included for certain entites, this allows setting +limits for any entity type. + +diff --git a/src/main/java/net/minecraft/world/entity/EntityType.java b/src/main/java/net/minecraft/world/entity/EntityType.java +index 56b51096ca4147363a843accf6ef2510f05e8f1a..a46bf73c608641bf1f00fd55242de71a0f2ee06e 100644 +--- a/src/main/java/net/minecraft/world/entity/EntityType.java ++++ b/src/main/java/net/minecraft/world/entity/EntityType.java +@@ -649,9 +649,20 @@ public class EntityType implements FeatureElement, EntityTypeT + final Spliterator spliterator = entityNbtList.spliterator(); + + return StreamSupport.stream(new Spliterator() { ++ final java.util.Map, Integer> loadedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk + public boolean tryAdvance(Consumer consumer) { + return spliterator.tryAdvance((nbtbase) -> { + EntityType.loadEntityRecursive((CompoundTag) nbtbase, world, (entity) -> { ++ // Paper start - Entity load/save limit per chunk ++ final EntityType entityType = entity.getType(); ++ final int saveLimit = world.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1); ++ if (saveLimit > -1) { ++ if (this.loadedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) { ++ return null; ++ } ++ this.loadedEntityCounts.merge(entityType, 1, Integer::sum); ++ } ++ // Paper end - Entity load/save limit per chunk + consumer.accept(entity); + return entity; + }); +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java +index 9fdf8f857a5f9b231c6d0633eaba498244214f74..bee39dee1b96023c907407877aedf3aafaf5e1b8 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java +@@ -105,7 +105,18 @@ public class EntityStorage implements EntityPersistentStorage { + } + + ListTag listTag = new ListTag(); ++ final java.util.Map, Integer> savedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk + entities.forEach((entity) -> { // diff here: use entities parameter ++ // Paper start - Entity load/save limit per chunk ++ final EntityType entityType = entity.getType(); ++ final int saveLimit = level.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1); ++ if (saveLimit > -1) { ++ if (savedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) { ++ return; ++ } ++ savedEntityCounts.merge(entityType, 1, Integer::sum); ++ } ++ // Paper end - Entity load/save limit per chunk + CompoundTag compoundTag = new CompoundTag(); + if (entity.save(compoundTag)) { + listTag.add(compoundTag); diff --git a/patches/server/1000-Optimize-GoalSelector-Goal.Flag-Set-operations.patch b/patches/server/1000-Optimize-GoalSelector-Goal.Flag-Set-operations.patch deleted file mode 100644 index 1f46712639..0000000000 --- a/patches/server/1000-Optimize-GoalSelector-Goal.Flag-Set-operations.patch +++ /dev/null @@ -1,160 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Mon, 6 Apr 2020 17:53:29 -0700 -Subject: [PATCH] Optimize GoalSelector Goal.Flag Set operations - -Optimise the stream.anyMatch statement to move to a bitset -where we can replace the call with a single bitwise operation. - -diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/Goal.java b/src/main/java/net/minecraft/world/entity/ai/goal/Goal.java -index 16f9a98b8a939e5ca7e2dc04f87134a7ed66736b..dd423302b1baa64ef86ded87a29659342e28c142 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/goal/Goal.java -+++ b/src/main/java/net/minecraft/world/entity/ai/goal/Goal.java -@@ -4,7 +4,16 @@ import java.util.EnumSet; - import net.minecraft.util.Mth; - - public abstract class Goal { -- private final EnumSet flags = EnumSet.noneOf(Goal.Flag.class); -+ private final EnumSet flags = EnumSet.noneOf(Goal.Flag.class); // Paper unused, but dummy to prevent plugins from crashing as hard. Theyll need to support paper in a special case if this is super important, but really doesn't seem like it would be. -+ private final com.destroystokyo.paper.util.set.OptimizedSmallEnumSet goalTypes = new com.destroystokyo.paper.util.set.OptimizedSmallEnumSet<>(Goal.Flag.class); // Paper - remove streams from pathfindergoalselector -+ -+ // Paper start - remove streams from pathfindergoalselector; make sure types are not empty -+ public Goal() { -+ if (this.goalTypes.size() == 0) { -+ this.goalTypes.add(Flag.UNKNOWN_BEHAVIOR); -+ } -+ } -+ // Paper end - remove streams from pathfindergoalselector - - public abstract boolean canUse(); - -@@ -30,8 +39,13 @@ public abstract class Goal { - } - - public void setFlags(EnumSet controls) { -- this.flags.clear(); -- this.flags.addAll(controls); -+ // Paper start - remove streams from pathfindergoalselector -+ this.goalTypes.clear(); -+ this.goalTypes.addAll(controls); -+ if (this.goalTypes.size() == 0) { -+ this.goalTypes.add(Flag.UNKNOWN_BEHAVIOR); -+ } -+ // Paper end - remove streams from pathfindergoalselector - } - - @Override -@@ -39,8 +53,10 @@ public abstract class Goal { - return this.getClass().getSimpleName(); - } - -- public EnumSet getFlags() { -- return this.flags; -+ // Paper start - remove streams from pathfindergoalselector -+ public com.destroystokyo.paper.util.set.OptimizedSmallEnumSet getFlags() { -+ return this.goalTypes; -+ // Paper end - remove streams from pathfindergoalselector - } - - protected int adjustedTickDelay(int ticks) { -diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java -index 074ef807258139f818e30494126585262c2f33c0..74d4f653d5c7f1923c59019effd78337402f7025 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java -+++ b/src/main/java/net/minecraft/world/entity/ai/goal/GoalSelector.java -@@ -25,7 +25,8 @@ public class GoalSelector { - private final Map lockedFlags = new EnumMap<>(Goal.Flag.class); - private final Set availableGoals = new ObjectLinkedOpenHashSet<>(); - private final Supplier profiler; -- private final EnumSet disabledFlags = EnumSet.noneOf(Goal.Flag.class); -+ private static final Goal.Flag[] GOAL_FLAG_VALUES = Goal.Flag.values(); // Paper - remove streams from pathfindergoalselector -+ private final com.destroystokyo.paper.util.set.OptimizedSmallEnumSet goalTypes = new com.destroystokyo.paper.util.set.OptimizedSmallEnumSet<>(Goal.Flag.class); // Paper - remove streams from pathfindergoalselector - private int curRate; - - public GoalSelector(Supplier profiler) { -@@ -65,18 +66,18 @@ public class GoalSelector { - this.availableGoals.removeIf(wrappedGoalx -> wrappedGoalx.getGoal() == goal); - } - -- private static boolean goalContainsAnyFlags(WrappedGoal goal, EnumSet controls) { -- for (Goal.Flag flag : goal.getFlags()) { -- if (controls.contains(flag)) { -- return true; -- } -- } -- -- return false; -+ // Paper start -+ private static boolean goalContainsAnyFlags(WrappedGoal goal, com.destroystokyo.paper.util.set.OptimizedSmallEnumSet controls) { -+ return goal.getFlags().hasCommonElements(controls); - } - - private static boolean goalCanBeReplacedForAllFlags(WrappedGoal goal, Map goalsByControl) { -- for (Goal.Flag flag : goal.getFlags()) { -+ long flagIterator = goal.getFlags().getBackingSet(); -+ int wrappedGoalSize = goal.getFlags().size(); -+ for (int i = 0; i < wrappedGoalSize; ++i) { -+ final Goal.Flag flag = GOAL_FLAG_VALUES[Long.numberOfTrailingZeros(flagIterator)]; -+ flagIterator ^= io.papermc.paper.util.IntegerUtil.getTrailingBit(flagIterator); -+ // Paper end - if (!goalsByControl.getOrDefault(flag, NO_GOAL).canBeReplacedBy(goal)) { - return false; - } -@@ -90,7 +91,7 @@ public class GoalSelector { - profilerFiller.push("goalCleanup"); - - for (WrappedGoal wrappedGoal : this.availableGoals) { -- if (wrappedGoal.isRunning() && (goalContainsAnyFlags(wrappedGoal, this.disabledFlags) || !wrappedGoal.canContinueToUse())) { -+ if (wrappedGoal.isRunning() && (goalContainsAnyFlags(wrappedGoal, this.goalTypes) || !wrappedGoal.canContinueToUse())) { // Paper - Perf: optimize goal types by removing streams - wrappedGoal.stop(); - } - } -@@ -100,11 +101,14 @@ public class GoalSelector { - profilerFiller.push("goalUpdate"); - - for (WrappedGoal wrappedGoal2 : this.availableGoals) { -- if (!wrappedGoal2.isRunning() -- && !goalContainsAnyFlags(wrappedGoal2, this.disabledFlags) -- && goalCanBeReplacedForAllFlags(wrappedGoal2, this.lockedFlags) -- && wrappedGoal2.canUse()) { -- for (Goal.Flag flag : wrappedGoal2.getFlags()) { -+ // Paper start -+ if (!wrappedGoal2.isRunning() && !goalContainsAnyFlags(wrappedGoal2, this.goalTypes) && goalCanBeReplacedForAllFlags(wrappedGoal2, this.lockedFlags) && wrappedGoal2.canUse()) { -+ long flagIterator = wrappedGoal2.getFlags().getBackingSet(); -+ int wrappedGoalSize = wrappedGoal2.getFlags().size(); -+ for (int i = 0; i < wrappedGoalSize; ++i) { -+ final Goal.Flag flag = GOAL_FLAG_VALUES[Long.numberOfTrailingZeros(flagIterator)]; -+ flagIterator ^= io.papermc.paper.util.IntegerUtil.getTrailingBit(flagIterator); -+ // Paper end - WrappedGoal wrappedGoal3 = this.lockedFlags.getOrDefault(flag, NO_GOAL); - wrappedGoal3.stop(); - this.lockedFlags.put(flag, wrappedGoal2); -@@ -136,11 +140,11 @@ public class GoalSelector { - } - - public void disableControlFlag(Goal.Flag control) { -- this.disabledFlags.add(control); -+ this.goalTypes.add(control); // Paper - remove streams from pathfindergoalselector - } - - public void enableControlFlag(Goal.Flag control) { -- this.disabledFlags.remove(control); -+ this.goalTypes.remove(control); // Paper - remove streams from pathfindergoalselector - } - - public void setControlFlag(Goal.Flag control, boolean enabled) { -diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/WrappedGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/WrappedGoal.java -index b02d3deb550830245c8945ef17d3073ea930fdda..65ccdbaa5230c02d44a5959bca0f6fc30237a6fd 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/goal/WrappedGoal.java -+++ b/src/main/java/net/minecraft/world/entity/ai/goal/WrappedGoal.java -@@ -69,8 +69,10 @@ public class WrappedGoal extends Goal { - } - - @Override -- public EnumSet getFlags() { -+ // Paper start - remove streams from pathfindergoalselector -+ public com.destroystokyo.paper.util.set.OptimizedSmallEnumSet getFlags() { - return this.goal.getFlags(); -+ // Paper end - remove streams from pathfindergoalselector - } - - public boolean isRunning() { diff --git a/patches/server/1001-Entity-load-save-limit-per-chunk.patch b/patches/server/1001-Entity-load-save-limit-per-chunk.patch deleted file mode 100644 index 1dbf729bda..0000000000 --- a/patches/server/1001-Entity-load-save-limit-per-chunk.patch +++ /dev/null @@ -1,58 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> -Date: Wed, 18 Nov 2020 20:52:25 -0800 -Subject: [PATCH] Entity load/save limit per chunk - -Adds a config option to limit the number of entities saved and loaded -to a chunk. The default values of -1 disable the limit. Although -defaults are only included for certain entites, this allows setting -limits for any entity type. - -diff --git a/src/main/java/net/minecraft/world/entity/EntityType.java b/src/main/java/net/minecraft/world/entity/EntityType.java -index 56b51096ca4147363a843accf6ef2510f05e8f1a..a46bf73c608641bf1f00fd55242de71a0f2ee06e 100644 ---- a/src/main/java/net/minecraft/world/entity/EntityType.java -+++ b/src/main/java/net/minecraft/world/entity/EntityType.java -@@ -649,9 +649,20 @@ public class EntityType implements FeatureElement, EntityTypeT - final Spliterator spliterator = entityNbtList.spliterator(); - - return StreamSupport.stream(new Spliterator() { -+ final java.util.Map, Integer> loadedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk - public boolean tryAdvance(Consumer consumer) { - return spliterator.tryAdvance((nbtbase) -> { - EntityType.loadEntityRecursive((CompoundTag) nbtbase, world, (entity) -> { -+ // Paper start - Entity load/save limit per chunk -+ final EntityType entityType = entity.getType(); -+ final int saveLimit = world.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1); -+ if (saveLimit > -1) { -+ if (this.loadedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) { -+ return null; -+ } -+ this.loadedEntityCounts.merge(entityType, 1, Integer::sum); -+ } -+ // Paper end - Entity load/save limit per chunk - consumer.accept(entity); - return entity; - }); -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java -index 9fdf8f857a5f9b231c6d0633eaba498244214f74..bee39dee1b96023c907407877aedf3aafaf5e1b8 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/EntityStorage.java -@@ -105,7 +105,18 @@ public class EntityStorage implements EntityPersistentStorage { - } - - ListTag listTag = new ListTag(); -+ final java.util.Map, Integer> savedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk - entities.forEach((entity) -> { // diff here: use entities parameter -+ // Paper start - Entity load/save limit per chunk -+ final EntityType entityType = entity.getType(); -+ final int saveLimit = level.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1); -+ if (saveLimit > -1) { -+ if (savedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) { -+ return; -+ } -+ savedEntityCounts.merge(entityType, 1, Integer::sum); -+ } -+ // Paper end - Entity load/save limit per chunk - CompoundTag compoundTag = new CompoundTag(); - if (entity.save(compoundTag)) { - listTag.add(compoundTag); diff --git a/patches/server/1001-Improved-Watchdog-Support.patch b/patches/server/1001-Improved-Watchdog-Support.patch new file mode 100644 index 0000000000..73e3c133cb --- /dev/null +++ b/patches/server/1001-Improved-Watchdog-Support.patch @@ -0,0 +1,544 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Sun, 12 Apr 2020 15:50:48 -0400 +Subject: [PATCH] Improved Watchdog Support + +Forced Watchdog Crash support and Improve Async Shutdown + +If the request to shut down the server is received while we are in +a watchdog hang, immediately treat it as a crash and begin the shutdown +process. Shutdown process is now improved to also shutdown cleanly when +not using restart scripts either. + +If a server is deadlocked, a server owner can send SIGUP (or any other signal +the JVM understands to shut down as it currently does) and the watchdog +will no longer need to wait until the full timeout, allowing you to trigger +a close process and try to shut the server down gracefully, saving player and +world data. + +Previously there was no way to trigger this outside of waiting for a full watchdog +timeout, which may be set to a really long time... + +Additionally, fix everything to do with shutting the server down asynchronously. + +Previously, nearly everything about the process was fragile and unsafe. Main might +not have actually been frozen, and might still be manipulating state. + +Or, some reuest might ask main to do something in the shutdown but main is dead. + +Or worse, other things might start closing down items such as the Console or Thread Pool +before we are fully shutdown. + +This change tries to resolve all of these issues by moving everything into the stop +method and guaranteeing only one thread is stopping the server. + +We then issue Thread Death to the main thread of another thread initiates the stop process. +We have to ensure Thread Death propagates correctly though to stop main completely. + +This is to ensure that if main isn't truely stuck, it's not manipulating state we are trying to save. + +This also moves all plugins who register "delayed init" tasks to occur just before "Done" so they +are properly accounted for and wont trip watchdog on init. + +diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java +index 6aaed8e8bf8c721fc834da5c76ac72a4c3e92458..4b002e8b75d117b726b0de274a76d3596fce015b 100644 +--- a/src/main/java/com/destroystokyo/paper/Metrics.java ++++ b/src/main/java/com/destroystokyo/paper/Metrics.java +@@ -92,7 +92,12 @@ public class Metrics { + * Starts the Scheduler which submits our data every 30 minutes. + */ + private void startSubmitting() { +- final Runnable submitTask = this::submitData; ++ final Runnable submitTask = () -> { ++ if (MinecraftServer.getServer().hasStopped()) { ++ return; ++ } ++ submitData(); ++ }; + + // Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution of requests on the + // bStats backend. To circumvent this problem, we introduce some randomness into the initial and second delay. +diff --git a/src/main/java/net/minecraft/CrashReport.java b/src/main/java/net/minecraft/CrashReport.java +index bcb6a3b3cd17ce5db9aaf6bd3ec7a0ec1b44b979..4f3cc14d48690bb183d09bb7a5ba1e23e8a0c08a 100644 +--- a/src/main/java/net/minecraft/CrashReport.java ++++ b/src/main/java/net/minecraft/CrashReport.java +@@ -234,6 +234,7 @@ public class CrashReport { + } + + public static CrashReport forThrowable(Throwable cause, String title) { ++ if (cause instanceof ThreadDeath) com.destroystokyo.paper.util.SneakyThrow.sneaky(cause); // Paper + while (cause instanceof CompletionException && cause.getCause() != null) { + cause = cause.getCause(); + } +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 7f3359745089584448b8f32f0570cf86b73076c7..101c465397176d873f7a7871c84c8fb5a9897b0b 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -303,7 +303,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); + public int autosavePeriod; + public Commands vanillaCommandDispatcher; +- private boolean forceTicks; ++ public boolean forceTicks; // Paper + // CraftBukkit end + // Spigot start + public static final int TPS = 20; +@@ -316,6 +316,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { + AtomicReference atomicreference = new AtomicReference(); + Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system +@@ -933,6 +936,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop {}; ++ } ++ // Paper end + return new TickTask(this.tickCount, runnable); + } + +@@ -2178,7 +2234,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop implements Profiler + try { + task.run(); + } catch (Exception var3) { ++ if (var3.getCause() instanceof ThreadDeath) throw var3; // Paper + LOGGER.error(LogUtils.FATAL_MARKER, "Error executing task on {}", this.name(), var3); + } + } +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index ff8d947c5a498e097a4c480a5dc9af71b0be5ee9..ee49f59ba8ce5708fc5e244eba7b1f910cf69263 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -926,6 +926,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + try { + tickConsumer.accept(entity); + } catch (Throwable throwable) { ++ if (throwable instanceof ThreadDeath) throw throwable; // Paper + // Paper start - Prevent block entity and entity crashes + final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level().getWorld().getName(), entity.getX(), entity.getY(), entity.getZ()); + MinecraftServer.LOGGER.error(msg, throwable); +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +index 424c4613e202c6ba50fa0de65d2526d400a8e299..2a8609e33716949ff1877b6d10f64a9d7a7c81e9 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +@@ -1179,6 +1179,7 @@ public class LevelChunk extends ChunkAccess { + + gameprofilerfiller.pop(); + } catch (Throwable throwable) { ++ if (throwable instanceof ThreadDeath) throw throwable; // Paper + // Paper start - Prevent block entity and entity crashes + final String msg = String.format("BlockEntity threw exception at %s:%s,%s,%s", LevelChunk.this.getLevel().getWorld().getName(), this.getPos().getX(), this.getPos().getY(), this.getPos().getZ()); + net.minecraft.server.MinecraftServer.LOGGER.error(msg, throwable); +diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java +index 3b8a2de79c79302626f43f320b15bae52416cc37..c097f5d5fbd51cbbc01bbd54101905c59b3f3a4c 100644 +--- a/src/main/java/org/bukkit/craftbukkit/Main.java ++++ b/src/main/java/org/bukkit/craftbukkit/Main.java +@@ -187,6 +187,36 @@ public class Main { + + OptionSet options = null; + ++ // Paper start - preload logger classes to avoid plugins mixing versions ++ tryPreloadClass("org.apache.logging.log4j.core.Core"); ++ tryPreloadClass("org.apache.logging.log4j.core.appender.AsyncAppender"); ++ tryPreloadClass("org.apache.logging.log4j.core.Appender"); ++ tryPreloadClass("org.apache.logging.log4j.core.ContextDataInjector"); ++ tryPreloadClass("org.apache.logging.log4j.core.Filter"); ++ tryPreloadClass("org.apache.logging.log4j.core.ErrorHandler"); ++ tryPreloadClass("org.apache.logging.log4j.core.LogEvent"); ++ tryPreloadClass("org.apache.logging.log4j.core.Logger"); ++ tryPreloadClass("org.apache.logging.log4j.core.LoggerContext"); ++ tryPreloadClass("org.apache.logging.log4j.core.LogEventListener"); ++ tryPreloadClass("org.apache.logging.log4j.core.AbstractLogEvent"); ++ tryPreloadClass("org.apache.logging.log4j.message.AsynchronouslyFormattable"); ++ tryPreloadClass("org.apache.logging.log4j.message.FormattedMessage"); ++ tryPreloadClass("org.apache.logging.log4j.message.ParameterizedMessage"); ++ tryPreloadClass("org.apache.logging.log4j.message.Message"); ++ tryPreloadClass("org.apache.logging.log4j.message.MessageFactory"); ++ tryPreloadClass("org.apache.logging.log4j.message.TimestampMessage"); ++ tryPreloadClass("org.apache.logging.log4j.message.SimpleMessage"); ++ tryPreloadClass("org.apache.logging.log4j.core.async.AsyncLogger"); ++ tryPreloadClass("org.apache.logging.log4j.core.async.AsyncLoggerContext"); ++ tryPreloadClass("org.apache.logging.log4j.core.async.AsyncQueueFullPolicy"); ++ tryPreloadClass("org.apache.logging.log4j.core.async.AsyncLoggerDisruptor"); ++ tryPreloadClass("org.apache.logging.log4j.core.async.RingBufferLogEvent"); ++ tryPreloadClass("org.apache.logging.log4j.core.async.DisruptorUtil"); ++ tryPreloadClass("org.apache.logging.log4j.core.async.RingBufferLogEventHandler"); ++ tryPreloadClass("org.apache.logging.log4j.core.impl.ThrowableProxy"); ++ tryPreloadClass("org.apache.logging.log4j.core.impl.ExtendedClassInfo"); ++ tryPreloadClass("org.apache.logging.log4j.core.impl.ExtendedStackTraceElement"); ++ // Paper end + try { + options = parser.parse(args); + } catch (joptsimple.OptionException ex) { +@@ -298,8 +328,65 @@ public class Main { + } catch (Throwable t) { + t.printStackTrace(); + } ++ // Paper start ++ // load some required classes to avoid errors during shutdown if jar is replaced ++ // also to guarantee our version loads over plugins ++ tryPreloadClass("com.destroystokyo.paper.util.SneakyThrow"); ++ tryPreloadClass("com.google.common.collect.Iterators$PeekingImpl"); ++ tryPreloadClass("com.google.common.collect.MapMakerInternalMap$Values"); ++ tryPreloadClass("com.google.common.collect.MapMakerInternalMap$ValueIterator"); ++ tryPreloadClass("com.google.common.collect.MapMakerInternalMap$WriteThroughEntry"); ++ tryPreloadClass("com.google.common.collect.Iterables"); ++ for (int i = 1; i <= 15; i++) { ++ tryPreloadClass("com.google.common.collect.Iterables$" + i, false); ++ } ++ tryPreloadClass("org.apache.commons.lang3.mutable.MutableBoolean"); ++ tryPreloadClass("org.apache.commons.lang3.mutable.MutableInt"); ++ tryPreloadClass("org.jline.terminal.impl.MouseSupport"); ++ tryPreloadClass("org.jline.terminal.impl.MouseSupport$1"); ++ tryPreloadClass("org.jline.terminal.Terminal$MouseTracking"); ++ tryPreloadClass("co.aikar.timings.TimingHistory"); ++ tryPreloadClass("co.aikar.timings.TimingHistory$MinuteReport"); ++ tryPreloadClass("io.netty.channel.AbstractChannelHandlerContext"); ++ tryPreloadClass("io.netty.channel.AbstractChannelHandlerContext$11"); ++ tryPreloadClass("io.netty.channel.AbstractChannelHandlerContext$12"); ++ tryPreloadClass("io.netty.channel.AbstractChannel$AbstractUnsafe$8"); ++ tryPreloadClass("io.netty.util.concurrent.DefaultPromise"); ++ tryPreloadClass("io.netty.util.concurrent.DefaultPromise$1"); ++ tryPreloadClass("io.netty.util.internal.PromiseNotificationUtil"); ++ tryPreloadClass("io.netty.util.internal.SystemPropertyUtil"); ++ tryPreloadClass("org.bukkit.craftbukkit.scheduler.CraftScheduler"); ++ tryPreloadClass("org.bukkit.craftbukkit.scheduler.CraftScheduler$1"); ++ tryPreloadClass("org.bukkit.craftbukkit.scheduler.CraftScheduler$2"); ++ tryPreloadClass("org.bukkit.craftbukkit.scheduler.CraftScheduler$3"); ++ tryPreloadClass("org.bukkit.craftbukkit.scheduler.CraftScheduler$4"); ++ tryPreloadClass("org.slf4j.helpers.MessageFormatter"); ++ tryPreloadClass("org.slf4j.helpers.FormattingTuple"); ++ tryPreloadClass("org.slf4j.helpers.BasicMarker"); ++ tryPreloadClass("org.slf4j.helpers.Util"); ++ tryPreloadClass("com.destroystokyo.paper.event.player.PlayerConnectionCloseEvent"); ++ tryPreloadClass("com.destroystokyo.paper.event.entity.EntityRemoveFromWorldEvent"); ++ // Minecraft, seen during saving ++ tryPreloadClass(net.minecraft.world.level.lighting.LayerLightEventListener.DummyLightLayerEventListener.class.getName()); ++ tryPreloadClass(net.minecraft.world.level.lighting.LayerLightEventListener.class.getName()); ++ tryPreloadClass(net.minecraft.util.ExceptionCollector.class.getName()); ++ tryPreloadClass(io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.PlayerChunkLoaderData.class.getName()); ++ // Paper end ++ } ++ } ++ ++ // Paper start ++ private static void tryPreloadClass(String className) { ++ tryPreloadClass(className, true); ++ } ++ private static void tryPreloadClass(String className, boolean printError) { ++ try { ++ Class.forName(className); ++ } catch (ClassNotFoundException e) { ++ if (printError) System.err.println("An expected class " + className + " was not found for preloading: " + e.getMessage()); + } + } ++ // Paper end + + private static List asList(String... params) { + return Arrays.asList(params); +diff --git a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java +index c6e8441e299f477ddb22c1ce2618710763978f1a..e8e93538dfd71de86515d9405f728db1631e949a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java +@@ -12,11 +12,27 @@ public class ServerShutdownThread extends Thread { + @Override + public void run() { + try { ++ // Paper start - try to shutdown on main ++ server.safeShutdown(false, false); ++ for (int i = 1000; i > 0 && !server.hasStopped(); i -= 100) { ++ Thread.sleep(100); ++ } ++ if (server.hasStopped()) { ++ while (!server.hasFullyShutdown) Thread.sleep(1000); ++ return; ++ } ++ // Looks stalled, close async + org.spigotmc.AsyncCatcher.enabled = false; // Spigot ++ server.forceTicks = true; + this.server.close(); ++ while (!server.hasFullyShutdown) Thread.sleep(1000); ++ } catch (InterruptedException e) { ++ e.printStackTrace(); ++ // Paper end + } finally { ++ org.apache.logging.log4j.LogManager.shutdown(); // Paper + try { +- net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Use TerminalConsoleAppender ++ //net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Move into stop + } catch (Exception e) { + } + } +diff --git a/src/main/java/org/spigotmc/RestartCommand.java b/src/main/java/org/spigotmc/RestartCommand.java +index e3b262add194a126e731c68e68f3139a00cacacb..da7d5efd76c9ef92e9ce22860fec791890a687be 100644 +--- a/src/main/java/org/spigotmc/RestartCommand.java ++++ b/src/main/java/org/spigotmc/RestartCommand.java +@@ -138,7 +138,7 @@ public class RestartCommand extends Command + // Paper end + + // Paper start - copied from above and modified to return if the hook registered +- private static boolean addShutdownHook(String restartScript) ++ public static boolean addShutdownHook(String restartScript) + { + String[] split = restartScript.split( " " ); + if ( split.length > 0 && new File( split[0] ).isFile() ) +diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java +index 9e5d08f57aa448552d100ca892c211d44441ef68..577f29519156f33b49ee319a64a52249630e28d8 100644 +--- a/src/main/java/org/spigotmc/WatchdogThread.java ++++ b/src/main/java/org/spigotmc/WatchdogThread.java +@@ -11,6 +11,7 @@ import org.bukkit.Bukkit; + public final class WatchdogThread extends io.papermc.paper.util.TickThread // Paper - rewrite chunk system + { + ++ public static final boolean DISABLE_WATCHDOG = Boolean.getBoolean("disable.watchdog"); // Paper + private static WatchdogThread instance; + private long timeoutTime; + private boolean restart; +@@ -39,6 +40,7 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa + { + if ( WatchdogThread.instance == null ) + { ++ if (timeoutTime <= 0) timeoutTime = 300; // Paper + WatchdogThread.instance = new WatchdogThread( timeoutTime * 1000L, restart ); + WatchdogThread.instance.start(); + } else +@@ -70,12 +72,13 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa + // Paper start + Logger log = Bukkit.getServer().getLogger(); + long currentTime = WatchdogThread.monotonicMillis(); +- if ( this.lastTick != 0 && this.timeoutTime > 0 && currentTime > this.lastTick + this.earlyWarningEvery && !Boolean.getBoolean("disable.watchdog")) // Paper - Add property to disable ++ MinecraftServer server = MinecraftServer.getServer(); ++ if ( this.lastTick != 0 && this.timeoutTime > 0 && WatchdogThread.hasStarted && (!server.isRunning() || (currentTime > this.lastTick + this.earlyWarningEvery && !DISABLE_WATCHDOG) )) // Paper - add property to disable + { +- boolean isLongTimeout = currentTime > lastTick + timeoutTime; ++ boolean isLongTimeout = currentTime > lastTick + timeoutTime || (!server.isRunning() && !server.hasStopped() && currentTime > lastTick + 1000); + // Don't spam early warning dumps + if ( !isLongTimeout && (earlyWarningEvery <= 0 || !hasStarted || currentTime < lastEarlyWarning + earlyWarningEvery || currentTime < lastTick + earlyWarningDelay)) continue; +- if ( !isLongTimeout && MinecraftServer.getServer().hasStopped()) continue; // Don't spam early watchdog warnings during shutdown, we'll come back to this... ++ if ( !isLongTimeout && server.hasStopped()) continue; // Don't spam early watchdog warnings during shutdown, we'll come back to this... + lastEarlyWarning = currentTime; + if (isLongTimeout) { + // Paper end +@@ -136,9 +139,24 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa + + if ( isLongTimeout ) + { +- if ( this.restart && !MinecraftServer.getServer().hasStopped() ) ++ if ( !server.hasStopped() ) + { +- RestartCommand.restart(); ++ AsyncCatcher.enabled = false; // Disable async catcher incase it interferes with us ++ server.forceTicks = true; ++ if (restart) { ++ RestartCommand.addShutdownHook( SpigotConfig.restartScript ); ++ } ++ // try one last chance to safe shutdown on main incase it 'comes back' ++ server.abnormalExit = true; ++ server.safeShutdown(false, restart); ++ try { ++ Thread.sleep(1000); ++ } catch (InterruptedException e) { ++ e.printStackTrace(); ++ } ++ if (!server.hasStopped()) { ++ server.close(); ++ } + } + break; + } // Paper end +diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml +index 637d64da9938e51a97338b9253b43889585c67bb..d2a75850af9c6ad2aca66a5f994f1b587d73eac4 100644 +--- a/src/main/resources/log4j2.xml ++++ b/src/main/resources/log4j2.xml +@@ -1,5 +1,5 @@ + +- ++ + + + diff --git a/patches/server/1002-Improved-Watchdog-Support.patch b/patches/server/1002-Improved-Watchdog-Support.patch deleted file mode 100644 index 73e3c133cb..0000000000 --- a/patches/server/1002-Improved-Watchdog-Support.patch +++ /dev/null @@ -1,544 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Sun, 12 Apr 2020 15:50:48 -0400 -Subject: [PATCH] Improved Watchdog Support - -Forced Watchdog Crash support and Improve Async Shutdown - -If the request to shut down the server is received while we are in -a watchdog hang, immediately treat it as a crash and begin the shutdown -process. Shutdown process is now improved to also shutdown cleanly when -not using restart scripts either. - -If a server is deadlocked, a server owner can send SIGUP (or any other signal -the JVM understands to shut down as it currently does) and the watchdog -will no longer need to wait until the full timeout, allowing you to trigger -a close process and try to shut the server down gracefully, saving player and -world data. - -Previously there was no way to trigger this outside of waiting for a full watchdog -timeout, which may be set to a really long time... - -Additionally, fix everything to do with shutting the server down asynchronously. - -Previously, nearly everything about the process was fragile and unsafe. Main might -not have actually been frozen, and might still be manipulating state. - -Or, some reuest might ask main to do something in the shutdown but main is dead. - -Or worse, other things might start closing down items such as the Console or Thread Pool -before we are fully shutdown. - -This change tries to resolve all of these issues by moving everything into the stop -method and guaranteeing only one thread is stopping the server. - -We then issue Thread Death to the main thread of another thread initiates the stop process. -We have to ensure Thread Death propagates correctly though to stop main completely. - -This is to ensure that if main isn't truely stuck, it's not manipulating state we are trying to save. - -This also moves all plugins who register "delayed init" tasks to occur just before "Done" so they -are properly accounted for and wont trip watchdog on init. - -diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java -index 6aaed8e8bf8c721fc834da5c76ac72a4c3e92458..4b002e8b75d117b726b0de274a76d3596fce015b 100644 ---- a/src/main/java/com/destroystokyo/paper/Metrics.java -+++ b/src/main/java/com/destroystokyo/paper/Metrics.java -@@ -92,7 +92,12 @@ public class Metrics { - * Starts the Scheduler which submits our data every 30 minutes. - */ - private void startSubmitting() { -- final Runnable submitTask = this::submitData; -+ final Runnable submitTask = () -> { -+ if (MinecraftServer.getServer().hasStopped()) { -+ return; -+ } -+ submitData(); -+ }; - - // Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution of requests on the - // bStats backend. To circumvent this problem, we introduce some randomness into the initial and second delay. -diff --git a/src/main/java/net/minecraft/CrashReport.java b/src/main/java/net/minecraft/CrashReport.java -index bcb6a3b3cd17ce5db9aaf6bd3ec7a0ec1b44b979..4f3cc14d48690bb183d09bb7a5ba1e23e8a0c08a 100644 ---- a/src/main/java/net/minecraft/CrashReport.java -+++ b/src/main/java/net/minecraft/CrashReport.java -@@ -234,6 +234,7 @@ public class CrashReport { - } - - public static CrashReport forThrowable(Throwable cause, String title) { -+ if (cause instanceof ThreadDeath) com.destroystokyo.paper.util.SneakyThrow.sneaky(cause); // Paper - while (cause instanceof CompletionException && cause.getCause() != null) { - cause = cause.getCause(); - } -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 7f3359745089584448b8f32f0570cf86b73076c7..101c465397176d873f7a7871c84c8fb5a9897b0b 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -303,7 +303,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); - public int autosavePeriod; - public Commands vanillaCommandDispatcher; -- private boolean forceTicks; -+ public boolean forceTicks; // Paper - // CraftBukkit end - // Spigot start - public static final int TPS = 20; -@@ -316,6 +316,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { - AtomicReference atomicreference = new AtomicReference(); - Thread thread = new io.papermc.paper.util.TickThread(() -> { // Paper - rewrite chunk system -@@ -933,6 +936,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop {}; -+ } -+ // Paper end - return new TickTask(this.tickCount, runnable); - } - -@@ -2178,7 +2234,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop implements Profiler - try { - task.run(); - } catch (Exception var3) { -+ if (var3.getCause() instanceof ThreadDeath) throw var3; // Paper - LOGGER.error(LogUtils.FATAL_MARKER, "Error executing task on {}", this.name(), var3); - } - } -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index ff8d947c5a498e097a4c480a5dc9af71b0be5ee9..ee49f59ba8ce5708fc5e244eba7b1f910cf69263 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -926,6 +926,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - try { - tickConsumer.accept(entity); - } catch (Throwable throwable) { -+ if (throwable instanceof ThreadDeath) throw throwable; // Paper - // Paper start - Prevent block entity and entity crashes - final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level().getWorld().getName(), entity.getX(), entity.getY(), entity.getZ()); - MinecraftServer.LOGGER.error(msg, throwable); -diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -index 424c4613e202c6ba50fa0de65d2526d400a8e299..2a8609e33716949ff1877b6d10f64a9d7a7c81e9 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -@@ -1179,6 +1179,7 @@ public class LevelChunk extends ChunkAccess { - - gameprofilerfiller.pop(); - } catch (Throwable throwable) { -+ if (throwable instanceof ThreadDeath) throw throwable; // Paper - // Paper start - Prevent block entity and entity crashes - final String msg = String.format("BlockEntity threw exception at %s:%s,%s,%s", LevelChunk.this.getLevel().getWorld().getName(), this.getPos().getX(), this.getPos().getY(), this.getPos().getZ()); - net.minecraft.server.MinecraftServer.LOGGER.error(msg, throwable); -diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java -index 3b8a2de79c79302626f43f320b15bae52416cc37..c097f5d5fbd51cbbc01bbd54101905c59b3f3a4c 100644 ---- a/src/main/java/org/bukkit/craftbukkit/Main.java -+++ b/src/main/java/org/bukkit/craftbukkit/Main.java -@@ -187,6 +187,36 @@ public class Main { - - OptionSet options = null; - -+ // Paper start - preload logger classes to avoid plugins mixing versions -+ tryPreloadClass("org.apache.logging.log4j.core.Core"); -+ tryPreloadClass("org.apache.logging.log4j.core.appender.AsyncAppender"); -+ tryPreloadClass("org.apache.logging.log4j.core.Appender"); -+ tryPreloadClass("org.apache.logging.log4j.core.ContextDataInjector"); -+ tryPreloadClass("org.apache.logging.log4j.core.Filter"); -+ tryPreloadClass("org.apache.logging.log4j.core.ErrorHandler"); -+ tryPreloadClass("org.apache.logging.log4j.core.LogEvent"); -+ tryPreloadClass("org.apache.logging.log4j.core.Logger"); -+ tryPreloadClass("org.apache.logging.log4j.core.LoggerContext"); -+ tryPreloadClass("org.apache.logging.log4j.core.LogEventListener"); -+ tryPreloadClass("org.apache.logging.log4j.core.AbstractLogEvent"); -+ tryPreloadClass("org.apache.logging.log4j.message.AsynchronouslyFormattable"); -+ tryPreloadClass("org.apache.logging.log4j.message.FormattedMessage"); -+ tryPreloadClass("org.apache.logging.log4j.message.ParameterizedMessage"); -+ tryPreloadClass("org.apache.logging.log4j.message.Message"); -+ tryPreloadClass("org.apache.logging.log4j.message.MessageFactory"); -+ tryPreloadClass("org.apache.logging.log4j.message.TimestampMessage"); -+ tryPreloadClass("org.apache.logging.log4j.message.SimpleMessage"); -+ tryPreloadClass("org.apache.logging.log4j.core.async.AsyncLogger"); -+ tryPreloadClass("org.apache.logging.log4j.core.async.AsyncLoggerContext"); -+ tryPreloadClass("org.apache.logging.log4j.core.async.AsyncQueueFullPolicy"); -+ tryPreloadClass("org.apache.logging.log4j.core.async.AsyncLoggerDisruptor"); -+ tryPreloadClass("org.apache.logging.log4j.core.async.RingBufferLogEvent"); -+ tryPreloadClass("org.apache.logging.log4j.core.async.DisruptorUtil"); -+ tryPreloadClass("org.apache.logging.log4j.core.async.RingBufferLogEventHandler"); -+ tryPreloadClass("org.apache.logging.log4j.core.impl.ThrowableProxy"); -+ tryPreloadClass("org.apache.logging.log4j.core.impl.ExtendedClassInfo"); -+ tryPreloadClass("org.apache.logging.log4j.core.impl.ExtendedStackTraceElement"); -+ // Paper end - try { - options = parser.parse(args); - } catch (joptsimple.OptionException ex) { -@@ -298,8 +328,65 @@ public class Main { - } catch (Throwable t) { - t.printStackTrace(); - } -+ // Paper start -+ // load some required classes to avoid errors during shutdown if jar is replaced -+ // also to guarantee our version loads over plugins -+ tryPreloadClass("com.destroystokyo.paper.util.SneakyThrow"); -+ tryPreloadClass("com.google.common.collect.Iterators$PeekingImpl"); -+ tryPreloadClass("com.google.common.collect.MapMakerInternalMap$Values"); -+ tryPreloadClass("com.google.common.collect.MapMakerInternalMap$ValueIterator"); -+ tryPreloadClass("com.google.common.collect.MapMakerInternalMap$WriteThroughEntry"); -+ tryPreloadClass("com.google.common.collect.Iterables"); -+ for (int i = 1; i <= 15; i++) { -+ tryPreloadClass("com.google.common.collect.Iterables$" + i, false); -+ } -+ tryPreloadClass("org.apache.commons.lang3.mutable.MutableBoolean"); -+ tryPreloadClass("org.apache.commons.lang3.mutable.MutableInt"); -+ tryPreloadClass("org.jline.terminal.impl.MouseSupport"); -+ tryPreloadClass("org.jline.terminal.impl.MouseSupport$1"); -+ tryPreloadClass("org.jline.terminal.Terminal$MouseTracking"); -+ tryPreloadClass("co.aikar.timings.TimingHistory"); -+ tryPreloadClass("co.aikar.timings.TimingHistory$MinuteReport"); -+ tryPreloadClass("io.netty.channel.AbstractChannelHandlerContext"); -+ tryPreloadClass("io.netty.channel.AbstractChannelHandlerContext$11"); -+ tryPreloadClass("io.netty.channel.AbstractChannelHandlerContext$12"); -+ tryPreloadClass("io.netty.channel.AbstractChannel$AbstractUnsafe$8"); -+ tryPreloadClass("io.netty.util.concurrent.DefaultPromise"); -+ tryPreloadClass("io.netty.util.concurrent.DefaultPromise$1"); -+ tryPreloadClass("io.netty.util.internal.PromiseNotificationUtil"); -+ tryPreloadClass("io.netty.util.internal.SystemPropertyUtil"); -+ tryPreloadClass("org.bukkit.craftbukkit.scheduler.CraftScheduler"); -+ tryPreloadClass("org.bukkit.craftbukkit.scheduler.CraftScheduler$1"); -+ tryPreloadClass("org.bukkit.craftbukkit.scheduler.CraftScheduler$2"); -+ tryPreloadClass("org.bukkit.craftbukkit.scheduler.CraftScheduler$3"); -+ tryPreloadClass("org.bukkit.craftbukkit.scheduler.CraftScheduler$4"); -+ tryPreloadClass("org.slf4j.helpers.MessageFormatter"); -+ tryPreloadClass("org.slf4j.helpers.FormattingTuple"); -+ tryPreloadClass("org.slf4j.helpers.BasicMarker"); -+ tryPreloadClass("org.slf4j.helpers.Util"); -+ tryPreloadClass("com.destroystokyo.paper.event.player.PlayerConnectionCloseEvent"); -+ tryPreloadClass("com.destroystokyo.paper.event.entity.EntityRemoveFromWorldEvent"); -+ // Minecraft, seen during saving -+ tryPreloadClass(net.minecraft.world.level.lighting.LayerLightEventListener.DummyLightLayerEventListener.class.getName()); -+ tryPreloadClass(net.minecraft.world.level.lighting.LayerLightEventListener.class.getName()); -+ tryPreloadClass(net.minecraft.util.ExceptionCollector.class.getName()); -+ tryPreloadClass(io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.PlayerChunkLoaderData.class.getName()); -+ // Paper end -+ } -+ } -+ -+ // Paper start -+ private static void tryPreloadClass(String className) { -+ tryPreloadClass(className, true); -+ } -+ private static void tryPreloadClass(String className, boolean printError) { -+ try { -+ Class.forName(className); -+ } catch (ClassNotFoundException e) { -+ if (printError) System.err.println("An expected class " + className + " was not found for preloading: " + e.getMessage()); - } - } -+ // Paper end - - private static List asList(String... params) { - return Arrays.asList(params); -diff --git a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java -index c6e8441e299f477ddb22c1ce2618710763978f1a..e8e93538dfd71de86515d9405f728db1631e949a 100644 ---- a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java -+++ b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java -@@ -12,11 +12,27 @@ public class ServerShutdownThread extends Thread { - @Override - public void run() { - try { -+ // Paper start - try to shutdown on main -+ server.safeShutdown(false, false); -+ for (int i = 1000; i > 0 && !server.hasStopped(); i -= 100) { -+ Thread.sleep(100); -+ } -+ if (server.hasStopped()) { -+ while (!server.hasFullyShutdown) Thread.sleep(1000); -+ return; -+ } -+ // Looks stalled, close async - org.spigotmc.AsyncCatcher.enabled = false; // Spigot -+ server.forceTicks = true; - this.server.close(); -+ while (!server.hasFullyShutdown) Thread.sleep(1000); -+ } catch (InterruptedException e) { -+ e.printStackTrace(); -+ // Paper end - } finally { -+ org.apache.logging.log4j.LogManager.shutdown(); // Paper - try { -- net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Use TerminalConsoleAppender -+ //net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Move into stop - } catch (Exception e) { - } - } -diff --git a/src/main/java/org/spigotmc/RestartCommand.java b/src/main/java/org/spigotmc/RestartCommand.java -index e3b262add194a126e731c68e68f3139a00cacacb..da7d5efd76c9ef92e9ce22860fec791890a687be 100644 ---- a/src/main/java/org/spigotmc/RestartCommand.java -+++ b/src/main/java/org/spigotmc/RestartCommand.java -@@ -138,7 +138,7 @@ public class RestartCommand extends Command - // Paper end - - // Paper start - copied from above and modified to return if the hook registered -- private static boolean addShutdownHook(String restartScript) -+ public static boolean addShutdownHook(String restartScript) - { - String[] split = restartScript.split( " " ); - if ( split.length > 0 && new File( split[0] ).isFile() ) -diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java -index 9e5d08f57aa448552d100ca892c211d44441ef68..577f29519156f33b49ee319a64a52249630e28d8 100644 ---- a/src/main/java/org/spigotmc/WatchdogThread.java -+++ b/src/main/java/org/spigotmc/WatchdogThread.java -@@ -11,6 +11,7 @@ import org.bukkit.Bukkit; - public final class WatchdogThread extends io.papermc.paper.util.TickThread // Paper - rewrite chunk system - { - -+ public static final boolean DISABLE_WATCHDOG = Boolean.getBoolean("disable.watchdog"); // Paper - private static WatchdogThread instance; - private long timeoutTime; - private boolean restart; -@@ -39,6 +40,7 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa - { - if ( WatchdogThread.instance == null ) - { -+ if (timeoutTime <= 0) timeoutTime = 300; // Paper - WatchdogThread.instance = new WatchdogThread( timeoutTime * 1000L, restart ); - WatchdogThread.instance.start(); - } else -@@ -70,12 +72,13 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa - // Paper start - Logger log = Bukkit.getServer().getLogger(); - long currentTime = WatchdogThread.monotonicMillis(); -- if ( this.lastTick != 0 && this.timeoutTime > 0 && currentTime > this.lastTick + this.earlyWarningEvery && !Boolean.getBoolean("disable.watchdog")) // Paper - Add property to disable -+ MinecraftServer server = MinecraftServer.getServer(); -+ if ( this.lastTick != 0 && this.timeoutTime > 0 && WatchdogThread.hasStarted && (!server.isRunning() || (currentTime > this.lastTick + this.earlyWarningEvery && !DISABLE_WATCHDOG) )) // Paper - add property to disable - { -- boolean isLongTimeout = currentTime > lastTick + timeoutTime; -+ boolean isLongTimeout = currentTime > lastTick + timeoutTime || (!server.isRunning() && !server.hasStopped() && currentTime > lastTick + 1000); - // Don't spam early warning dumps - if ( !isLongTimeout && (earlyWarningEvery <= 0 || !hasStarted || currentTime < lastEarlyWarning + earlyWarningEvery || currentTime < lastTick + earlyWarningDelay)) continue; -- if ( !isLongTimeout && MinecraftServer.getServer().hasStopped()) continue; // Don't spam early watchdog warnings during shutdown, we'll come back to this... -+ if ( !isLongTimeout && server.hasStopped()) continue; // Don't spam early watchdog warnings during shutdown, we'll come back to this... - lastEarlyWarning = currentTime; - if (isLongTimeout) { - // Paper end -@@ -136,9 +139,24 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa - - if ( isLongTimeout ) - { -- if ( this.restart && !MinecraftServer.getServer().hasStopped() ) -+ if ( !server.hasStopped() ) - { -- RestartCommand.restart(); -+ AsyncCatcher.enabled = false; // Disable async catcher incase it interferes with us -+ server.forceTicks = true; -+ if (restart) { -+ RestartCommand.addShutdownHook( SpigotConfig.restartScript ); -+ } -+ // try one last chance to safe shutdown on main incase it 'comes back' -+ server.abnormalExit = true; -+ server.safeShutdown(false, restart); -+ try { -+ Thread.sleep(1000); -+ } catch (InterruptedException e) { -+ e.printStackTrace(); -+ } -+ if (!server.hasStopped()) { -+ server.close(); -+ } - } - break; - } // Paper end -diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml -index 637d64da9938e51a97338b9253b43889585c67bb..d2a75850af9c6ad2aca66a5f994f1b587d73eac4 100644 ---- a/src/main/resources/log4j2.xml -+++ b/src/main/resources/log4j2.xml -@@ -1,5 +1,5 @@ - -- -+ - - - diff --git a/patches/server/1002-Optimize-Voxel-Shape-Merging.patch b/patches/server/1002-Optimize-Voxel-Shape-Merging.patch new file mode 100644 index 0000000000..4ec779560c --- /dev/null +++ b/patches/server/1002-Optimize-Voxel-Shape-Merging.patch @@ -0,0 +1,123 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Sun, 3 May 2020 22:35:09 -0400 +Subject: [PATCH] Optimize Voxel Shape Merging + +This method shows up as super hot in profiler, and also a high "self" time. + +Upon analyzing, it appears most usages of this method fall down to the final +else statement of the nasty ternary. + +Upon even further analyzation, it appears then the majority of those have a +consistent list 1.... One with Infinity head and Tails. + +First optimization is to detect these infinite states and immediately return that +VoxelShapeMergerList so we can avoid testing the rest for most cases. + +Break the method into 2 to help the JVM promote inlining of this fast path. + +Then it was also noticed that VoxelShapeMergerList constructor is also a hotspot +with a high self time... + +Well, knowing that in most cases our list 1 is actualy the same value, it allows +us to know that with an infinite list1, the result on the merger is essentially +list2 as the final values. + +This let us analyze the 2 potential states (Infinite with 2 sources or 4 sources) +and compute a deterministic result for the MergerList values. + +Additionally, this lets us avoid even allocating new objects for this too, further +reducing memory usage. + +diff --git a/src/main/java/net/minecraft/world/phys/shapes/IndirectMerger.java b/src/main/java/net/minecraft/world/phys/shapes/IndirectMerger.java +index e164c524aef4fa81fe96ac43454eecff1c38b9c1..9cfbbc61fcfc678f0988d6d45c7994d128051744 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/IndirectMerger.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/IndirectMerger.java +@@ -10,12 +10,33 @@ public class IndirectMerger implements IndexMerger { + private final int[] firstIndices; + private final int[] secondIndices; + private final int resultLength; ++ // Paper start ++ private static final int[] INFINITE_B_1 = new int[]{1, 1}; ++ private static final int[] INFINITE_B_0 = new int[]{0, 0}; ++ private static final int[] INFINITE_C = new int[]{0, 1}; ++ // Paper end + + public IndirectMerger(DoubleList first, DoubleList second, boolean includeFirstOnly, boolean includeSecondOnly) { + double d = Double.NaN; + int i = first.size(); + int j = second.size(); + int k = i + j; ++ // Paper start - optimize common path of infinity doublelist ++ int size = first.size(); ++ double tail = first.getDouble(size - 1); ++ double head = first.getDouble(0); ++ if (head == Double.NEGATIVE_INFINITY && tail == Double.POSITIVE_INFINITY && !includeFirstOnly && !includeSecondOnly && (size == 2 || size == 4)) { ++ this.result = second.toDoubleArray(); ++ this.resultLength = second.size(); ++ if (size == 2) { ++ this.firstIndices = INFINITE_B_0; ++ } else { ++ this.firstIndices = INFINITE_B_1; ++ } ++ this.secondIndices = INFINITE_C; ++ return; ++ } ++ // Paper end + this.result = new double[k]; + this.firstIndices = new int[k]; + this.secondIndices = new int[k]; +diff --git a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java +index 0fdd2cdd8d215ca1523eda8ad7316cdd5f41a6b5..86df4ef44d0a5107ee929dfd40d8ccb0779e8bfc 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java +@@ -286,9 +286,21 @@ public final class Shapes { + } + + @VisibleForTesting +- protected static IndexMerger createIndexMerger(int size, DoubleList first, DoubleList second, boolean includeFirst, boolean includeSecond) { ++ private static IndexMerger createIndexMerger(int size, DoubleList first, DoubleList second, boolean includeFirst, boolean includeSecond) { // Paper - private ++ // Paper start - fast track the most common scenario ++ // doublelist is usually a DoubleArrayList with Infinite head/tails that falls to the final else clause ++ // This is actually the most common path, so jump to it straight away ++ if (first.getDouble(0) == Double.NEGATIVE_INFINITY && first.getDouble(first.size() - 1) == Double.POSITIVE_INFINITY) { ++ return new IndirectMerger(first, second, includeFirst, includeSecond); ++ } ++ // Split out rest to hopefully inline the above ++ return lessCommonMerge(size, first, second, includeFirst, includeSecond); ++ } ++ ++ private static IndexMerger lessCommonMerge(int size, DoubleList first, DoubleList second, boolean includeFirst, boolean includeSecond) { + int i = first.size() - 1; + int j = second.size() - 1; ++ // Paper note - Rewrite below as optimized order if instead of nasty ternary + if (first instanceof CubePointRange && second instanceof CubePointRange) { + long l = lcm(i, j); + if ((long)size * l <= 256L) { +@@ -296,15 +308,22 @@ public final class Shapes { + } + } + +- if (first.getDouble(i) < second.getDouble(0) - 1.0E-7) { ++ // Paper start - Identical happens more often than Disjoint ++ if (i == j && Objects.equals(first, second)) { ++ if (first instanceof IdenticalMerger) { ++ return (IndexMerger) first; ++ } else if (second instanceof IdenticalMerger) { ++ return (IndexMerger) second; ++ } ++ return new IdenticalMerger(first); ++ } else if (first.getDouble(i) < second.getDouble(0) - 1.0E-7) { + return new NonOverlappingMerger(first, second, false); + } else if (second.getDouble(j) < first.getDouble(0) - 1.0E-7) { + return new NonOverlappingMerger(second, first, true); + } else { +- return (IndexMerger)(i == j && Objects.equals(first, second) +- ? new IdenticalMerger(first) +- : new IndirectMerger(first, second, includeFirst, includeSecond)); ++ return new IndirectMerger(first, second, includeFirst, includeSecond); + } ++ // Paper end + } + + public interface DoubleLineConsumer { diff --git a/patches/server/1003-Optimize-Voxel-Shape-Merging.patch b/patches/server/1003-Optimize-Voxel-Shape-Merging.patch deleted file mode 100644 index 4ec779560c..0000000000 --- a/patches/server/1003-Optimize-Voxel-Shape-Merging.patch +++ /dev/null @@ -1,123 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Sun, 3 May 2020 22:35:09 -0400 -Subject: [PATCH] Optimize Voxel Shape Merging - -This method shows up as super hot in profiler, and also a high "self" time. - -Upon analyzing, it appears most usages of this method fall down to the final -else statement of the nasty ternary. - -Upon even further analyzation, it appears then the majority of those have a -consistent list 1.... One with Infinity head and Tails. - -First optimization is to detect these infinite states and immediately return that -VoxelShapeMergerList so we can avoid testing the rest for most cases. - -Break the method into 2 to help the JVM promote inlining of this fast path. - -Then it was also noticed that VoxelShapeMergerList constructor is also a hotspot -with a high self time... - -Well, knowing that in most cases our list 1 is actualy the same value, it allows -us to know that with an infinite list1, the result on the merger is essentially -list2 as the final values. - -This let us analyze the 2 potential states (Infinite with 2 sources or 4 sources) -and compute a deterministic result for the MergerList values. - -Additionally, this lets us avoid even allocating new objects for this too, further -reducing memory usage. - -diff --git a/src/main/java/net/minecraft/world/phys/shapes/IndirectMerger.java b/src/main/java/net/minecraft/world/phys/shapes/IndirectMerger.java -index e164c524aef4fa81fe96ac43454eecff1c38b9c1..9cfbbc61fcfc678f0988d6d45c7994d128051744 100644 ---- a/src/main/java/net/minecraft/world/phys/shapes/IndirectMerger.java -+++ b/src/main/java/net/minecraft/world/phys/shapes/IndirectMerger.java -@@ -10,12 +10,33 @@ public class IndirectMerger implements IndexMerger { - private final int[] firstIndices; - private final int[] secondIndices; - private final int resultLength; -+ // Paper start -+ private static final int[] INFINITE_B_1 = new int[]{1, 1}; -+ private static final int[] INFINITE_B_0 = new int[]{0, 0}; -+ private static final int[] INFINITE_C = new int[]{0, 1}; -+ // Paper end - - public IndirectMerger(DoubleList first, DoubleList second, boolean includeFirstOnly, boolean includeSecondOnly) { - double d = Double.NaN; - int i = first.size(); - int j = second.size(); - int k = i + j; -+ // Paper start - optimize common path of infinity doublelist -+ int size = first.size(); -+ double tail = first.getDouble(size - 1); -+ double head = first.getDouble(0); -+ if (head == Double.NEGATIVE_INFINITY && tail == Double.POSITIVE_INFINITY && !includeFirstOnly && !includeSecondOnly && (size == 2 || size == 4)) { -+ this.result = second.toDoubleArray(); -+ this.resultLength = second.size(); -+ if (size == 2) { -+ this.firstIndices = INFINITE_B_0; -+ } else { -+ this.firstIndices = INFINITE_B_1; -+ } -+ this.secondIndices = INFINITE_C; -+ return; -+ } -+ // Paper end - this.result = new double[k]; - this.firstIndices = new int[k]; - this.secondIndices = new int[k]; -diff --git a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java -index 0fdd2cdd8d215ca1523eda8ad7316cdd5f41a6b5..86df4ef44d0a5107ee929dfd40d8ccb0779e8bfc 100644 ---- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java -+++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java -@@ -286,9 +286,21 @@ public final class Shapes { - } - - @VisibleForTesting -- protected static IndexMerger createIndexMerger(int size, DoubleList first, DoubleList second, boolean includeFirst, boolean includeSecond) { -+ private static IndexMerger createIndexMerger(int size, DoubleList first, DoubleList second, boolean includeFirst, boolean includeSecond) { // Paper - private -+ // Paper start - fast track the most common scenario -+ // doublelist is usually a DoubleArrayList with Infinite head/tails that falls to the final else clause -+ // This is actually the most common path, so jump to it straight away -+ if (first.getDouble(0) == Double.NEGATIVE_INFINITY && first.getDouble(first.size() - 1) == Double.POSITIVE_INFINITY) { -+ return new IndirectMerger(first, second, includeFirst, includeSecond); -+ } -+ // Split out rest to hopefully inline the above -+ return lessCommonMerge(size, first, second, includeFirst, includeSecond); -+ } -+ -+ private static IndexMerger lessCommonMerge(int size, DoubleList first, DoubleList second, boolean includeFirst, boolean includeSecond) { - int i = first.size() - 1; - int j = second.size() - 1; -+ // Paper note - Rewrite below as optimized order if instead of nasty ternary - if (first instanceof CubePointRange && second instanceof CubePointRange) { - long l = lcm(i, j); - if ((long)size * l <= 256L) { -@@ -296,15 +308,22 @@ public final class Shapes { - } - } - -- if (first.getDouble(i) < second.getDouble(0) - 1.0E-7) { -+ // Paper start - Identical happens more often than Disjoint -+ if (i == j && Objects.equals(first, second)) { -+ if (first instanceof IdenticalMerger) { -+ return (IndexMerger) first; -+ } else if (second instanceof IdenticalMerger) { -+ return (IndexMerger) second; -+ } -+ return new IdenticalMerger(first); -+ } else if (first.getDouble(i) < second.getDouble(0) - 1.0E-7) { - return new NonOverlappingMerger(first, second, false); - } else if (second.getDouble(j) < first.getDouble(0) - 1.0E-7) { - return new NonOverlappingMerger(second, first, true); - } else { -- return (IndexMerger)(i == j && Objects.equals(first, second) -- ? new IdenticalMerger(first) -- : new IndirectMerger(first, second, includeFirst, includeSecond)); -+ return new IndirectMerger(first, second, includeFirst, includeSecond); - } -+ // Paper end - } - - public interface DoubleLineConsumer { diff --git a/patches/server/1003-Write-SavedData-IO-async.patch b/patches/server/1003-Write-SavedData-IO-async.patch new file mode 100644 index 0000000000..68fd4545a8 --- /dev/null +++ b/patches/server/1003-Write-SavedData-IO-async.patch @@ -0,0 +1,173 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Cryptite +Date: Tue, 27 Jun 2023 11:35:52 -0500 +Subject: [PATCH] Write SavedData IO async + +Co-Authored-By: Shane Freeder + +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index c7b7f153895a4b95b2071a31db00c9c4b69fa094..0b7a38b9e92b19345a34c6226413a9b133264077 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -447,6 +447,13 @@ public class ServerChunkCache extends ChunkSource { + + public void close(boolean save) { // Paper - rewrite chunk system + this.level.chunkTaskScheduler.chunkHolderManager.close(save, true); // Paper - rewrite chunk system ++ // Paper start - Write SavedData IO async ++ try { ++ this.dataStorage.close(); ++ } catch (IOException exception) { ++ LOGGER.error("Failed to close persistent world data", exception); ++ } ++ // Paper end - Write SavedData IO async + } + + // CraftBukkit start - modelled on below +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index bc609da7105293dfa4ed339f553be66b0a632698..4b7cad5f551d620dbd091111df94fb39efcb79ac 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -1330,7 +1330,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + + try (co.aikar.timings.Timing ignored = this.timings.worldSave.startTiming()) { + if (doFull) { +- this.saveLevelData(); ++ this.saveLevelData(true); // Paper - Write SavedData IO async + } + + this.timings.worldSaveChunks.startTiming(); // Paper +@@ -1366,7 +1366,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + progressListener.progressStartNoAbort(Component.translatable("menu.savingLevel")); + } + +- this.saveLevelData(); ++ this.saveLevelData(!close); // Paper - Write SavedData IO async + if (progressListener != null) { + progressListener.progressStage(Component.translatable("menu.savingChunks")); + } +@@ -1389,12 +1389,12 @@ public class ServerLevel extends Level implements WorldGenLevel { + // CraftBukkit end + } + +- private void saveLevelData() { ++ private void saveLevelData(boolean async) { // Paper - Write SavedData IO async + if (this.dragonFight != null) { + this.serverLevelData.setEndDragonFightData(this.dragonFight.saveData()); // CraftBukkit + } + +- this.getChunkSource().getDataStorage().save(); ++ this.getChunkSource().getDataStorage().save(async); // Paper - Write SavedData IO async + } + + public List getEntities(EntityTypeTest filter, Predicate predicate) { +diff --git a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java +index 639f72618a7c22fa94effa9d0406b97fffc64cb5..3e582c49069f2a820ba3baac03917493877d9875 100644 +--- a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java ++++ b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java +@@ -116,7 +116,13 @@ public class WorldUpgrader { + (new WorldUpgrader.PoiUpgrader(this)).upgrade(); + WorldUpgrader.LOGGER.info("Upgrading blocks"); + (new WorldUpgrader.ChunkUpgrader()).upgrade(); +- this.overworldDataStorage.save(); ++ // Paper start - Write SavedData IO async ++ try { ++ this.overworldDataStorage.close(); ++ } catch (final IOException e) { ++ LOGGER.error("Failed to close persistent world data", e); ++ } ++ // Paper end - Write SavedData IO async + i = Util.getMillis() - i; + WorldUpgrader.LOGGER.info("World optimizaton finished after {} seconds", i / 1000L); + this.finished = true; +diff --git a/src/main/java/net/minecraft/world/level/saveddata/SavedData.java b/src/main/java/net/minecraft/world/level/saveddata/SavedData.java +index 9cc3850bb70dfbcf342651360314a19fd9ea3ecc..4cbb943b702baaeb8bfd2b558cc848e719cf095d 100644 +--- a/src/main/java/net/minecraft/world/level/saveddata/SavedData.java ++++ b/src/main/java/net/minecraft/world/level/saveddata/SavedData.java +@@ -30,20 +30,36 @@ public abstract class SavedData { + return this.dirty; + } + ++ // Paper start - Write SavedData IO async - joining is evil, but we assume the old blocking behavior here just for safety ++ @io.papermc.paper.annotation.DoNotUse + public void save(File file, HolderLookup.Provider registryLookup) { ++ save(file, registryLookup, null).join(); ++ } ++ ++ public java.util.concurrent.CompletableFuture save(File file, HolderLookup.Provider registryLookup, @org.jetbrains.annotations.Nullable java.util.concurrent.ExecutorService ioExecutor) { ++ // Paper end - Write SavedData IO async + if (this.isDirty()) { + CompoundTag compoundTag = new CompoundTag(); + compoundTag.put("data", this.save(new CompoundTag(), registryLookup)); + NbtUtils.addCurrentDataVersion(compoundTag); + ++ Runnable writeRunnable = () -> { // Paper - Write SavedData IO async + try { + NbtIo.writeCompressed(compoundTag, file.toPath()); + } catch (IOException var5) { + LOGGER.error("Could not save data {}", this, var5); + } ++ }; // Paper - Write SavedData IO async + + this.setDirty(false); ++ // Paper start - Write SavedData IO async ++ if (ioExecutor == null) { ++ return java.util.concurrent.CompletableFuture.runAsync(writeRunnable); // No executor, just use common pool ++ } ++ return java.util.concurrent.CompletableFuture.runAsync(writeRunnable, ioExecutor); + } ++ return java.util.concurrent.CompletableFuture.completedFuture(null); ++ // Paper end - Write SavedData IO async + } + + public static record Factory( +diff --git a/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java b/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java +index 7d98d2911e9d6ec429ce7df1f1f2650c7ea32325..6e23e69abd56eeda3b52a22019e1b74ae10682e7 100644 +--- a/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java ++++ b/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java +@@ -23,17 +23,19 @@ import net.minecraft.util.datafix.DataFixTypes; + import net.minecraft.world.level.saveddata.SavedData; + import org.slf4j.Logger; + +-public class DimensionDataStorage { ++public class DimensionDataStorage implements java.io.Closeable { // Paper - Write SavedData IO async + private static final Logger LOGGER = LogUtils.getLogger(); + public final Map cache = Maps.newHashMap(); + private final DataFixer fixerUpper; + private final HolderLookup.Provider registries; + private final File dataFolder; ++ protected final java.util.concurrent.ExecutorService ioExecutor; // Paper - Write SavedData IO async + + public DimensionDataStorage(File directory, DataFixer dataFixer, HolderLookup.Provider registryLookup) { + this.fixerUpper = dataFixer; + this.dataFolder = directory; + this.registries = registryLookup; ++ this.ioExecutor = java.util.concurrent.Executors.newSingleThreadExecutor(new com.google.common.util.concurrent.ThreadFactoryBuilder().setNameFormat("DimensionDataIO - " + dataFolder.getParent() + " - %d").setDaemon(true).build()); // Paper - Write SavedData IO async + } + + private File getDataFile(String id) { +@@ -123,10 +125,23 @@ public class DimensionDataStorage { + return bl; + } + +- public void save() { ++ // Paper start - Write SavedData IO async ++ @Override ++ public void close() throws IOException { ++ save(false); ++ this.ioExecutor.shutdown(); ++ } ++ // Paper end - Write SavedData IO async ++ ++ public void save(boolean async) { // Paper - Write SavedData IO async + this.cache.forEach((id, state) -> { + if (state != null) { +- state.save(this.getDataFile(id), this.registries); ++ // Paper start - Write SavedData IO async ++ final java.util.concurrent.CompletableFuture save = state.save(this.getDataFile(id), this.registries, this.ioExecutor); ++ if (!async) { ++ save.join(); ++ } ++ // Paper end - Write SavedData IO async + } + }); + } diff --git a/patches/server/1004-Protect-Bedrock-and-End-Portal-Frames-from-being-des.patch b/patches/server/1004-Protect-Bedrock-and-End-Portal-Frames-from-being-des.patch new file mode 100644 index 0000000000..9d41c60b80 --- /dev/null +++ b/patches/server/1004-Protect-Bedrock-and-End-Portal-Frames-from-being-des.patch @@ -0,0 +1,169 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Wed, 13 May 2020 23:01:26 -0400 +Subject: [PATCH] Protect Bedrock and End Portal/Frames from being destroyed + +This fixes exploits that let players destroy bedrock by Pistons, explosions +and Mushrooom/Tree generation. + +These blocks are designed to not be broken except by creative players/commands. +So protect them from a multitude of methods of destroying them. + +A config is provided if you rather let players use these exploits, and let +them destroy the worlds End Portals and get on top of the nether easy. + +diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java +index 550c0cf4d84afc0cd6b2675bdc6aa2282d87e312..6bd6533e314b64d3512cbb46f5174a4956c323b2 100644 +--- a/src/main/java/net/minecraft/world/level/Explosion.java ++++ b/src/main/java/net/minecraft/world/level/Explosion.java +@@ -192,6 +192,7 @@ public class Explosion { + for (float f1 = 0.3F; f > 0.0F; f -= 0.22500001F) { + BlockPos blockposition = BlockPos.containing(d4, d5, d6); + BlockState iblockdata = this.level.getBlockState(blockposition); ++ if (!iblockdata.isDestroyable()) continue; // Paper - Protect Bedrock and End Portal/Frames from being destroyed + FluidState fluid = iblockdata.getFluidState(); // Paper - Perf: Optimize call to getFluid for explosions + + if (!this.level.isInWorldBounds(blockposition)) { +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index ee49f59ba8ce5708fc5e244eba7b1f910cf69263..d3d7abb2d31e8ce9f9c53eca66a83a1c28fec792 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -533,6 +533,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public boolean setBlock(BlockPos pos, BlockState state, int flags, int maxUpdateDepth) { + // CraftBukkit start - tree generation + if (this.captureTreeGeneration) { ++ // Paper start - Protect Bedrock and End Portal/Frames from being destroyed ++ BlockState type = getBlockState(pos); ++ if (!type.isDestroyable()) return false; ++ // Paper end - Protect Bedrock and End Portal/Frames from being destroyed + CraftBlockState blockstate = this.capturedBlockStates.get(pos); + if (blockstate == null) { + blockstate = CapturedBlockState.getTreeBlockState(this, pos, flags); +diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java +index 7f0c0ca49e7575c18935b71e3180d112440289f7..054593fc0b8d13f6bf449cc20a1f7ddfd5f1d1f0 100644 +--- a/src/main/java/net/minecraft/world/level/block/Block.java ++++ b/src/main/java/net/minecraft/world/level/block/Block.java +@@ -90,6 +90,19 @@ public class Block extends BlockBehaviour implements ItemLike { + protected final StateDefinition stateDefinition; + private BlockState defaultBlockState; + // Paper start ++ public final boolean isDestroyable() { ++ return io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits || ++ this != Blocks.BEDROCK && ++ this != Blocks.END_PORTAL_FRAME && ++ this != Blocks.END_PORTAL && ++ this != Blocks.END_GATEWAY && ++ this != Blocks.COMMAND_BLOCK && ++ this != Blocks.REPEATING_COMMAND_BLOCK && ++ this != Blocks.CHAIN_COMMAND_BLOCK && ++ this != Blocks.BARRIER && ++ this != Blocks.STRUCTURE_BLOCK && ++ this != Blocks.JIGSAW; ++ } + public co.aikar.timings.Timing timing; + public co.aikar.timings.Timing getTiming() { + if (timing == null) { +diff --git a/src/main/java/net/minecraft/world/level/block/piston/PistonBaseBlock.java b/src/main/java/net/minecraft/world/level/block/piston/PistonBaseBlock.java +index e6bfbe2588e0c2a1be14e38d654e889d392ad4db..e0c62227b279a5fe0f3868fbf9ce8c78c515a09c 100644 +--- a/src/main/java/net/minecraft/world/level/block/piston/PistonBaseBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/piston/PistonBaseBlock.java +@@ -213,6 +213,12 @@ public class PistonBaseBlock extends DirectionalBlock { + @Override + protected boolean triggerEvent(BlockState state, Level world, BlockPos pos, int type, int data) { + Direction enumdirection = (Direction) state.getValue(PistonBaseBlock.FACING); ++ // Paper start - Protect Bedrock and End Portal/Frames from being destroyed; prevent retracting when we're facing the wrong way (we were replaced before retraction could occur) ++ Direction directionQueuedAs = Direction.from3DDataValue(data & 7); // Paper - copied from below ++ if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits && enumdirection != directionQueuedAs) { ++ return false; ++ } ++ // Paper end - Protect Bedrock and End Portal/Frames from being destroyed + BlockState iblockdata1 = (BlockState) state.setValue(PistonBaseBlock.EXTENDED, true); + + if (!world.isClientSide) { +@@ -253,7 +259,7 @@ public class PistonBaseBlock extends DirectionalBlock { + } + // Paper end - Fix sticky pistons and BlockPistonRetractEvent + world.setBlock(pos, iblockdata2, 20); +- world.setBlockEntity(MovingPistonBlock.newMovingBlockEntity(pos, iblockdata2, (BlockState) this.defaultBlockState().setValue(PistonBaseBlock.FACING, Direction.from3DDataValue(data & 7)), enumdirection, false, true)); ++ world.setBlockEntity(MovingPistonBlock.newMovingBlockEntity(pos, iblockdata2, (BlockState) this.defaultBlockState().setValue(PistonBaseBlock.FACING, Direction.from3DDataValue(data & 7)), enumdirection, false, true)); // Paper - Protect Bedrock and End Portal/Frames from being destroyed; diff on change + world.blockUpdated(pos, iblockdata2.getBlock()); + iblockdata2.updateNeighbourShapes(world, pos, 2); + if (this.isSticky) { +@@ -289,7 +295,14 @@ public class PistonBaseBlock extends DirectionalBlock { + } + } + } else { +- world.removeBlock(pos.relative(enumdirection), false); ++ // Paper start - Protect Bedrock and End Portal/Frames from being destroyed; fix headless pistons breaking blocks ++ BlockPos headPos = pos.relative(enumdirection); ++ if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits || world.getBlockState(headPos) == Blocks.PISTON_HEAD.defaultBlockState().setValue(FACING, enumdirection)) { // double check to make sure we're not a headless piston. ++ world.removeBlock(headPos, false); ++ } else { ++ ((ServerLevel) world).getChunkSource().blockChanged(headPos); // ... fix client desync ++ } ++ // Paper end - Protect Bedrock and End Portal/Frames from being destroyed + } + + world.playSound((Player) null, pos, SoundEvents.PISTON_CONTRACT, SoundSource.BLOCKS, 0.5F, world.random.nextFloat() * 0.15F + 0.6F); +diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +index c7da359c525522b55763e594a1db0c26a026b73f..55efd0d379bac79935f62446cd3479d1e59361a4 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java ++++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +@@ -174,7 +174,7 @@ public abstract class BlockBehaviour implements FeatureElement { + } + + protected void onExplosionHit(BlockState state, Level world, BlockPos pos, Explosion explosion, BiConsumer stackMerger) { +- if (!state.isAir() && explosion.getBlockInteraction() != Explosion.BlockInteraction.TRIGGER_BLOCK) { ++ if (!state.isAir() && explosion.getBlockInteraction() != Explosion.BlockInteraction.TRIGGER_BLOCK && state.isDestroyable()) { // Paper - Protect Bedrock and End Portal/Frames from being destroyed + Block block = state.getBlock(); + boolean flag = explosion.getIndirectSourceEntity() instanceof Player; + +@@ -254,7 +254,7 @@ public abstract class BlockBehaviour implements FeatureElement { + } + + protected boolean canBeReplaced(BlockState state, BlockPlaceContext context) { +- return state.canBeReplaced() && (context.getItemInHand().isEmpty() || !context.getItemInHand().is(this.asItem())); ++ return state.canBeReplaced() && (context.getItemInHand().isEmpty() || !context.getItemInHand().is(this.asItem())) && (state.isDestroyable() || (context.getPlayer() != null && context.getPlayer().getAbilities().instabuild)); // Paper - Protect Bedrock and End Portal/Frames from being destroyed + } + + protected boolean canBeReplaced(BlockState state, Fluid fluid) { +@@ -896,6 +896,12 @@ public abstract class BlockBehaviour implements FeatureElement { + return this.legacySolid; + } + ++ // Paper start - Protect Bedrock and End Portal/Frames from being destroyed ++ public final boolean isDestroyable() { ++ return getBlock().isDestroyable(); ++ } ++ // Paper end - Protect Bedrock and End Portal/Frames from being destroyed ++ + public boolean isValidSpawn(BlockGetter world, BlockPos pos, EntityType type) { + return this.getBlock().properties.isValidSpawn.test(this.asState(), world, pos, type); + } +@@ -999,7 +1005,7 @@ public abstract class BlockBehaviour implements FeatureElement { + } + + public PushReaction getPistonPushReaction() { +- return this.pushReaction; ++ return !this.isDestroyable() ? PushReaction.BLOCK : this.pushReaction; // Paper - Protect Bedrock and End Portal/Frames from being destroyed + } + + public boolean isSolidRender(BlockGetter world, BlockPos pos) { +diff --git a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java +index 03dd833d61d5152af3032f23dd1fc4c75da9bc4f..a61959700d5e00739a79eaa617ac383160335f26 100644 +--- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java ++++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java +@@ -221,6 +221,13 @@ public class PortalForcer { + for (int j = -1; j < 3; ++j) { + for (int k = -1; k < 4; ++k) { + temp.setWithOffset(pos, portalDirection.getStepX() * j + enumdirection1.getStepX() * distanceOrthogonalToPortal, k, portalDirection.getStepZ() * j + enumdirection1.getStepZ() * distanceOrthogonalToPortal); ++ // Paper start - Protect Bedrock and End Portal/Frames from being destroyed ++ if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits) { ++ if (!this.level.getBlockState(temp).isDestroyable()) { ++ return false; ++ } ++ } ++ // Paper end - Protect Bedrock and End Portal/Frames from being destroyed + if (k < 0 && !this.level.getBlockState(temp).isSolid()) { + return false; + } diff --git a/patches/server/1004-Write-SavedData-IO-async.patch b/patches/server/1004-Write-SavedData-IO-async.patch deleted file mode 100644 index 68fd4545a8..0000000000 --- a/patches/server/1004-Write-SavedData-IO-async.patch +++ /dev/null @@ -1,173 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Cryptite -Date: Tue, 27 Jun 2023 11:35:52 -0500 -Subject: [PATCH] Write SavedData IO async - -Co-Authored-By: Shane Freeder - -diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -index c7b7f153895a4b95b2071a31db00c9c4b69fa094..0b7a38b9e92b19345a34c6226413a9b133264077 100644 ---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java -+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -@@ -447,6 +447,13 @@ public class ServerChunkCache extends ChunkSource { - - public void close(boolean save) { // Paper - rewrite chunk system - this.level.chunkTaskScheduler.chunkHolderManager.close(save, true); // Paper - rewrite chunk system -+ // Paper start - Write SavedData IO async -+ try { -+ this.dataStorage.close(); -+ } catch (IOException exception) { -+ LOGGER.error("Failed to close persistent world data", exception); -+ } -+ // Paper end - Write SavedData IO async - } - - // CraftBukkit start - modelled on below -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index bc609da7105293dfa4ed339f553be66b0a632698..4b7cad5f551d620dbd091111df94fb39efcb79ac 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -1330,7 +1330,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - - try (co.aikar.timings.Timing ignored = this.timings.worldSave.startTiming()) { - if (doFull) { -- this.saveLevelData(); -+ this.saveLevelData(true); // Paper - Write SavedData IO async - } - - this.timings.worldSaveChunks.startTiming(); // Paper -@@ -1366,7 +1366,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - progressListener.progressStartNoAbort(Component.translatable("menu.savingLevel")); - } - -- this.saveLevelData(); -+ this.saveLevelData(!close); // Paper - Write SavedData IO async - if (progressListener != null) { - progressListener.progressStage(Component.translatable("menu.savingChunks")); - } -@@ -1389,12 +1389,12 @@ public class ServerLevel extends Level implements WorldGenLevel { - // CraftBukkit end - } - -- private void saveLevelData() { -+ private void saveLevelData(boolean async) { // Paper - Write SavedData IO async - if (this.dragonFight != null) { - this.serverLevelData.setEndDragonFightData(this.dragonFight.saveData()); // CraftBukkit - } - -- this.getChunkSource().getDataStorage().save(); -+ this.getChunkSource().getDataStorage().save(async); // Paper - Write SavedData IO async - } - - public List getEntities(EntityTypeTest filter, Predicate predicate) { -diff --git a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java -index 639f72618a7c22fa94effa9d0406b97fffc64cb5..3e582c49069f2a820ba3baac03917493877d9875 100644 ---- a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java -+++ b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java -@@ -116,7 +116,13 @@ public class WorldUpgrader { - (new WorldUpgrader.PoiUpgrader(this)).upgrade(); - WorldUpgrader.LOGGER.info("Upgrading blocks"); - (new WorldUpgrader.ChunkUpgrader()).upgrade(); -- this.overworldDataStorage.save(); -+ // Paper start - Write SavedData IO async -+ try { -+ this.overworldDataStorage.close(); -+ } catch (final IOException e) { -+ LOGGER.error("Failed to close persistent world data", e); -+ } -+ // Paper end - Write SavedData IO async - i = Util.getMillis() - i; - WorldUpgrader.LOGGER.info("World optimizaton finished after {} seconds", i / 1000L); - this.finished = true; -diff --git a/src/main/java/net/minecraft/world/level/saveddata/SavedData.java b/src/main/java/net/minecraft/world/level/saveddata/SavedData.java -index 9cc3850bb70dfbcf342651360314a19fd9ea3ecc..4cbb943b702baaeb8bfd2b558cc848e719cf095d 100644 ---- a/src/main/java/net/minecraft/world/level/saveddata/SavedData.java -+++ b/src/main/java/net/minecraft/world/level/saveddata/SavedData.java -@@ -30,20 +30,36 @@ public abstract class SavedData { - return this.dirty; - } - -+ // Paper start - Write SavedData IO async - joining is evil, but we assume the old blocking behavior here just for safety -+ @io.papermc.paper.annotation.DoNotUse - public void save(File file, HolderLookup.Provider registryLookup) { -+ save(file, registryLookup, null).join(); -+ } -+ -+ public java.util.concurrent.CompletableFuture save(File file, HolderLookup.Provider registryLookup, @org.jetbrains.annotations.Nullable java.util.concurrent.ExecutorService ioExecutor) { -+ // Paper end - Write SavedData IO async - if (this.isDirty()) { - CompoundTag compoundTag = new CompoundTag(); - compoundTag.put("data", this.save(new CompoundTag(), registryLookup)); - NbtUtils.addCurrentDataVersion(compoundTag); - -+ Runnable writeRunnable = () -> { // Paper - Write SavedData IO async - try { - NbtIo.writeCompressed(compoundTag, file.toPath()); - } catch (IOException var5) { - LOGGER.error("Could not save data {}", this, var5); - } -+ }; // Paper - Write SavedData IO async - - this.setDirty(false); -+ // Paper start - Write SavedData IO async -+ if (ioExecutor == null) { -+ return java.util.concurrent.CompletableFuture.runAsync(writeRunnable); // No executor, just use common pool -+ } -+ return java.util.concurrent.CompletableFuture.runAsync(writeRunnable, ioExecutor); - } -+ return java.util.concurrent.CompletableFuture.completedFuture(null); -+ // Paper end - Write SavedData IO async - } - - public static record Factory( -diff --git a/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java b/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java -index 7d98d2911e9d6ec429ce7df1f1f2650c7ea32325..6e23e69abd56eeda3b52a22019e1b74ae10682e7 100644 ---- a/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java -+++ b/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java -@@ -23,17 +23,19 @@ import net.minecraft.util.datafix.DataFixTypes; - import net.minecraft.world.level.saveddata.SavedData; - import org.slf4j.Logger; - --public class DimensionDataStorage { -+public class DimensionDataStorage implements java.io.Closeable { // Paper - Write SavedData IO async - private static final Logger LOGGER = LogUtils.getLogger(); - public final Map cache = Maps.newHashMap(); - private final DataFixer fixerUpper; - private final HolderLookup.Provider registries; - private final File dataFolder; -+ protected final java.util.concurrent.ExecutorService ioExecutor; // Paper - Write SavedData IO async - - public DimensionDataStorage(File directory, DataFixer dataFixer, HolderLookup.Provider registryLookup) { - this.fixerUpper = dataFixer; - this.dataFolder = directory; - this.registries = registryLookup; -+ this.ioExecutor = java.util.concurrent.Executors.newSingleThreadExecutor(new com.google.common.util.concurrent.ThreadFactoryBuilder().setNameFormat("DimensionDataIO - " + dataFolder.getParent() + " - %d").setDaemon(true).build()); // Paper - Write SavedData IO async - } - - private File getDataFile(String id) { -@@ -123,10 +125,23 @@ public class DimensionDataStorage { - return bl; - } - -- public void save() { -+ // Paper start - Write SavedData IO async -+ @Override -+ public void close() throws IOException { -+ save(false); -+ this.ioExecutor.shutdown(); -+ } -+ // Paper end - Write SavedData IO async -+ -+ public void save(boolean async) { // Paper - Write SavedData IO async - this.cache.forEach((id, state) -> { - if (state != null) { -- state.save(this.getDataFile(id), this.registries); -+ // Paper start - Write SavedData IO async -+ final java.util.concurrent.CompletableFuture save = state.save(this.getDataFile(id), this.registries, this.ioExecutor); -+ if (!async) { -+ save.join(); -+ } -+ // Paper end - Write SavedData IO async - } - }); - } diff --git a/patches/server/1005-Protect-Bedrock-and-End-Portal-Frames-from-being-des.patch b/patches/server/1005-Protect-Bedrock-and-End-Portal-Frames-from-being-des.patch deleted file mode 100644 index 9d41c60b80..0000000000 --- a/patches/server/1005-Protect-Bedrock-and-End-Portal-Frames-from-being-des.patch +++ /dev/null @@ -1,169 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Wed, 13 May 2020 23:01:26 -0400 -Subject: [PATCH] Protect Bedrock and End Portal/Frames from being destroyed - -This fixes exploits that let players destroy bedrock by Pistons, explosions -and Mushrooom/Tree generation. - -These blocks are designed to not be broken except by creative players/commands. -So protect them from a multitude of methods of destroying them. - -A config is provided if you rather let players use these exploits, and let -them destroy the worlds End Portals and get on top of the nether easy. - -diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java -index 550c0cf4d84afc0cd6b2675bdc6aa2282d87e312..6bd6533e314b64d3512cbb46f5174a4956c323b2 100644 ---- a/src/main/java/net/minecraft/world/level/Explosion.java -+++ b/src/main/java/net/minecraft/world/level/Explosion.java -@@ -192,6 +192,7 @@ public class Explosion { - for (float f1 = 0.3F; f > 0.0F; f -= 0.22500001F) { - BlockPos blockposition = BlockPos.containing(d4, d5, d6); - BlockState iblockdata = this.level.getBlockState(blockposition); -+ if (!iblockdata.isDestroyable()) continue; // Paper - Protect Bedrock and End Portal/Frames from being destroyed - FluidState fluid = iblockdata.getFluidState(); // Paper - Perf: Optimize call to getFluid for explosions - - if (!this.level.isInWorldBounds(blockposition)) { -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index ee49f59ba8ce5708fc5e244eba7b1f910cf69263..d3d7abb2d31e8ce9f9c53eca66a83a1c28fec792 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -533,6 +533,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - public boolean setBlock(BlockPos pos, BlockState state, int flags, int maxUpdateDepth) { - // CraftBukkit start - tree generation - if (this.captureTreeGeneration) { -+ // Paper start - Protect Bedrock and End Portal/Frames from being destroyed -+ BlockState type = getBlockState(pos); -+ if (!type.isDestroyable()) return false; -+ // Paper end - Protect Bedrock and End Portal/Frames from being destroyed - CraftBlockState blockstate = this.capturedBlockStates.get(pos); - if (blockstate == null) { - blockstate = CapturedBlockState.getTreeBlockState(this, pos, flags); -diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java -index 7f0c0ca49e7575c18935b71e3180d112440289f7..054593fc0b8d13f6bf449cc20a1f7ddfd5f1d1f0 100644 ---- a/src/main/java/net/minecraft/world/level/block/Block.java -+++ b/src/main/java/net/minecraft/world/level/block/Block.java -@@ -90,6 +90,19 @@ public class Block extends BlockBehaviour implements ItemLike { - protected final StateDefinition stateDefinition; - private BlockState defaultBlockState; - // Paper start -+ public final boolean isDestroyable() { -+ return io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits || -+ this != Blocks.BEDROCK && -+ this != Blocks.END_PORTAL_FRAME && -+ this != Blocks.END_PORTAL && -+ this != Blocks.END_GATEWAY && -+ this != Blocks.COMMAND_BLOCK && -+ this != Blocks.REPEATING_COMMAND_BLOCK && -+ this != Blocks.CHAIN_COMMAND_BLOCK && -+ this != Blocks.BARRIER && -+ this != Blocks.STRUCTURE_BLOCK && -+ this != Blocks.JIGSAW; -+ } - public co.aikar.timings.Timing timing; - public co.aikar.timings.Timing getTiming() { - if (timing == null) { -diff --git a/src/main/java/net/minecraft/world/level/block/piston/PistonBaseBlock.java b/src/main/java/net/minecraft/world/level/block/piston/PistonBaseBlock.java -index e6bfbe2588e0c2a1be14e38d654e889d392ad4db..e0c62227b279a5fe0f3868fbf9ce8c78c515a09c 100644 ---- a/src/main/java/net/minecraft/world/level/block/piston/PistonBaseBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/piston/PistonBaseBlock.java -@@ -213,6 +213,12 @@ public class PistonBaseBlock extends DirectionalBlock { - @Override - protected boolean triggerEvent(BlockState state, Level world, BlockPos pos, int type, int data) { - Direction enumdirection = (Direction) state.getValue(PistonBaseBlock.FACING); -+ // Paper start - Protect Bedrock and End Portal/Frames from being destroyed; prevent retracting when we're facing the wrong way (we were replaced before retraction could occur) -+ Direction directionQueuedAs = Direction.from3DDataValue(data & 7); // Paper - copied from below -+ if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits && enumdirection != directionQueuedAs) { -+ return false; -+ } -+ // Paper end - Protect Bedrock and End Portal/Frames from being destroyed - BlockState iblockdata1 = (BlockState) state.setValue(PistonBaseBlock.EXTENDED, true); - - if (!world.isClientSide) { -@@ -253,7 +259,7 @@ public class PistonBaseBlock extends DirectionalBlock { - } - // Paper end - Fix sticky pistons and BlockPistonRetractEvent - world.setBlock(pos, iblockdata2, 20); -- world.setBlockEntity(MovingPistonBlock.newMovingBlockEntity(pos, iblockdata2, (BlockState) this.defaultBlockState().setValue(PistonBaseBlock.FACING, Direction.from3DDataValue(data & 7)), enumdirection, false, true)); -+ world.setBlockEntity(MovingPistonBlock.newMovingBlockEntity(pos, iblockdata2, (BlockState) this.defaultBlockState().setValue(PistonBaseBlock.FACING, Direction.from3DDataValue(data & 7)), enumdirection, false, true)); // Paper - Protect Bedrock and End Portal/Frames from being destroyed; diff on change - world.blockUpdated(pos, iblockdata2.getBlock()); - iblockdata2.updateNeighbourShapes(world, pos, 2); - if (this.isSticky) { -@@ -289,7 +295,14 @@ public class PistonBaseBlock extends DirectionalBlock { - } - } - } else { -- world.removeBlock(pos.relative(enumdirection), false); -+ // Paper start - Protect Bedrock and End Portal/Frames from being destroyed; fix headless pistons breaking blocks -+ BlockPos headPos = pos.relative(enumdirection); -+ if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits || world.getBlockState(headPos) == Blocks.PISTON_HEAD.defaultBlockState().setValue(FACING, enumdirection)) { // double check to make sure we're not a headless piston. -+ world.removeBlock(headPos, false); -+ } else { -+ ((ServerLevel) world).getChunkSource().blockChanged(headPos); // ... fix client desync -+ } -+ // Paper end - Protect Bedrock and End Portal/Frames from being destroyed - } - - world.playSound((Player) null, pos, SoundEvents.PISTON_CONTRACT, SoundSource.BLOCKS, 0.5F, world.random.nextFloat() * 0.15F + 0.6F); -diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -index c7da359c525522b55763e594a1db0c26a026b73f..55efd0d379bac79935f62446cd3479d1e59361a4 100644 ---- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -+++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -@@ -174,7 +174,7 @@ public abstract class BlockBehaviour implements FeatureElement { - } - - protected void onExplosionHit(BlockState state, Level world, BlockPos pos, Explosion explosion, BiConsumer stackMerger) { -- if (!state.isAir() && explosion.getBlockInteraction() != Explosion.BlockInteraction.TRIGGER_BLOCK) { -+ if (!state.isAir() && explosion.getBlockInteraction() != Explosion.BlockInteraction.TRIGGER_BLOCK && state.isDestroyable()) { // Paper - Protect Bedrock and End Portal/Frames from being destroyed - Block block = state.getBlock(); - boolean flag = explosion.getIndirectSourceEntity() instanceof Player; - -@@ -254,7 +254,7 @@ public abstract class BlockBehaviour implements FeatureElement { - } - - protected boolean canBeReplaced(BlockState state, BlockPlaceContext context) { -- return state.canBeReplaced() && (context.getItemInHand().isEmpty() || !context.getItemInHand().is(this.asItem())); -+ return state.canBeReplaced() && (context.getItemInHand().isEmpty() || !context.getItemInHand().is(this.asItem())) && (state.isDestroyable() || (context.getPlayer() != null && context.getPlayer().getAbilities().instabuild)); // Paper - Protect Bedrock and End Portal/Frames from being destroyed - } - - protected boolean canBeReplaced(BlockState state, Fluid fluid) { -@@ -896,6 +896,12 @@ public abstract class BlockBehaviour implements FeatureElement { - return this.legacySolid; - } - -+ // Paper start - Protect Bedrock and End Portal/Frames from being destroyed -+ public final boolean isDestroyable() { -+ return getBlock().isDestroyable(); -+ } -+ // Paper end - Protect Bedrock and End Portal/Frames from being destroyed -+ - public boolean isValidSpawn(BlockGetter world, BlockPos pos, EntityType type) { - return this.getBlock().properties.isValidSpawn.test(this.asState(), world, pos, type); - } -@@ -999,7 +1005,7 @@ public abstract class BlockBehaviour implements FeatureElement { - } - - public PushReaction getPistonPushReaction() { -- return this.pushReaction; -+ return !this.isDestroyable() ? PushReaction.BLOCK : this.pushReaction; // Paper - Protect Bedrock and End Portal/Frames from being destroyed - } - - public boolean isSolidRender(BlockGetter world, BlockPos pos) { -diff --git a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java -index 03dd833d61d5152af3032f23dd1fc4c75da9bc4f..a61959700d5e00739a79eaa617ac383160335f26 100644 ---- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java -+++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java -@@ -221,6 +221,13 @@ public class PortalForcer { - for (int j = -1; j < 3; ++j) { - for (int k = -1; k < 4; ++k) { - temp.setWithOffset(pos, portalDirection.getStepX() * j + enumdirection1.getStepX() * distanceOrthogonalToPortal, k, portalDirection.getStepZ() * j + enumdirection1.getStepZ() * distanceOrthogonalToPortal); -+ // Paper start - Protect Bedrock and End Portal/Frames from being destroyed -+ if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits) { -+ if (!this.level.getBlockState(temp).isDestroyable()) { -+ return false; -+ } -+ } -+ // Paper end - Protect Bedrock and End Portal/Frames from being destroyed - if (k < 0 && !this.level.getBlockState(temp).isSolid()) { - return false; - } diff --git a/patches/server/1005-Use-distance-map-to-optimise-entity-tracker.patch b/patches/server/1005-Use-distance-map-to-optimise-entity-tracker.patch new file mode 100644 index 0000000000..22535d896b --- /dev/null +++ b/patches/server/1005-Use-distance-map-to-optimise-entity-tracker.patch @@ -0,0 +1,341 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Tue, 5 May 2020 20:18:05 -0700 +Subject: [PATCH] Use distance map to optimise entity tracker + +Use the distance map to find candidate players for tracking. + +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index 4621c33ed73b0db64e78e7b9be7013a2ba7393c8..48f7997e8a20f5a5a77516cbde990d0aacc2078a 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -149,6 +149,23 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + // Paper start - distance maps + private final com.destroystokyo.paper.util.misc.PooledLinkedHashSets pooledLinkedPlayerHashSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>(); ++ // Paper start - use distance map to optimise tracker ++ public static boolean isLegacyTrackingEntity(Entity entity) { ++ return entity.isLegacyTrackingEntity; ++ } ++ ++ // inlined EnumMap, TrackingRange.TrackingRangeType ++ static final org.spigotmc.TrackingRange.TrackingRangeType[] TRACKING_RANGE_TYPES = org.spigotmc.TrackingRange.TrackingRangeType.values(); ++ public final com.destroystokyo.paper.util.misc.PlayerAreaMap[] playerEntityTrackerTrackMaps; ++ final int[] entityTrackerTrackRanges; ++ public final int getEntityTrackerRange(final int ordinal) { ++ return this.entityTrackerTrackRanges[ordinal]; ++ } ++ ++ private int convertSpigotRangeToVanilla(final int vanilla) { ++ return net.minecraft.server.MinecraftServer.getServer().getScaledTrackingDistance(vanilla); ++ } ++ // Paper end - use distance map to optimise tracker + + void addPlayerToDistanceMaps(ServerPlayer player) { + int chunkX = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getX()); +@@ -156,6 +173,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // Note: players need to be explicitly added to distance maps before they can be updated + this.nearbyPlayers.addPlayer(player); + this.level.playerChunkLoader.addPlayer(player); // Paper - replace chunk loader ++ // Paper start - use distance map to optimise entity tracker ++ for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) { ++ com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i]; ++ int trackRange = this.entityTrackerTrackRanges[i]; ++ ++ trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(player))); ++ } ++ // Paper end - use distance map to optimise entity tracker + } + + void removePlayerFromDistanceMaps(ServerPlayer player) { +@@ -164,6 +189,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // Note: players need to be explicitly added to distance maps before they can be updated + this.nearbyPlayers.removePlayer(player); + this.level.playerChunkLoader.removePlayer(player); // Paper - replace chunk loader ++ // 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); ++ } ++ // Paper end - use distance map to optimise tracker + } + + void updateMaps(ServerPlayer player) { +@@ -172,6 +202,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // Note: players need to be explicitly added to distance maps before they can be updated + this.nearbyPlayers.tickPlayer(player); + this.level.playerChunkLoader.updatePlayer(player); // Paper - replace chunk loader ++ // Paper start - use distance map to optimise entity tracker ++ for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) { ++ com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i]; ++ int trackRange = this.entityTrackerTrackRanges[i]; ++ ++ trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(player))); ++ } ++ // Paper end - use distance map to optimise entity tracker + } + // Paper end + // Paper start +@@ -258,6 +296,48 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.regionManagers.add(this.dataRegionManager); + this.nearbyPlayers = new io.papermc.paper.util.player.NearbyPlayers(this.level); + // Paper end ++ // Paper start - use distance map to optimise entity tracker ++ this.playerEntityTrackerTrackMaps = new com.destroystokyo.paper.util.misc.PlayerAreaMap[TRACKING_RANGE_TYPES.length]; ++ this.entityTrackerTrackRanges = new int[TRACKING_RANGE_TYPES.length]; ++ ++ org.spigotmc.SpigotWorldConfig spigotWorldConfig = this.level.spigotConfig; ++ ++ for (int ordinal = 0, len = TRACKING_RANGE_TYPES.length; ordinal < len; ++ordinal) { ++ org.spigotmc.TrackingRange.TrackingRangeType trackingRangeType = TRACKING_RANGE_TYPES[ordinal]; ++ int configuredSpigotValue; ++ switch (trackingRangeType) { ++ case PLAYER: ++ configuredSpigotValue = spigotWorldConfig.playerTrackingRange; ++ break; ++ case ANIMAL: ++ configuredSpigotValue = spigotWorldConfig.animalTrackingRange; ++ break; ++ case MONSTER: ++ configuredSpigotValue = spigotWorldConfig.monsterTrackingRange; ++ break; ++ case MISC: ++ configuredSpigotValue = spigotWorldConfig.miscTrackingRange; ++ break; ++ case OTHER: ++ configuredSpigotValue = spigotWorldConfig.otherTrackingRange; ++ break; ++ case ENDERDRAGON: ++ configuredSpigotValue = EntityType.ENDER_DRAGON.clientTrackingRange() * 16; ++ break; ++ case DISPLAY: ++ configuredSpigotValue = spigotWorldConfig.displayTrackingRange; ++ break; ++ default: ++ throw new IllegalStateException("Missing case for enum " + trackingRangeType); ++ } ++ configuredSpigotValue = convertSpigotRangeToVanilla(configuredSpigotValue); ++ ++ int trackRange = (configuredSpigotValue >>> 4) + ((configuredSpigotValue & 15) != 0 ? 1 : 0); ++ this.entityTrackerTrackRanges[ordinal] = trackRange; ++ ++ this.playerEntityTrackerTrackMaps[ordinal] = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets); ++ } ++ // Paper end - use distance map to optimise entity tracker + } + + // Paper start +@@ -951,17 +1031,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + public void move(ServerPlayer player) { +- ObjectIterator objectiterator = this.entityMap.values().iterator(); +- +- while (objectiterator.hasNext()) { +- ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); +- +- if (playerchunkmap_entitytracker.entity == player) { +- playerchunkmap_entitytracker.updatePlayers(this.level.players()); +- } else { +- playerchunkmap_entitytracker.updatePlayer(player); +- } +- } ++ // Paper - delay this logic for the entity tracker tick, no need to duplicate it + + SectionPos sectionposition = player.getLastSectionPos(); + SectionPos sectionposition1 = SectionPos.of((EntityAccess) player); +@@ -1038,7 +1108,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + + entity.tracker = playerchunkmap_entitytracker; // Paper - Fast access to tracker + this.entityMap.put(entity.getId(), playerchunkmap_entitytracker); +- playerchunkmap_entitytracker.updatePlayers(this.level.players()); ++ playerchunkmap_entitytracker.updatePlayers(entity.getPlayersInTrackRange()); // Paper - don't search all players + if (entity instanceof ServerPlayer) { + ServerPlayer entityplayer = (ServerPlayer) entity; + +@@ -1080,9 +1150,37 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + entity.tracker = null; // Paper - We're no longer tracked + } + +- protected void tick() { +- // Paper - replaced by PlayerChunkLoader ++ // Paper start - optimised tracker ++ private final void processTrackQueue() { ++ this.level.timings.tracker1.startTiming(); ++ try { ++ for (TrackedEntity tracker : this.entityMap.values()) { ++ // update tracker entry ++ tracker.updatePlayers(tracker.entity.getPlayersInTrackRange()); ++ } ++ } finally { ++ this.level.timings.tracker1.stopTiming(); ++ } ++ ++ ++ this.level.timings.tracker2.startTiming(); ++ try { ++ for (TrackedEntity tracker : this.entityMap.values()) { ++ tracker.serverEntity.sendChanges(); ++ } ++ } finally { ++ this.level.timings.tracker2.stopTiming(); ++ } ++ } ++ // Paper end - optimised tracker + ++ protected void tick() { ++ // Paper start - optimized tracker ++ if (true) { ++ this.processTrackQueue(); ++ return; ++ } ++ // Paper end - optimized tracker + List list = Lists.newArrayList(); + List list1 = this.level.players(); + ObjectIterator objectiterator = this.entityMap.values().iterator(); +@@ -1230,6 +1328,42 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.lastSectionPos = SectionPos.of((EntityAccess) entity); + } + ++ // Paper start - use distance map to optimise tracker ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet lastTrackerCandidates; ++ ++ final void updatePlayers(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newTrackerCandidates) { ++ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet oldTrackerCandidates = this.lastTrackerCandidates; ++ this.lastTrackerCandidates = newTrackerCandidates; ++ ++ if (newTrackerCandidates != null) { ++ Object[] rawData = newTrackerCandidates.getBackingSet(); ++ for (int i = 0, len = rawData.length; i < len; ++i) { ++ Object raw = rawData[i]; ++ if (!(raw instanceof ServerPlayer)) { ++ continue; ++ } ++ ServerPlayer player = (ServerPlayer)raw; ++ this.updatePlayer(player); ++ } ++ } ++ ++ if (oldTrackerCandidates == newTrackerCandidates) { ++ // this is likely the case. ++ // means there has been no range changes, so we can just use the above for tracking. ++ return; ++ } ++ ++ // stuff could have been removed, so we need to check the trackedPlayers set ++ // for players that were removed ++ ++ for (ServerPlayerConnection conn : this.seenBy.toArray(new ServerPlayerConnection[0])) { // avoid CME ++ if (newTrackerCandidates == null || !newTrackerCandidates.contains(conn.getPlayer())) { ++ this.updatePlayer(conn.getPlayer()); ++ } ++ } ++ } ++ // Paper end - use distance map to optimise tracker ++ + public boolean equals(Object object) { + return object instanceof ChunkMap.TrackedEntity ? ((ChunkMap.TrackedEntity) object).entity.getId() == this.entity.getId() : false; + } +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index b57644317b5085d74d11ac6ba858c3747d703a47..257943b4c984d6faee29eca17c8f951e7f43168b 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -59,6 +59,7 @@ import net.minecraft.network.syncher.SyncedDataHolder; + import net.minecraft.network.syncher.SynchedEntityData; + import net.minecraft.resources.ResourceKey; + import net.minecraft.resources.ResourceLocation; ++import io.papermc.paper.util.MCUtil; + import net.minecraft.server.MinecraftServer; + import net.minecraft.server.level.ServerLevel; + import net.minecraft.server.level.ServerPlayer; +@@ -471,6 +472,38 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.teleportTo(worldserver, null); + } + // Paper end - make end portalling safe ++ // Paper start - optimise entity tracking ++ final org.spigotmc.TrackingRange.TrackingRangeType trackingRangeType = org.spigotmc.TrackingRange.getTrackingRangeType(this); ++ ++ public boolean isLegacyTrackingEntity = false; ++ ++ public final void setLegacyTrackingEntity(final boolean isLegacyTrackingEntity) { ++ this.isLegacyTrackingEntity = isLegacyTrackingEntity; ++ } ++ ++ public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getPlayersInTrackRange() { ++ // determine highest range of passengers ++ if (this.passengers.isEmpty()) { ++ return ((ServerLevel)this.level).getChunkSource().chunkMap.playerEntityTrackerTrackMaps[this.trackingRangeType.ordinal()] ++ .getObjectsInRange(MCUtil.getCoordinateKey(this)); ++ } ++ Iterable passengers = this.getIndirectPassengers(); ++ net.minecraft.server.level.ChunkMap chunkMap = ((ServerLevel)this.level).getChunkSource().chunkMap; ++ org.spigotmc.TrackingRange.TrackingRangeType type = this.trackingRangeType; ++ int range = chunkMap.getEntityTrackerRange(type.ordinal()); ++ ++ for (Entity passenger : passengers) { ++ org.spigotmc.TrackingRange.TrackingRangeType passengerType = passenger.trackingRangeType; ++ int passengerRange = chunkMap.getEntityTrackerRange(passengerType.ordinal()); ++ if (passengerRange > range) { ++ type = passengerType; ++ range = passengerRange; ++ } ++ } ++ ++ return chunkMap.playerEntityTrackerTrackMaps[type.ordinal()].getObjectsInRange(MCUtil.getCoordinateKey(this)); ++ } ++ // Paper end - optimise entity tracking + public float getBukkitYaw() { + return this.yRot; + } +diff --git a/src/main/java/org/spigotmc/TrackingRange.java b/src/main/java/org/spigotmc/TrackingRange.java +index bb06f89a29f30144e7e2113e088a503db006a83c..e4425b242fe73d1fd2bd10c313aa16925432329f 100644 +--- a/src/main/java/org/spigotmc/TrackingRange.java ++++ b/src/main/java/org/spigotmc/TrackingRange.java +@@ -55,4 +55,48 @@ public class TrackingRange + return config.otherTrackingRange; + } + } ++ ++ // Paper start - optimise entity tracking ++ // copied from above, TODO check on update ++ public static TrackingRangeType getTrackingRangeType(Entity entity) ++ { ++ if (entity instanceof net.minecraft.world.entity.boss.enderdragon.EnderDragon) return TrackingRangeType.ENDERDRAGON; // Paper - enderdragon is exempt ++ if ( entity instanceof ServerPlayer ) ++ { ++ return TrackingRangeType.PLAYER; ++ // Paper start - Simplify and set water mobs to animal tracking range ++ } ++ switch (entity.activationType) { ++ case RAIDER: ++ case MONSTER: ++ case FLYING_MONSTER: ++ return TrackingRangeType.MONSTER; ++ case WATER: ++ case VILLAGER: ++ case ANIMAL: ++ return TrackingRangeType.ANIMAL; ++ case MISC: ++ } ++ if ( entity instanceof ItemFrame || entity instanceof Painting || entity instanceof ItemEntity || entity instanceof ExperienceOrb ) ++ // Paper end ++ { ++ return TrackingRangeType.MISC; ++ } else if (entity instanceof Display) { ++ return TrackingRangeType.DISPLAY; ++ } else ++ { ++ return TrackingRangeType.OTHER; ++ } ++ } ++ ++ public static enum TrackingRangeType { ++ PLAYER, ++ ANIMAL, ++ MONSTER, ++ MISC, ++ OTHER, ++ ENDERDRAGON, ++ DISPLAY; ++ } ++ // Paper end - optimise entity tracking + } diff --git a/patches/server/1006-Optimize-Bit-Operations-by-inlining.patch b/patches/server/1006-Optimize-Bit-Operations-by-inlining.patch new file mode 100644 index 0000000000..a1f24dd8dc --- /dev/null +++ b/patches/server/1006-Optimize-Bit-Operations-by-inlining.patch @@ -0,0 +1,213 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Thu, 4 Jun 2020 02:24:49 -0400 +Subject: [PATCH] Optimize Bit Operations by inlining + +Inline bit operations and reduce instruction count to make these hot +operations faster + +diff --git a/src/main/java/net/minecraft/core/BlockPos.java b/src/main/java/net/minecraft/core/BlockPos.java +index 4144c872fbd89d22827ad1f586e9a8d63a39ed46..665e88b2dedf9d5bb50914d5f3d377f2d19f40b0 100644 +--- a/src/main/java/net/minecraft/core/BlockPos.java ++++ b/src/main/java/net/minecraft/core/BlockPos.java +@@ -50,15 +50,16 @@ public class BlockPos extends Vec3i { + }; + private static final Logger LOGGER = LogUtils.getLogger(); + public static final BlockPos ZERO = new BlockPos(0, 0, 0); +- private static final int PACKED_X_LENGTH = 1 + Mth.log2(Mth.smallestEncompassingPowerOfTwo(30000000)); +- private static final int PACKED_Z_LENGTH = PACKED_X_LENGTH; +- public static final int PACKED_Y_LENGTH = 64 - PACKED_X_LENGTH - PACKED_Z_LENGTH; +- private static final long PACKED_X_MASK = (1L << PACKED_X_LENGTH) - 1L; +- private static final long PACKED_Y_MASK = (1L << PACKED_Y_LENGTH) - 1L; +- private static final long PACKED_Z_MASK = (1L << PACKED_Z_LENGTH) - 1L; +- private static final int Y_OFFSET = 0; +- private static final int Z_OFFSET = PACKED_Y_LENGTH; +- private static final int X_OFFSET = PACKED_Y_LENGTH + PACKED_Z_LENGTH; ++ // Paper start - Optimize Bit Operations by inlining ++ private static final int PACKED_X_LENGTH = 26; ++ private static final int PACKED_Z_LENGTH = 26; ++ public static final int PACKED_Y_LENGTH = 12; ++ private static final long PACKED_X_MASK = 67108863; ++ private static final long PACKED_Y_MASK = 4095; ++ private static final long PACKED_Z_MASK = 67108863; ++ private static final int Z_OFFSET = 12; ++ private static final int X_OFFSET = 38; ++ // Paper end - Optimize Bit Operations by inlining + + public BlockPos(int x, int y, int z) { + super(x, y, z); +@@ -68,28 +69,29 @@ public class BlockPos extends Vec3i { + this(pos.getX(), pos.getY(), pos.getZ()); + } + ++ public static long getAdjacent(int baseX, int baseY, int baseZ, Direction enumdirection) { return asLong(baseX + enumdirection.getStepX(), baseY + enumdirection.getStepY(), baseZ + enumdirection.getStepZ()); } // Paper + public static long offset(long value, Direction direction) { + return offset(value, direction.getStepX(), direction.getStepY(), direction.getStepZ()); + } + + public static long offset(long value, int x, int y, int z) { +- return asLong(getX(value) + x, getY(value) + y, getZ(value) + z); ++ return asLong((int) (value >> 38) + x, (int) ((value << 52) >> 52) + y, (int) ((value << 26) >> 38) + z); // Paper - simplify/inline + } + + public static int getX(long packedPos) { +- return (int)(packedPos << 64 - X_OFFSET - PACKED_X_LENGTH >> 64 - PACKED_X_LENGTH); ++ return (int) (packedPos >> 38); // Paper - simplify/inline + } + + public static int getY(long packedPos) { +- return (int)(packedPos << 64 - PACKED_Y_LENGTH >> 64 - PACKED_Y_LENGTH); ++ return (int) ((packedPos << 52) >> 52); // Paper - simplify/inline + } + + public static int getZ(long packedPos) { +- return (int)(packedPos << 64 - Z_OFFSET - PACKED_Z_LENGTH >> 64 - PACKED_Z_LENGTH); ++ return (int) ((packedPos << 26) >> 38); // Paper - simplify/inline + } + + public static BlockPos of(long packedPos) { +- return new BlockPos(getX(packedPos), getY(packedPos), getZ(packedPos)); ++ return new BlockPos((int) (packedPos >> 38), (int) ((packedPos << 52) >> 52), (int) ((packedPos << 26) >> 38)); // Paper - simplify/inline + } + + public static BlockPos containing(double x, double y, double z) { +@@ -113,10 +115,7 @@ public class BlockPos extends Vec3i { + } + + public static long asLong(int x, int y, int z) { +- long l = 0L; +- l |= ((long)x & PACKED_X_MASK) << X_OFFSET; +- l |= ((long)y & PACKED_Y_MASK) << 0; +- return l | ((long)z & PACKED_Z_MASK) << Z_OFFSET; ++ return (((long) x & (long) 67108863) << 38) | (((long) y & (long) 4095)) | (((long) z & (long) 67108863) << 12); // Paper - inline constants and simplify + } + + public static long getFlatIndex(long y) { +diff --git a/src/main/java/net/minecraft/core/SectionPos.java b/src/main/java/net/minecraft/core/SectionPos.java +index 27e0d53d5893a13a340deddc93a1128968db7e5b..fe3577e533fb829c85fd4881b1bcca3b70aaf1a5 100644 +--- a/src/main/java/net/minecraft/core/SectionPos.java ++++ b/src/main/java/net/minecraft/core/SectionPos.java +@@ -38,7 +38,7 @@ public class SectionPos extends Vec3i { + } + + public static SectionPos of(BlockPos pos) { +- return new SectionPos(blockToSectionCoord(pos.getX()), blockToSectionCoord(pos.getY()), blockToSectionCoord(pos.getZ())); ++ return new SectionPos(pos.getX() >> 4, pos.getY() >> 4, pos.getZ() >> 4); // Paper + } + + public static SectionPos of(ChunkPos chunkPos, int y) { +@@ -54,7 +54,7 @@ public class SectionPos extends Vec3i { + } + + public static SectionPos of(long packed) { +- return new SectionPos(x(packed), y(packed), z(packed)); ++ return new SectionPos((int) (packed >> 42), (int) (packed << 44 >> 44), (int) (packed << 22 >> 42)); // Paper + } + + public static SectionPos bottomOf(ChunkAccess chunk) { +@@ -65,8 +65,16 @@ public class SectionPos extends Vec3i { + return offset(packed, direction.getStepX(), direction.getStepY(), direction.getStepZ()); + } + ++ // Paper start ++ public static long getAdjacentFromBlockPos(int x, int y, int z, Direction enumdirection) { ++ return (((long) ((x >> 4) + enumdirection.getStepX()) & 4194303L) << 42) | (((long) ((y >> 4) + enumdirection.getStepY()) & 1048575L)) | (((long) ((z >> 4) + enumdirection.getStepZ()) & 4194303L) << 20); ++ } ++ public static long getAdjacentFromSectionPos(int x, int y, int z, Direction enumdirection) { ++ return (((long) (x + enumdirection.getStepX()) & 4194303L) << 42) | (((long) ((y) + enumdirection.getStepY()) & 1048575L)) | (((long) (z + enumdirection.getStepZ()) & 4194303L) << 20); ++ } ++ // Paper end + public static long offset(long packed, int x, int y, int z) { +- return asLong(x(packed) + x, y(packed) + y, z(packed) + z); ++ return (((long) ((int) (packed >> 42) + x) & 4194303L) << 42) | (((long) ((int) (packed << 44 >> 44) + y) & 1048575L)) | (((long) ((int) (packed << 22 >> 42) + z) & 4194303L) << 20); // Simplify to reduce instruction count + } + + public static int posToSectionCoord(double coord) { +@@ -86,10 +94,7 @@ public class SectionPos extends Vec3i { + } + + public static short sectionRelativePos(BlockPos pos) { +- int i = sectionRelative(pos.getX()); +- int j = sectionRelative(pos.getY()); +- int k = sectionRelative(pos.getZ()); +- return (short)(i << 8 | k << 4 | j << 0); ++ return (short) ((pos.getX() & 15) << 8 | (pos.getZ() & 15) << 4 | pos.getY() & 15); // Paper - simplify/inline + } + + public static int sectionRelativeX(short packedLocalPos) { +@@ -152,16 +157,16 @@ public class SectionPos extends Vec3i { + return this.getZ(); + } + +- public int minBlockX() { +- return sectionToBlockCoord(this.x()); ++ public final int minBlockX() { // Paper - make final ++ return this.getX() << 4; // Paper - inline + } + +- public int minBlockY() { +- return sectionToBlockCoord(this.y()); ++ public final int minBlockY() { // Paper - make final ++ return this.getY() << 4; // Paper - inline + } + +- public int minBlockZ() { +- return sectionToBlockCoord(this.z()); ++ public int minBlockZ() { // Paper - make final ++ return this.getZ() << 4; // Paper - inline + } + + public int maxBlockX() { +@@ -177,7 +182,8 @@ public class SectionPos extends Vec3i { + } + + public static long blockToSection(long blockPos) { +- return asLong(blockToSectionCoord(BlockPos.getX(blockPos)), blockToSectionCoord(BlockPos.getY(blockPos)), blockToSectionCoord(BlockPos.getZ(blockPos))); ++ // b(a(BlockPosition.b(i)), a(BlockPosition.c(i)), a(BlockPosition.d(i))); ++ return (((long) (int) (blockPos >> 42) & 4194303L) << 42) | (((long) (int) ((blockPos << 52) >> 56) & 1048575L)) | (((long) (int) ((blockPos << 26) >> 42) & 4194303L) << 20); // Simplify to reduce instruction count + } + + public static long getZeroNode(int x, int z) { +@@ -205,15 +211,18 @@ public class SectionPos extends Vec3i { + return asLong(blockToSectionCoord(pos.getX()), blockToSectionCoord(pos.getY()), blockToSectionCoord(pos.getZ())); + } + ++ // Paper start ++ public static long blockPosAsSectionLong(int i, int j, int k) { ++ return (((long) (i >> 4) & 4194303L) << 42) | (((long) (j >> 4) & 1048575L)) | (((long) (k >> 4) & 4194303L) << 20); ++ } ++ // Paper end ++ + public static long asLong(int x, int y, int z) { +- long l = 0L; +- l |= ((long)x & 4194303L) << 42; +- l |= ((long)y & 1048575L) << 0; +- return l | ((long)z & 4194303L) << 20; ++ return (((long) x & 4194303L) << 42) | (((long) y & 1048575L)) | (((long) z & 4194303L) << 20); // Paper - Simplify to reduce instruction count + } + + public long asLong() { +- return asLong(this.x(), this.y(), this.z()); ++ return (((long) getX() & 4194303L) << 42) | (((long) getY() & 1048575L)) | (((long) getZ() & 4194303L) << 20); // Paper - Simplify to reduce instruction count + } + + @Override +@@ -226,16 +235,11 @@ public class SectionPos extends Vec3i { + } + + public static Stream cube(SectionPos center, int radius) { +- int i = center.x(); +- int j = center.y(); +- int k = center.z(); +- return betweenClosedStream(i - radius, j - radius, k - radius, i + radius, j + radius, k + radius); ++ return betweenClosedStream(center.getX() - radius, center.getY() - radius, center.getZ() - radius, center.getX() + radius, center.getY() + radius, center.getZ() + radius); // Paper - simplify/inline + } + + public static Stream aroundChunk(ChunkPos center, int radius, int minY, int maxY) { +- int i = center.x; +- int j = center.z; +- return betweenClosedStream(i - radius, minY, j - radius, i + radius, maxY - 1, j + radius); ++ return betweenClosedStream(center.x - radius, 0, center.z - radius, center.x + radius, 15, center.z + radius); // Paper - simplify/inline + } + + public static Stream betweenClosedStream(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { diff --git a/patches/server/1006-Use-distance-map-to-optimise-entity-tracker.patch b/patches/server/1006-Use-distance-map-to-optimise-entity-tracker.patch deleted file mode 100644 index 22535d896b..0000000000 --- a/patches/server/1006-Use-distance-map-to-optimise-entity-tracker.patch +++ /dev/null @@ -1,341 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Tue, 5 May 2020 20:18:05 -0700 -Subject: [PATCH] Use distance map to optimise entity tracker - -Use the distance map to find candidate players for tracking. - -diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java -index 4621c33ed73b0db64e78e7b9be7013a2ba7393c8..48f7997e8a20f5a5a77516cbde990d0aacc2078a 100644 ---- a/src/main/java/net/minecraft/server/level/ChunkMap.java -+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java -@@ -149,6 +149,23 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - - // Paper start - distance maps - private final com.destroystokyo.paper.util.misc.PooledLinkedHashSets pooledLinkedPlayerHashSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>(); -+ // Paper start - use distance map to optimise tracker -+ public static boolean isLegacyTrackingEntity(Entity entity) { -+ return entity.isLegacyTrackingEntity; -+ } -+ -+ // inlined EnumMap, TrackingRange.TrackingRangeType -+ static final org.spigotmc.TrackingRange.TrackingRangeType[] TRACKING_RANGE_TYPES = org.spigotmc.TrackingRange.TrackingRangeType.values(); -+ public final com.destroystokyo.paper.util.misc.PlayerAreaMap[] playerEntityTrackerTrackMaps; -+ final int[] entityTrackerTrackRanges; -+ public final int getEntityTrackerRange(final int ordinal) { -+ return this.entityTrackerTrackRanges[ordinal]; -+ } -+ -+ private int convertSpigotRangeToVanilla(final int vanilla) { -+ return net.minecraft.server.MinecraftServer.getServer().getScaledTrackingDistance(vanilla); -+ } -+ // Paper end - use distance map to optimise tracker - - void addPlayerToDistanceMaps(ServerPlayer player) { - int chunkX = io.papermc.paper.util.MCUtil.getChunkCoordinate(player.getX()); -@@ -156,6 +173,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - // Note: players need to be explicitly added to distance maps before they can be updated - this.nearbyPlayers.addPlayer(player); - this.level.playerChunkLoader.addPlayer(player); // Paper - replace chunk loader -+ // Paper start - use distance map to optimise entity tracker -+ for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) { -+ com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i]; -+ int trackRange = this.entityTrackerTrackRanges[i]; -+ -+ trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(player))); -+ } -+ // Paper end - use distance map to optimise entity tracker - } - - void removePlayerFromDistanceMaps(ServerPlayer player) { -@@ -164,6 +189,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - // Note: players need to be explicitly added to distance maps before they can be updated - this.nearbyPlayers.removePlayer(player); - this.level.playerChunkLoader.removePlayer(player); // Paper - replace chunk loader -+ // 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); -+ } -+ // Paper end - use distance map to optimise tracker - } - - void updateMaps(ServerPlayer player) { -@@ -172,6 +202,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - // Note: players need to be explicitly added to distance maps before they can be updated - this.nearbyPlayers.tickPlayer(player); - this.level.playerChunkLoader.updatePlayer(player); // Paper - replace chunk loader -+ // Paper start - use distance map to optimise entity tracker -+ for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) { -+ com.destroystokyo.paper.util.misc.PlayerAreaMap trackMap = this.playerEntityTrackerTrackMaps[i]; -+ int trackRange = this.entityTrackerTrackRanges[i]; -+ -+ trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(player))); -+ } -+ // Paper end - use distance map to optimise entity tracker - } - // Paper end - // Paper start -@@ -258,6 +296,48 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - this.regionManagers.add(this.dataRegionManager); - this.nearbyPlayers = new io.papermc.paper.util.player.NearbyPlayers(this.level); - // Paper end -+ // Paper start - use distance map to optimise entity tracker -+ this.playerEntityTrackerTrackMaps = new com.destroystokyo.paper.util.misc.PlayerAreaMap[TRACKING_RANGE_TYPES.length]; -+ this.entityTrackerTrackRanges = new int[TRACKING_RANGE_TYPES.length]; -+ -+ org.spigotmc.SpigotWorldConfig spigotWorldConfig = this.level.spigotConfig; -+ -+ for (int ordinal = 0, len = TRACKING_RANGE_TYPES.length; ordinal < len; ++ordinal) { -+ org.spigotmc.TrackingRange.TrackingRangeType trackingRangeType = TRACKING_RANGE_TYPES[ordinal]; -+ int configuredSpigotValue; -+ switch (trackingRangeType) { -+ case PLAYER: -+ configuredSpigotValue = spigotWorldConfig.playerTrackingRange; -+ break; -+ case ANIMAL: -+ configuredSpigotValue = spigotWorldConfig.animalTrackingRange; -+ break; -+ case MONSTER: -+ configuredSpigotValue = spigotWorldConfig.monsterTrackingRange; -+ break; -+ case MISC: -+ configuredSpigotValue = spigotWorldConfig.miscTrackingRange; -+ break; -+ case OTHER: -+ configuredSpigotValue = spigotWorldConfig.otherTrackingRange; -+ break; -+ case ENDERDRAGON: -+ configuredSpigotValue = EntityType.ENDER_DRAGON.clientTrackingRange() * 16; -+ break; -+ case DISPLAY: -+ configuredSpigotValue = spigotWorldConfig.displayTrackingRange; -+ break; -+ default: -+ throw new IllegalStateException("Missing case for enum " + trackingRangeType); -+ } -+ configuredSpigotValue = convertSpigotRangeToVanilla(configuredSpigotValue); -+ -+ int trackRange = (configuredSpigotValue >>> 4) + ((configuredSpigotValue & 15) != 0 ? 1 : 0); -+ this.entityTrackerTrackRanges[ordinal] = trackRange; -+ -+ this.playerEntityTrackerTrackMaps[ordinal] = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets); -+ } -+ // Paper end - use distance map to optimise entity tracker - } - - // Paper start -@@ -951,17 +1031,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - public void move(ServerPlayer player) { -- ObjectIterator objectiterator = this.entityMap.values().iterator(); -- -- while (objectiterator.hasNext()) { -- ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); -- -- if (playerchunkmap_entitytracker.entity == player) { -- playerchunkmap_entitytracker.updatePlayers(this.level.players()); -- } else { -- playerchunkmap_entitytracker.updatePlayer(player); -- } -- } -+ // Paper - delay this logic for the entity tracker tick, no need to duplicate it - - SectionPos sectionposition = player.getLastSectionPos(); - SectionPos sectionposition1 = SectionPos.of((EntityAccess) player); -@@ -1038,7 +1108,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - - entity.tracker = playerchunkmap_entitytracker; // Paper - Fast access to tracker - this.entityMap.put(entity.getId(), playerchunkmap_entitytracker); -- playerchunkmap_entitytracker.updatePlayers(this.level.players()); -+ playerchunkmap_entitytracker.updatePlayers(entity.getPlayersInTrackRange()); // Paper - don't search all players - if (entity instanceof ServerPlayer) { - ServerPlayer entityplayer = (ServerPlayer) entity; - -@@ -1080,9 +1150,37 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - entity.tracker = null; // Paper - We're no longer tracked - } - -- protected void tick() { -- // Paper - replaced by PlayerChunkLoader -+ // Paper start - optimised tracker -+ private final void processTrackQueue() { -+ this.level.timings.tracker1.startTiming(); -+ try { -+ for (TrackedEntity tracker : this.entityMap.values()) { -+ // update tracker entry -+ tracker.updatePlayers(tracker.entity.getPlayersInTrackRange()); -+ } -+ } finally { -+ this.level.timings.tracker1.stopTiming(); -+ } -+ -+ -+ this.level.timings.tracker2.startTiming(); -+ try { -+ for (TrackedEntity tracker : this.entityMap.values()) { -+ tracker.serverEntity.sendChanges(); -+ } -+ } finally { -+ this.level.timings.tracker2.stopTiming(); -+ } -+ } -+ // Paper end - optimised tracker - -+ protected void tick() { -+ // Paper start - optimized tracker -+ if (true) { -+ this.processTrackQueue(); -+ return; -+ } -+ // Paper end - optimized tracker - List list = Lists.newArrayList(); - List list1 = this.level.players(); - ObjectIterator objectiterator = this.entityMap.values().iterator(); -@@ -1230,6 +1328,42 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - this.lastSectionPos = SectionPos.of((EntityAccess) entity); - } - -+ // Paper start - use distance map to optimise tracker -+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet lastTrackerCandidates; -+ -+ final void updatePlayers(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newTrackerCandidates) { -+ com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet oldTrackerCandidates = this.lastTrackerCandidates; -+ this.lastTrackerCandidates = newTrackerCandidates; -+ -+ if (newTrackerCandidates != null) { -+ Object[] rawData = newTrackerCandidates.getBackingSet(); -+ for (int i = 0, len = rawData.length; i < len; ++i) { -+ Object raw = rawData[i]; -+ if (!(raw instanceof ServerPlayer)) { -+ continue; -+ } -+ ServerPlayer player = (ServerPlayer)raw; -+ this.updatePlayer(player); -+ } -+ } -+ -+ if (oldTrackerCandidates == newTrackerCandidates) { -+ // this is likely the case. -+ // means there has been no range changes, so we can just use the above for tracking. -+ return; -+ } -+ -+ // stuff could have been removed, so we need to check the trackedPlayers set -+ // for players that were removed -+ -+ for (ServerPlayerConnection conn : this.seenBy.toArray(new ServerPlayerConnection[0])) { // avoid CME -+ if (newTrackerCandidates == null || !newTrackerCandidates.contains(conn.getPlayer())) { -+ this.updatePlayer(conn.getPlayer()); -+ } -+ } -+ } -+ // Paper end - use distance map to optimise tracker -+ - public boolean equals(Object object) { - return object instanceof ChunkMap.TrackedEntity ? ((ChunkMap.TrackedEntity) object).entity.getId() == this.entity.getId() : false; - } -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index b57644317b5085d74d11ac6ba858c3747d703a47..257943b4c984d6faee29eca17c8f951e7f43168b 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -59,6 +59,7 @@ import net.minecraft.network.syncher.SyncedDataHolder; - import net.minecraft.network.syncher.SynchedEntityData; - import net.minecraft.resources.ResourceKey; - import net.minecraft.resources.ResourceLocation; -+import io.papermc.paper.util.MCUtil; - import net.minecraft.server.MinecraftServer; - import net.minecraft.server.level.ServerLevel; - import net.minecraft.server.level.ServerPlayer; -@@ -471,6 +472,38 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - this.teleportTo(worldserver, null); - } - // Paper end - make end portalling safe -+ // Paper start - optimise entity tracking -+ final org.spigotmc.TrackingRange.TrackingRangeType trackingRangeType = org.spigotmc.TrackingRange.getTrackingRangeType(this); -+ -+ public boolean isLegacyTrackingEntity = false; -+ -+ public final void setLegacyTrackingEntity(final boolean isLegacyTrackingEntity) { -+ this.isLegacyTrackingEntity = isLegacyTrackingEntity; -+ } -+ -+ public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getPlayersInTrackRange() { -+ // determine highest range of passengers -+ if (this.passengers.isEmpty()) { -+ return ((ServerLevel)this.level).getChunkSource().chunkMap.playerEntityTrackerTrackMaps[this.trackingRangeType.ordinal()] -+ .getObjectsInRange(MCUtil.getCoordinateKey(this)); -+ } -+ Iterable passengers = this.getIndirectPassengers(); -+ net.minecraft.server.level.ChunkMap chunkMap = ((ServerLevel)this.level).getChunkSource().chunkMap; -+ org.spigotmc.TrackingRange.TrackingRangeType type = this.trackingRangeType; -+ int range = chunkMap.getEntityTrackerRange(type.ordinal()); -+ -+ for (Entity passenger : passengers) { -+ org.spigotmc.TrackingRange.TrackingRangeType passengerType = passenger.trackingRangeType; -+ int passengerRange = chunkMap.getEntityTrackerRange(passengerType.ordinal()); -+ if (passengerRange > range) { -+ type = passengerType; -+ range = passengerRange; -+ } -+ } -+ -+ return chunkMap.playerEntityTrackerTrackMaps[type.ordinal()].getObjectsInRange(MCUtil.getCoordinateKey(this)); -+ } -+ // Paper end - optimise entity tracking - public float getBukkitYaw() { - return this.yRot; - } -diff --git a/src/main/java/org/spigotmc/TrackingRange.java b/src/main/java/org/spigotmc/TrackingRange.java -index bb06f89a29f30144e7e2113e088a503db006a83c..e4425b242fe73d1fd2bd10c313aa16925432329f 100644 ---- a/src/main/java/org/spigotmc/TrackingRange.java -+++ b/src/main/java/org/spigotmc/TrackingRange.java -@@ -55,4 +55,48 @@ public class TrackingRange - return config.otherTrackingRange; - } - } -+ -+ // Paper start - optimise entity tracking -+ // copied from above, TODO check on update -+ public static TrackingRangeType getTrackingRangeType(Entity entity) -+ { -+ if (entity instanceof net.minecraft.world.entity.boss.enderdragon.EnderDragon) return TrackingRangeType.ENDERDRAGON; // Paper - enderdragon is exempt -+ if ( entity instanceof ServerPlayer ) -+ { -+ return TrackingRangeType.PLAYER; -+ // Paper start - Simplify and set water mobs to animal tracking range -+ } -+ switch (entity.activationType) { -+ case RAIDER: -+ case MONSTER: -+ case FLYING_MONSTER: -+ return TrackingRangeType.MONSTER; -+ case WATER: -+ case VILLAGER: -+ case ANIMAL: -+ return TrackingRangeType.ANIMAL; -+ case MISC: -+ } -+ if ( entity instanceof ItemFrame || entity instanceof Painting || entity instanceof ItemEntity || entity instanceof ExperienceOrb ) -+ // Paper end -+ { -+ return TrackingRangeType.MISC; -+ } else if (entity instanceof Display) { -+ return TrackingRangeType.DISPLAY; -+ } else -+ { -+ return TrackingRangeType.OTHER; -+ } -+ } -+ -+ public static enum TrackingRangeType { -+ PLAYER, -+ ANIMAL, -+ MONSTER, -+ MISC, -+ OTHER, -+ ENDERDRAGON, -+ DISPLAY; -+ } -+ // Paper end - optimise entity tracking - } diff --git a/patches/server/1007-Optimize-Bit-Operations-by-inlining.patch b/patches/server/1007-Optimize-Bit-Operations-by-inlining.patch deleted file mode 100644 index a1f24dd8dc..0000000000 --- a/patches/server/1007-Optimize-Bit-Operations-by-inlining.patch +++ /dev/null @@ -1,213 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Thu, 4 Jun 2020 02:24:49 -0400 -Subject: [PATCH] Optimize Bit Operations by inlining - -Inline bit operations and reduce instruction count to make these hot -operations faster - -diff --git a/src/main/java/net/minecraft/core/BlockPos.java b/src/main/java/net/minecraft/core/BlockPos.java -index 4144c872fbd89d22827ad1f586e9a8d63a39ed46..665e88b2dedf9d5bb50914d5f3d377f2d19f40b0 100644 ---- a/src/main/java/net/minecraft/core/BlockPos.java -+++ b/src/main/java/net/minecraft/core/BlockPos.java -@@ -50,15 +50,16 @@ public class BlockPos extends Vec3i { - }; - private static final Logger LOGGER = LogUtils.getLogger(); - public static final BlockPos ZERO = new BlockPos(0, 0, 0); -- private static final int PACKED_X_LENGTH = 1 + Mth.log2(Mth.smallestEncompassingPowerOfTwo(30000000)); -- private static final int PACKED_Z_LENGTH = PACKED_X_LENGTH; -- public static final int PACKED_Y_LENGTH = 64 - PACKED_X_LENGTH - PACKED_Z_LENGTH; -- private static final long PACKED_X_MASK = (1L << PACKED_X_LENGTH) - 1L; -- private static final long PACKED_Y_MASK = (1L << PACKED_Y_LENGTH) - 1L; -- private static final long PACKED_Z_MASK = (1L << PACKED_Z_LENGTH) - 1L; -- private static final int Y_OFFSET = 0; -- private static final int Z_OFFSET = PACKED_Y_LENGTH; -- private static final int X_OFFSET = PACKED_Y_LENGTH + PACKED_Z_LENGTH; -+ // Paper start - Optimize Bit Operations by inlining -+ private static final int PACKED_X_LENGTH = 26; -+ private static final int PACKED_Z_LENGTH = 26; -+ public static final int PACKED_Y_LENGTH = 12; -+ private static final long PACKED_X_MASK = 67108863; -+ private static final long PACKED_Y_MASK = 4095; -+ private static final long PACKED_Z_MASK = 67108863; -+ private static final int Z_OFFSET = 12; -+ private static final int X_OFFSET = 38; -+ // Paper end - Optimize Bit Operations by inlining - - public BlockPos(int x, int y, int z) { - super(x, y, z); -@@ -68,28 +69,29 @@ public class BlockPos extends Vec3i { - this(pos.getX(), pos.getY(), pos.getZ()); - } - -+ public static long getAdjacent(int baseX, int baseY, int baseZ, Direction enumdirection) { return asLong(baseX + enumdirection.getStepX(), baseY + enumdirection.getStepY(), baseZ + enumdirection.getStepZ()); } // Paper - public static long offset(long value, Direction direction) { - return offset(value, direction.getStepX(), direction.getStepY(), direction.getStepZ()); - } - - public static long offset(long value, int x, int y, int z) { -- return asLong(getX(value) + x, getY(value) + y, getZ(value) + z); -+ return asLong((int) (value >> 38) + x, (int) ((value << 52) >> 52) + y, (int) ((value << 26) >> 38) + z); // Paper - simplify/inline - } - - public static int getX(long packedPos) { -- return (int)(packedPos << 64 - X_OFFSET - PACKED_X_LENGTH >> 64 - PACKED_X_LENGTH); -+ return (int) (packedPos >> 38); // Paper - simplify/inline - } - - public static int getY(long packedPos) { -- return (int)(packedPos << 64 - PACKED_Y_LENGTH >> 64 - PACKED_Y_LENGTH); -+ return (int) ((packedPos << 52) >> 52); // Paper - simplify/inline - } - - public static int getZ(long packedPos) { -- return (int)(packedPos << 64 - Z_OFFSET - PACKED_Z_LENGTH >> 64 - PACKED_Z_LENGTH); -+ return (int) ((packedPos << 26) >> 38); // Paper - simplify/inline - } - - public static BlockPos of(long packedPos) { -- return new BlockPos(getX(packedPos), getY(packedPos), getZ(packedPos)); -+ return new BlockPos((int) (packedPos >> 38), (int) ((packedPos << 52) >> 52), (int) ((packedPos << 26) >> 38)); // Paper - simplify/inline - } - - public static BlockPos containing(double x, double y, double z) { -@@ -113,10 +115,7 @@ public class BlockPos extends Vec3i { - } - - public static long asLong(int x, int y, int z) { -- long l = 0L; -- l |= ((long)x & PACKED_X_MASK) << X_OFFSET; -- l |= ((long)y & PACKED_Y_MASK) << 0; -- return l | ((long)z & PACKED_Z_MASK) << Z_OFFSET; -+ return (((long) x & (long) 67108863) << 38) | (((long) y & (long) 4095)) | (((long) z & (long) 67108863) << 12); // Paper - inline constants and simplify - } - - public static long getFlatIndex(long y) { -diff --git a/src/main/java/net/minecraft/core/SectionPos.java b/src/main/java/net/minecraft/core/SectionPos.java -index 27e0d53d5893a13a340deddc93a1128968db7e5b..fe3577e533fb829c85fd4881b1bcca3b70aaf1a5 100644 ---- a/src/main/java/net/minecraft/core/SectionPos.java -+++ b/src/main/java/net/minecraft/core/SectionPos.java -@@ -38,7 +38,7 @@ public class SectionPos extends Vec3i { - } - - public static SectionPos of(BlockPos pos) { -- return new SectionPos(blockToSectionCoord(pos.getX()), blockToSectionCoord(pos.getY()), blockToSectionCoord(pos.getZ())); -+ return new SectionPos(pos.getX() >> 4, pos.getY() >> 4, pos.getZ() >> 4); // Paper - } - - public static SectionPos of(ChunkPos chunkPos, int y) { -@@ -54,7 +54,7 @@ public class SectionPos extends Vec3i { - } - - public static SectionPos of(long packed) { -- return new SectionPos(x(packed), y(packed), z(packed)); -+ return new SectionPos((int) (packed >> 42), (int) (packed << 44 >> 44), (int) (packed << 22 >> 42)); // Paper - } - - public static SectionPos bottomOf(ChunkAccess chunk) { -@@ -65,8 +65,16 @@ public class SectionPos extends Vec3i { - return offset(packed, direction.getStepX(), direction.getStepY(), direction.getStepZ()); - } - -+ // Paper start -+ public static long getAdjacentFromBlockPos(int x, int y, int z, Direction enumdirection) { -+ return (((long) ((x >> 4) + enumdirection.getStepX()) & 4194303L) << 42) | (((long) ((y >> 4) + enumdirection.getStepY()) & 1048575L)) | (((long) ((z >> 4) + enumdirection.getStepZ()) & 4194303L) << 20); -+ } -+ public static long getAdjacentFromSectionPos(int x, int y, int z, Direction enumdirection) { -+ return (((long) (x + enumdirection.getStepX()) & 4194303L) << 42) | (((long) ((y) + enumdirection.getStepY()) & 1048575L)) | (((long) (z + enumdirection.getStepZ()) & 4194303L) << 20); -+ } -+ // Paper end - public static long offset(long packed, int x, int y, int z) { -- return asLong(x(packed) + x, y(packed) + y, z(packed) + z); -+ return (((long) ((int) (packed >> 42) + x) & 4194303L) << 42) | (((long) ((int) (packed << 44 >> 44) + y) & 1048575L)) | (((long) ((int) (packed << 22 >> 42) + z) & 4194303L) << 20); // Simplify to reduce instruction count - } - - public static int posToSectionCoord(double coord) { -@@ -86,10 +94,7 @@ public class SectionPos extends Vec3i { - } - - public static short sectionRelativePos(BlockPos pos) { -- int i = sectionRelative(pos.getX()); -- int j = sectionRelative(pos.getY()); -- int k = sectionRelative(pos.getZ()); -- return (short)(i << 8 | k << 4 | j << 0); -+ return (short) ((pos.getX() & 15) << 8 | (pos.getZ() & 15) << 4 | pos.getY() & 15); // Paper - simplify/inline - } - - public static int sectionRelativeX(short packedLocalPos) { -@@ -152,16 +157,16 @@ public class SectionPos extends Vec3i { - return this.getZ(); - } - -- public int minBlockX() { -- return sectionToBlockCoord(this.x()); -+ public final int minBlockX() { // Paper - make final -+ return this.getX() << 4; // Paper - inline - } - -- public int minBlockY() { -- return sectionToBlockCoord(this.y()); -+ public final int minBlockY() { // Paper - make final -+ return this.getY() << 4; // Paper - inline - } - -- public int minBlockZ() { -- return sectionToBlockCoord(this.z()); -+ public int minBlockZ() { // Paper - make final -+ return this.getZ() << 4; // Paper - inline - } - - public int maxBlockX() { -@@ -177,7 +182,8 @@ public class SectionPos extends Vec3i { - } - - public static long blockToSection(long blockPos) { -- return asLong(blockToSectionCoord(BlockPos.getX(blockPos)), blockToSectionCoord(BlockPos.getY(blockPos)), blockToSectionCoord(BlockPos.getZ(blockPos))); -+ // b(a(BlockPosition.b(i)), a(BlockPosition.c(i)), a(BlockPosition.d(i))); -+ return (((long) (int) (blockPos >> 42) & 4194303L) << 42) | (((long) (int) ((blockPos << 52) >> 56) & 1048575L)) | (((long) (int) ((blockPos << 26) >> 42) & 4194303L) << 20); // Simplify to reduce instruction count - } - - public static long getZeroNode(int x, int z) { -@@ -205,15 +211,18 @@ public class SectionPos extends Vec3i { - return asLong(blockToSectionCoord(pos.getX()), blockToSectionCoord(pos.getY()), blockToSectionCoord(pos.getZ())); - } - -+ // Paper start -+ public static long blockPosAsSectionLong(int i, int j, int k) { -+ return (((long) (i >> 4) & 4194303L) << 42) | (((long) (j >> 4) & 1048575L)) | (((long) (k >> 4) & 4194303L) << 20); -+ } -+ // Paper end -+ - public static long asLong(int x, int y, int z) { -- long l = 0L; -- l |= ((long)x & 4194303L) << 42; -- l |= ((long)y & 1048575L) << 0; -- return l | ((long)z & 4194303L) << 20; -+ return (((long) x & 4194303L) << 42) | (((long) y & 1048575L)) | (((long) z & 4194303L) << 20); // Paper - Simplify to reduce instruction count - } - - public long asLong() { -- return asLong(this.x(), this.y(), this.z()); -+ return (((long) getX() & 4194303L) << 42) | (((long) getY() & 1048575L)) | (((long) getZ() & 4194303L) << 20); // Paper - Simplify to reduce instruction count - } - - @Override -@@ -226,16 +235,11 @@ public class SectionPos extends Vec3i { - } - - public static Stream cube(SectionPos center, int radius) { -- int i = center.x(); -- int j = center.y(); -- int k = center.z(); -- return betweenClosedStream(i - radius, j - radius, k - radius, i + radius, j + radius, k + radius); -+ return betweenClosedStream(center.getX() - radius, center.getY() - radius, center.getZ() - radius, center.getX() + radius, center.getY() + radius, center.getZ() + radius); // Paper - simplify/inline - } - - public static Stream aroundChunk(ChunkPos center, int radius, int minY, int maxY) { -- int i = center.x; -- int j = center.z; -- return betweenClosedStream(i - radius, minY, j - radius, i + radius, maxY - 1, j + radius); -+ return betweenClosedStream(center.x - radius, 0, center.z - radius, center.x + radius, 15, center.z + radius); // Paper - simplify/inline - } - - public static Stream betweenClosedStream(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { diff --git a/patches/server/1007-Remove-streams-from-hot-code.patch b/patches/server/1007-Remove-streams-from-hot-code.patch new file mode 100644 index 0000000000..4c6d94f7bf --- /dev/null +++ b/patches/server/1007-Remove-streams-from-hot-code.patch @@ -0,0 +1,215 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: JRoy +Date: Wed, 1 Jul 2020 18:01:49 -0400 +Subject: [PATCH] Remove streams from hot code + +Co-authored-by: Bjarne Koll +Co-authored-by: Spottedleaf + +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java b/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java +index 6a270c9adb044a6e0b1c8e09b9106d51989fd761..d4581127366736c54f74e4ef7479236b18fb487d 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java +@@ -57,7 +57,7 @@ public class GateBehavior implements BehaviorControl + if (this.hasRequiredMemories(entity)) { + this.status = Behavior.Status.RUNNING; + this.orderPolicy.apply(this.behaviors); +- this.runningPolicy.apply(this.behaviors.stream(), world, entity, time); ++ this.runningPolicy.apply(this.behaviors, world, entity, time); // Paper - Perf: Remove streams from hot code + return true; + } else { + return false; +@@ -66,7 +66,13 @@ public class GateBehavior implements BehaviorControl + + @Override + public final void tickOrStop(ServerLevel world, E entity, long time) { +- this.behaviors.stream().filter(task -> task.getStatus() == Behavior.Status.RUNNING).forEach(task -> task.tickOrStop(world, entity, time)); ++ // Paper start - Perf: Remove streams from hot code ++ for (final BehaviorControl task : this.behaviors) { ++ if (task.getStatus() == Behavior.Status.RUNNING) { ++ task.tickOrStop(world, entity, time); ++ } ++ } ++ // Paper end - Perf: Remove streams from hot code + if (this.behaviors.stream().noneMatch(task -> task.getStatus() == Behavior.Status.RUNNING)) { + this.doStop(world, entity, time); + } +@@ -75,8 +81,16 @@ public class GateBehavior implements BehaviorControl + @Override + public final void doStop(ServerLevel world, E entity, long time) { + this.status = Behavior.Status.STOPPED; +- this.behaviors.stream().filter(task -> task.getStatus() == Behavior.Status.RUNNING).forEach(task -> task.doStop(world, entity, time)); +- this.exitErasedMemories.forEach(entity.getBrain()::eraseMemory); ++ // Paper start - Perf: Remove streams from hot code ++ for (final BehaviorControl task : this.behaviors) { ++ if (task.getStatus() == Behavior.Status.RUNNING) { ++ task.doStop(world, entity, time); ++ } ++ } ++ for (final MemoryModuleType exitErasedMemory : this.exitErasedMemories) { ++ entity.getBrain().eraseMemory(exitErasedMemory); ++ } ++ // Paper end - Perf: Remove streams from hot code + } + + @Override +@@ -111,18 +125,30 @@ public class GateBehavior implements BehaviorControl + + public static enum RunningPolicy { + RUN_ONE { ++ // Paper start - Perf: Remove streams from hot code + @Override +- public void apply(Stream> tasks, ServerLevel world, E entity, long time) { +- tasks.filter(task -> task.getStatus() == Behavior.Status.STOPPED).filter(task -> task.tryStart(world, entity, time)).findFirst(); ++ public void apply(ShufflingList> tasks, ServerLevel world, E entity, long time) { ++ for (final BehaviorControl task : tasks) { ++ if (task.getStatus() == Behavior.Status.STOPPED && task.tryStart(world, entity, time)) { ++ break; ++ } ++ } ++ // Paper end - Perf: Remove streams from hot code + } + }, + TRY_ALL { ++ // Paper start - Perf: Remove streams from hot code + @Override +- public void apply(Stream> tasks, ServerLevel world, E entity, long time) { +- tasks.filter(task -> task.getStatus() == Behavior.Status.STOPPED).forEach(task -> task.tryStart(world, entity, time)); ++ public void apply(ShufflingList> tasks, ServerLevel world, E entity, long time) { ++ for (final BehaviorControl task : tasks) { ++ if (task.getStatus() == Behavior.Status.STOPPED) { ++ task.tryStart(world, entity, time); ++ } ++ } ++ // Paper end - Perf: Remove streams from hot code + } + }; + +- public abstract void apply(Stream> tasks, ServerLevel world, E entity, long time); ++ public abstract void apply(ShufflingList> tasks, ServerLevel world, E entity, long time); // Paper - Perf: Remove streams from hot code + } + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/gossip/GossipContainer.java b/src/main/java/net/minecraft/world/entity/ai/gossip/GossipContainer.java +index aa32804bc9affe9a615d3ffaa513f6f09aab3f32..c7f012674361a323c1efeca4660cd3f46d308ee1 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/gossip/GossipContainer.java ++++ b/src/main/java/net/minecraft/world/entity/ai/gossip/GossipContainer.java +@@ -59,8 +59,22 @@ public class GossipContainer { + return this.gossips.entrySet().stream().flatMap(entry -> entry.getValue().unpack(entry.getKey())); + } + ++ // Paper start - Perf: Remove streams from hot code ++ private List decompress() { ++ List list = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>(); ++ for (Map.Entry entry : this.gossips.entrySet()) { ++ for (GossipContainer.GossipEntry cur : entry.getValue().decompress(entry.getKey())) { ++ if (cur.weightedValue() != 0) { ++ list.add(cur); ++ } ++ } ++ } ++ return list; ++ } ++ // Paper end - Perf: Remove streams from hot code ++ + private Collection selectGossipsForTransfer(RandomSource random, int count) { +- List list = this.unpack().toList(); ++ List list = this.decompress(); // Paper - Perf: Remove streams from hot code + if (list.isEmpty()) { + return Collections.emptyList(); + } else { +@@ -145,7 +159,7 @@ public class GossipContainer { + + public T store(DynamicOps ops) { + return GossipContainer.GossipEntry.LIST_CODEC +- .encodeStart(ops, this.unpack().toList()) ++ .encodeStart(ops, this.decompress()) // Paper - Perf: Remove streams from hot code + .resultOrPartial(error -> LOGGER.warn("Failed to serialize gossips: {}", error)) + .orElseGet(ops::emptyList); + } +@@ -172,12 +186,23 @@ public class GossipContainer { + final Object2IntMap entries = new Object2IntOpenHashMap<>(); + + public int weightedValue(Predicate gossipTypeFilter) { +- return this.entries +- .object2IntEntrySet() +- .stream() +- .filter(entry -> gossipTypeFilter.test(entry.getKey())) +- .mapToInt(entry -> entry.getIntValue() * entry.getKey().weight) +- .sum(); ++ // Paper start - Perf: Remove streams from hot code ++ int weight = 0; ++ for (Object2IntMap.Entry entry : entries.object2IntEntrySet()) { ++ if (gossipTypeFilter.test(entry.getKey())) { ++ weight += entry.getIntValue() * entry.getKey().weight; ++ } ++ } ++ return weight; ++ } ++ ++ public List decompress(UUID uuid) { ++ List list = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>(); ++ for (Object2IntMap.Entry entry : entries.object2IntEntrySet()) { ++ list.add(new GossipContainer.GossipEntry(uuid, entry.getKey(), entry.getIntValue())); ++ } ++ return list; ++ // Paper end - Perf: Remove streams from hot code + } + + public Stream unpack(UUID target) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java +index 28fd073ddad358e087e8c78985a97cad21be81b7..a5bd308d1b3ea5db185c06a287167d1d8894a987 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java +@@ -24,13 +24,17 @@ public class NearestItemSensor extends Sensor { + @Override + protected void doTick(ServerLevel world, Mob entity) { + Brain brain = entity.getBrain(); +- List list = world.getEntitiesOfClass(ItemEntity.class, entity.getBoundingBox().inflate(32.0, 16.0, 32.0), itemEntity -> true); ++ List list = world.getEntitiesOfClass(ItemEntity.class, entity.getBoundingBox().inflate(32.0, 16.0, 32.0), itemEntity -> itemEntity.closerThan(entity, MAX_DISTANCE_TO_WANTED_ITEM) && entity.wantsToPickUp(itemEntity.getItem())); // Paper - Perf: Move predicate into getEntities + list.sort(Comparator.comparingDouble(entity::distanceToSqr)); +- Optional optional = list.stream() +- .filter(itemEntity -> entity.wantsToPickUp(itemEntity.getItem())) +- .filter(itemEntity -> itemEntity.closerThan(entity, 32.0)) +- .filter(entity::hasLineOfSight) +- .findFirst(); +- brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_WANTED_ITEM, optional); ++ // Paper start - Perf: remove streams from hot code ++ ItemEntity nearest = null; ++ for (ItemEntity entityItem : list) { ++ if (entity.hasLineOfSight(entityItem)) { // Paper - Perf: Move predicate into getEntities ++ nearest = entityItem; ++ break; ++ } ++ } ++ brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_WANTED_ITEM, Optional.ofNullable(nearest)); ++ // Paper end - Perf: remove streams from hot code + } + } +diff --git a/src/main/java/net/minecraft/world/level/levelgen/Beardifier.java b/src/main/java/net/minecraft/world/level/levelgen/Beardifier.java +index 04552e2cf3de2dc41dfe7e7a2e88b200eaaf280b..ca93a97256350789ca56f910862c9d717ca7670b 100644 +--- a/src/main/java/net/minecraft/world/level/levelgen/Beardifier.java ++++ b/src/main/java/net/minecraft/world/level/levelgen/Beardifier.java +@@ -35,9 +35,10 @@ public class Beardifier implements DensityFunctions.BeardifierOrMarker { + int j = pos.getMinBlockZ(); + ObjectList objectList = new ObjectArrayList<>(10); + ObjectList objectList2 = new ObjectArrayList<>(32); +- world.startsForStructure(pos, structure -> structure.terrainAdaptation() != TerrainAdjustment.NONE) +- .forEach( +- start -> { ++ // Paper start - Perf: Remove streams from hot code ++ for (net.minecraft.world.level.levelgen.structure.StructureStart start : world.startsForStructure(pos, (structure) -> { ++ return structure.terrainAdaptation() != TerrainAdjustment.NONE; ++ })) { // Paper end - Perf: Remove streams from hot code + TerrainAdjustment terrainAdjustment = start.getStructure().terrainAdaptation(); + + for (StructurePiece structurePiece : start.getPieces()) { +@@ -65,8 +66,7 @@ public class Beardifier implements DensityFunctions.BeardifierOrMarker { + } + } + } +- } +- ); ++ } // Paper - Perf: Remove streams from hot code + return new Beardifier(objectList.iterator(), objectList2.iterator()); + } + diff --git a/patches/server/1008-Eigencraft-redstone-implementation.patch b/patches/server/1008-Eigencraft-redstone-implementation.patch new file mode 100644 index 0000000000..b3e3db6504 --- /dev/null +++ b/patches/server/1008-Eigencraft-redstone-implementation.patch @@ -0,0 +1,1145 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: theosib +Date: Thu, 27 Sep 2018 01:43:35 -0600 +Subject: [PATCH] Eigencraft redstone implementation + +Author: theosib + +Original license: MIT + +This patch implements theosib's redstone algorithms to completely overhaul the way redstone works. +The new algorithms should be many times faster than current vanilla ones. +From the original author's comments, it looks like it shouldn't interfere with any redstone save for very extreme edge-cases. + +Surprisingly, not a lot was touched aside from a few obfuscation helpers and BlockRedstoneWire. +A lot of this code is self-contained in a helper class. + +Aside from making the obvious class/function renames and obfhelpers I didn't need to modify much. +Just added Bukkit's event system and took a few liberties with dead code and comment misspellings. + +== AT == +public net.minecraft.world.level.block.RedStoneWireBlock shouldSignal +public net.minecraft.world.level.block.RedStoneWireBlock canSurvive(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/LevelReader;Lnet/minecraft/core/BlockPos;)Z + +Co-authored-by: egg82 + +diff --git a/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java +new file mode 100644 +index 0000000000000000000000000000000000000000..9f17170179cc99d84ad25a1e838aff3d8cc66f93 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java +@@ -0,0 +1,958 @@ ++package com.destroystokyo.paper.util; ++ ++import java.util.List; ++import java.util.Map; ++import java.util.concurrent.ThreadLocalRandom; ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.Direction; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.item.Items; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.RedStoneWireBlock; ++import net.minecraft.world.level.block.state.BlockState; ++import org.bukkit.craftbukkit.block.CraftBlock; ++import org.bukkit.event.block.BlockRedstoneEvent; ++ ++import com.google.common.collect.Lists; ++import com.google.common.collect.Maps; ++ ++/** ++ * Used for the faster redstone algorithm. ++ * Original author: theosib ++ * Original license: MIT ++ * ++ * Ported to Paper and updated to 1.13 by egg82 ++ */ ++public class RedstoneWireTurbo { ++ /* ++ * This is Helper class for BlockRedstoneWire. It implements a minimally-invasive ++ * bolt-on accelerator that performs a breadth-first search through redstone wire blocks ++ * in order to more efficiently and deterministically compute new redstone wire power levels ++ * and determine the order in which other blocks should be updated. ++ * ++ * Features: ++ * - Changes to BlockRedstoneWire are very limited, no other classes are affected, and the ++ * choice between old and new redstone wire update algorithms is switchable on-line. ++ * - The vanilla implementation relied on World.notifyNeighborsOfStateChange for redstone ++ * wire blocks to communicate power level changes to each other, generating 36 block ++ * updates per call. This improved implementation propagates power level changes directly ++ * between redstone wire blocks. Redstone wire power levels are therefore computed more quickly, ++ * and block updates are sent only to non-redstone blocks, many of which may perform an ++ * action when informed of a change in redstone power level. (Note: Block updates are not ++ * the same as state changes to redstone wire. Wire block states are updated as soon ++ * as they are computed.) ++ * - Of the 36 block updates generated by a call to World.notifyNeighborsOfStateChange, ++ * 12 of them are obviously redundant (e.g. the west neighbor of the east neighbor). ++ * These are eliminated. ++ * - Updates to redstone wire and other connected blocks are propagated in a breath-first ++ * manner, radiating out from the initial trigger (a block update to a redstone wire ++ * from something other than redstone wire). ++ * - Updates are scheduled both deterministically and in an intuitive order, addressing bug ++ * MC-11193. ++ * - All redstone behavior that used to be locational now works the same in all locations. ++ * - All behaviors of redstone wire that used to be orientational now work the same in all ++ * orientations, as long as orientation can be determined; random otherwise. Some other ++ * redstone components still update directionally (e.g. switches), and this code can't ++ * compensate for that. ++ * - Information that is otherwise computed over and over again or which is expensive to ++ * to compute is cached for faster lookup. This includes coordinates of block position ++ * neighbors and block states that won't change behind our backs during the execution of ++ * this search algorithm. ++ * - Redundant block updates (both to redstone wire and to other blocks) are heavily ++ * consolidated. For worst-case scenarios (depowering of redstone wire) this results ++ * in a reduction of block updates by as much as 95% (factor of 1/21). Due to overheads, ++ * empirical testing shows a speedup better than 10x. This addresses bug MC-81098. ++ * ++ * Extensive testing has been performed to ensure that existing redstone contraptions still ++ * behave as expected. Results of early testing that identified undesirable behavior changes ++ * were addressed. Additionally, real-time performance testing revealed compute inefficiencies ++ * With earlier implementations of this accelerator. Some compatibility adjustments and ++ * performance optimizations resulted in harmless increases in block updates above the ++ * theoretical minimum. ++ * ++ * Only a single redstone machine was found to break: An instant dropper line hack that ++ * relies on powered rails and quasi-connectivity but doesn't work in all directions. The ++ * replacement is to lay redstone wire directly on top of the dropper line, which now works ++ * reliably in any direction. ++ * ++ * There are numerous other optimization that can be made, but those will be provided later in ++ * separate updates. This version is designed to be minimalistic. ++ * ++ * Many thanks to the following individuals for their help in testing this functionality: ++ * - pokechu22, _MethodZz_, WARBEN, NarcolepticFrog, CommandHelper (nessie), ilmango, ++ * OreoLamp, Xcom6000, tryashtar, RedCMD, Smokey95Dog, EDDxample, Rays Works, ++ * Nodnam, BlockyPlays, Grumm, NeunEinser, HelVince. ++ */ ++ ++ /* Reference to BlockRedstoneWire object, which uses this accelerator */ ++ private final RedStoneWireBlock wire; ++ ++ /* ++ * Implementation: ++ * ++ * RedstoneWire Blocks are updated in concentric rings or "layers" radiating out from the ++ * initial block update that came from a call to BlockRedstoneWire.neighborChanged(). ++ * All nodes put in Layer N are those with Manhattan distance N from the trigger ++ * position, reachable through connected redstone wire blocks. ++ * ++ * Layer 0 represents the trigger block position that was input to neighborChanged. ++ * Layer 1 contains the immediate neighbors of that position. ++ * Layer N contains the neighbors of blocks in layer N-1, not including ++ * those in previous layers. ++ * ++ * Layers enforce an update order that is a function of Manhattan distance ++ * from the initial coordinates input to neighborChanged. The same ++ * coordinates may appear in multiple layers, but redundant updates are minimized. ++ * Block updates are sent layer-by-layer. If multiple of a block's neighbors experience ++ * redstone wire changes before its layer is processed, then those updates will be merged. ++ * If a block's update has been sent, but its neighboring redstone changes ++ * after that, then another update will be sent. This preserves compatibility with ++ * machines that rely on zero-tick behavior, except that the new functionality is non- ++ * locational. ++ * ++ * Within each layer, updates are ordered left-to-right relative to the direction of ++ * information flow. This makes the implementation non-orientational. Only when ++ * this direction is ambiguous is randomness applied (intentionally). ++ */ ++ private List updateQueue0 = Lists.newArrayList(); ++ private List updateQueue1 = Lists.newArrayList(); ++ private List updateQueue2 = Lists.newArrayList(); ++ ++ public RedstoneWireTurbo(RedStoneWireBlock wire) { ++ this.wire = wire; ++ } ++ ++ /* ++ * Compute neighbors of a block. When a redstone wire value changes, previously it called ++ * World.notifyNeighborsOfStateChange. That lists immediately neighboring blocks in ++ * west, east, down, up, north, south order. For each of those neighbors, their own ++ * neighbors are updated in the same order. This generates 36 updates, but 12 of them are ++ * redundant; for instance the west neighbor of a block's east neighbor. ++ * ++ * Note that this ordering is only used to create the initial list of neighbors. Once ++ * the direction of signal flow is identified, the ordering of updates is completely ++ * reorganized. ++ */ ++ public static BlockPos[] computeAllNeighbors(final BlockPos pos) { ++ final int x = pos.getX(); ++ final int y = pos.getY(); ++ final int z = pos.getZ(); ++ final BlockPos[] n = new BlockPos[24]; ++ ++ // Immediate neighbors, in the same order as ++ // World.notifyNeighborsOfStateChange, etc.: ++ // west, east, down, up, north, south ++ n[0] = new BlockPos(x - 1, y, z); ++ n[1] = new BlockPos(x + 1, y, z); ++ n[2] = new BlockPos(x, y - 1, z); ++ n[3] = new BlockPos(x, y + 1, z); ++ n[4] = new BlockPos(x, y, z - 1); ++ n[5] = new BlockPos(x, y, z + 1); ++ ++ // Neighbors of neighbors, in the same order, ++ // except that duplicates are not included ++ n[6] = new BlockPos(x - 2, y, z); ++ n[7] = new BlockPos(x - 1, y - 1, z); ++ n[8] = new BlockPos(x - 1, y + 1, z); ++ n[9] = new BlockPos(x - 1, y, z - 1); ++ n[10] = new BlockPos(x - 1, y, z + 1); ++ n[11] = new BlockPos(x + 2, y, z); ++ n[12] = new BlockPos(x + 1, y - 1, z); ++ n[13] = new BlockPos(x + 1, y + 1, z); ++ n[14] = new BlockPos(x + 1, y, z - 1); ++ n[15] = new BlockPos(x + 1, y, z + 1); ++ n[16] = new BlockPos(x, y - 2, z); ++ n[17] = new BlockPos(x, y - 1, z - 1); ++ n[18] = new BlockPos(x, y - 1, z + 1); ++ n[19] = new BlockPos(x, y + 2, z); ++ n[20] = new BlockPos(x, y + 1, z - 1); ++ n[21] = new BlockPos(x, y + 1, z + 1); ++ n[22] = new BlockPos(x, y, z - 2); ++ n[23] = new BlockPos(x, y, z + 2); ++ return n; ++ } ++ ++ /* ++ * We only want redstone wires to update redstone wires that are ++ * immediately adjacent. Some more distant updates can result ++ * in cross-talk that (a) wastes time and (b) can make the update ++ * order unintuitive. Therefore (relative to the neighbor order ++ * computed by computeAllNeighbors), updates are not scheduled ++ * for redstone wire in those non-connecting positions. On the ++ * other hand, updates will always be sent to *other* types of blocks ++ * in any of the 24 neighboring positions. ++ */ ++ private static final boolean[] update_redstone = { ++ true, true, false, false, true, true, // 0 to 5 ++ false, true, true, false, false, false, // 6 to 11 ++ true, true, false, false, false, true, // 12 to 17 ++ true, false, true, true, false, false // 18 to 23 ++ }; ++ ++ // Internal numbering for cardinal directions ++ private static final int North = 0; ++ private static final int East = 1; ++ private static final int South = 2; ++ private static final int West = 3; ++ ++ /* ++ * These lookup tables completely remap neighbor positions into a left-to-right ++ * ordering, based on the cardinal direction that is determined to be forward. ++ * See below for more explanation. ++ */ ++ private static final int[] forward_is_north = {2, 3, 16, 19, 0, 4, 1, 5, 7, 8, 17, 20, 12, 13, 18, 21, 6, 9, 22, 14, 11, 10, 23, 15}; ++ private static final int[] forward_is_east = {2, 3, 16, 19, 4, 1, 5, 0, 17, 20, 12, 13, 18, 21, 7, 8, 22, 14, 11, 15, 23, 9, 6, 10}; ++ private static final int[] forward_is_south = {2, 3, 16, 19, 1, 5, 0, 4, 12, 13, 18, 21, 7, 8, 17, 20, 11, 15, 23, 10, 6, 14, 22, 9}; ++ private static final int[] forward_is_west = {2, 3, 16, 19, 5, 0, 4, 1, 18, 21, 7, 8, 17, 20, 12, 13, 23, 10, 6, 9, 22, 15, 11, 14}; ++ ++ /* For any orientation, we end up with the update order defined below. This order is relative to any redstone wire block ++ * that is itself having an update computed, and this center position is marked with C. ++ * - The update position marked 0 is computed first, and the one marked 23 is last. ++ * - Forward is determined by the local direction of information flow into position C from prior updates. ++ * - The first updates are scheduled for the four positions below and above C. ++ * - Then updates are scheduled for the four horizontal neighbors of C, followed by the positions below and above those neighbors. ++ * - Finally, updates are scheduled for the remaining positions with Manhattan distance 2 from C (at the same Y coordinate). ++ * - For a given horizontal distance from C, updates are scheduled starting from directly left and stepping clockwise to directly ++ * right. The remaining positions behind C are scheduled counterclockwise so as to maintain the left-to-right ordering. ++ * - If C is in layer N of the update schedule, then all 24 positions may be scheduled for layer N+1. For redstone wire, no ++ * updates are scheduled for positions that cannot directly connect. Additionally, the four positions above and below C ++ * are ALSO scheduled for layer N+2. ++ * - This update order was selected after experimenting with a number of alternative schedules, based on its compatibility ++ * with existing redstone designs and behaviors that were considered to be intuitive by various testers. WARBEN in particular ++ * made some of the most challenging test cases, but the 3-tick clocks (made by RedCMD) were also challenging to fix, ++ * along with the rail-based instant dropper line built by ilmango. Numerous others made test cases as well, including ++ * NarcolepticFrog, nessie, and Pokechu22. ++ * ++ * - The forward direction is determined locally. So when there are branches in the redstone wire, the left one will get updated ++ * before the right one. Each branch can have its own relative forward direction, resulting in the left side of a left branch ++ * having priority over the right branch of a left branch, which has priority over the left branch of a right branch, followed ++ * by the right branch of a right branch. And so forth. Since redstone power reduces to zero after a path distance of 15, ++ * that imposes a practical limit on the branching. Note that the branching is not tracked explicitly -- relative forward ++ * directions dictate relative sort order, which maintains the proper global ordering. This also makes it unnecessary to be ++ * concerned about branches meeting up with each other. ++ * ++ * ^ ++ * | ++ * Forward ++ * <-- Left Right --> ++ * ++ * 18 ++ * 10 17 5 19 11 ++ * 2 8 0 12 16 4 C 6 20 9 1 13 3 ++ * 14 21 7 23 15 ++ * Further 22 Further ++ * Down Down Up Up ++ * ++ * Backward ++ * | ++ * V ++ */ ++ ++ // This allows the above remapping tables to be looked up by cardial direction index ++ private static final int[][] reordering = { forward_is_north, forward_is_east, forward_is_south, forward_is_west }; ++ ++ /* ++ * Input: Array of UpdateNode objects in an order corresponding to the positions ++ * computed by computeAllNeighbors above. ++ * Output: Array of UpdateNode objects oriented using the above remapping tables ++ * corresponding to the identified heading (direction of information flow). ++ */ ++ private static void orientNeighbors(final UpdateNode[] src, final UpdateNode[] dst, final int heading) { ++ final int[] re = reordering[heading]; ++ for (int i = 0; i < 24; i++) { ++ dst[i] = src[re[i]]; ++ } ++ } ++ ++ /* ++ * Structure to keep track of redstone wire blocks and ++ * neighbors that will receive updates. ++ */ ++ private static class UpdateNode { ++ public static enum Type { ++ UNKNOWN, REDSTONE, OTHER ++ } ++ ++ BlockState currentState; // Keep track of redstone wire value ++ UpdateNode[] neighbor_nodes; // References to neighbors (directed graph edges) ++ BlockPos self; // UpdateNode's own position ++ BlockPos parent; // Which block pos spawned/updated this node ++ Type type = Type.UNKNOWN; // unknown, redstone wire, other type of block ++ int layer; // Highest layer this node is scheduled in ++ boolean visited; // To keep track of information flow direction, visited restone wire is marked ++ int xbias, zbias; // Remembers directionality of ancestor nodes; helps eliminate directional ambiguities. ++ } ++ ++ /* ++ * Keep track of all block positions discovered during search and their current states. ++ * We want to remember one entry for each position. ++ */ ++ private final Map nodeCache = Maps.newHashMap(); ++ ++ /* ++ * For a newly created UpdateNode object, determine what type of block it is. ++ */ ++ private void identifyNode(final Level worldIn, final UpdateNode upd1) { ++ final BlockPos pos = upd1.self; ++ final BlockState oldState = worldIn.getBlockState(pos); ++ upd1.currentState = oldState; ++ ++ // Some neighbors of redstone wire are other kinds of blocks. ++ // These need to receive block updates to inform them that ++ // redstone wire values have changed. ++ final Block block = oldState.getBlock(); ++ if (block != wire) { ++ // Mark this block as not redstone wire and therefore ++ // requiring updates ++ upd1.type = UpdateNode.Type.OTHER; ++ ++ // Non-redstone blocks may propagate updates, but those updates ++ // are not handled by this accelerator. Therefore, we do not ++ // expand this position's neighbors. ++ return; ++ } ++ ++ // One job of BlockRedstoneWire.neighborChanged is to convert ++ // redstone wires to items if the block beneath was removed. ++ // With this accelerator, BlockRedstoneWire.neighborChanged ++ // is only typically called for a single wire block, while ++ // others are processed internally by the breadth first search ++ // algorithm. To preserve this game behavior, this check must ++ // be replicated here. ++ if (!wire.canSurvive(null, worldIn, pos)) { ++ // Pop off the redstone dust ++ Block.popResource(worldIn, pos, new ItemStack(Items.REDSTONE)); // TODO ++ worldIn.removeBlock(pos, false); ++ ++ // Mark this position as not being redstone wire ++ upd1.type = UpdateNode.Type.OTHER; ++ ++ // Note: Sending updates to air blocks leads to an empty method. ++ // Testing shows this to be faster than explicitly avoiding updates to ++ // air blocks. ++ return; ++ } ++ ++ // If the above conditions fail, then this is a redstone wire block. ++ upd1.type = UpdateNode.Type.REDSTONE; ++ } ++ ++ /* ++ * Given which redstone wire blocks have been visited and not visited ++ * around the position currently being updated, compute the cardinal ++ * direction that is "forward." ++ * ++ * rx is the forward direction along the West/East axis ++ * rz is the forward direction along the North/South axis ++ */ ++ static private int computeHeading(final int rx, final int rz) { ++ // rx and rz can only take on values -1, 0, and 1, so we can ++ // compute a code number that allows us to use a single switch ++ // to determine the heading. ++ final int code = (rx + 1) + 3 * (rz + 1); ++ switch (code) { ++ case 0: { ++ // Both rx and rz are -1 (northwest) ++ // Randomly choose one to be forward. ++ final int j = ThreadLocalRandom.current().nextInt(0, 1); ++ return (j == 0) ? North : West; ++ } ++ case 1: { ++ // rx=0, rz=-1 ++ // Definitively North ++ return North; ++ } ++ case 2: { ++ // rx=1, rz=-1 (northeast) ++ // Choose randomly between north and east ++ final int j = ThreadLocalRandom.current().nextInt(0, 1); ++ return (j == 0) ? North : East; ++ } ++ case 3: { ++ // rx=-1, rz=0 ++ // Definitively West ++ return West; ++ } ++ case 4: { ++ // rx=0, rz=0 ++ // Heading is completely ambiguous. Choose ++ // randomly among the four cardinal directions. ++ return ThreadLocalRandom.current().nextInt(0, 4); ++ } ++ case 5: { ++ // rx=1, rz=0 ++ // Definitively East ++ return East; ++ } ++ case 6: { ++ // rx=-1, rz=1 (southwest) ++ // Choose randomly between south and west ++ final int j = ThreadLocalRandom.current().nextInt(0, 1); ++ return (j == 0) ? South : West; ++ } ++ case 7: { ++ // rx=0, rz=1 ++ // Definitively South ++ return South; ++ } ++ case 8: { ++ // rx=1, rz=1 (southeast) ++ // Choose randomly between south and east ++ final int j = ThreadLocalRandom.current().nextInt(0, 1); ++ return (j == 0) ? South : East; ++ } ++ } ++ ++ // We should never get here ++ return ThreadLocalRandom.current().nextInt(0, 4); ++ } ++ ++ // Select whether to use updateSurroundingRedstone from BlockRedstoneWire (old) ++ // or this helper class (new) ++ private static final boolean old_current_change = false; ++ ++ /* ++ * Process a node whose neighboring redstone wire has experienced value changes. ++ */ ++ private void updateNode(final Level worldIn, final UpdateNode upd1, final int layer) { ++ final BlockPos pos = upd1.self; ++ ++ // Mark this redstone wire as having been visited so that it can be used ++ // to calculate direction of information flow. ++ upd1.visited = true; ++ ++ // Look up the last known state. ++ // Due to the way other redstone components are updated, we do not ++ // have to worry about a state changing behind our backs. The rare ++ // exception is handled by scheduleReentrantNeighborChanged. ++ final BlockState oldState = upd1.currentState; ++ ++ // Ask the wire block to compute its power level from its neighbors. ++ // This will also update the wire's power level and return a new ++ // state if it has changed. When a wire power level is changed, ++ // calculateCurrentChanges will immediately update the block state in the world ++ // and return the same value here to be cached in the corresponding ++ // UpdateNode object. ++ BlockState newState; ++ if (old_current_change) { ++ newState = wire.calculateCurrentChanges(worldIn, pos, pos, oldState); ++ } else { ++ // Looking up block state is slow. This accelerator includes a version of ++ // calculateCurrentChanges that uses cahed wire values for a ++ // significant performance boost. ++ newState = this.calculateCurrentChanges(worldIn, upd1); ++ } ++ ++ // Only inform neighbors if the state has changed ++ if (newState != oldState) { ++ // Store the new state ++ upd1.currentState = newState; ++ ++ // Inform neighbors of the change ++ propagateChanges(worldIn, upd1, layer); ++ } ++ } ++ ++ /* ++ * This identifies the neighboring positions of a new UpdateNode object, ++ * determines their types, and links those to into the graph. Then based on ++ * what nodes in the redstone wire graph have been visited, the neighbors ++ * are reordered left-to-right relative to the direction of information flow. ++ */ ++ private void findNeighbors(final Level worldIn, final UpdateNode upd1) { ++ final BlockPos pos = upd1.self; ++ ++ // Get the list of neighbor coordinates ++ final BlockPos[] neighbors = computeAllNeighbors(pos); ++ ++ // Temporary array of neighbors in cardinal ordering ++ final UpdateNode[] neighbor_nodes = new UpdateNode[24]; ++ ++ // Target array of neighbors sorted left-to-right ++ upd1.neighbor_nodes = new UpdateNode[24]; ++ ++ for (int i=0; i<24; i++) { ++ // Look up each neighbor in the node cache ++ final BlockPos pos2 = neighbors[i]; ++ UpdateNode upd2 = nodeCache.get(pos2); ++ if (upd2 == null) { ++ // If this is a previously unreached position, create ++ // a new update node, add it to the cache, and identify what it is. ++ upd2 = new UpdateNode(); ++ upd2.self = pos2; ++ upd2.parent = pos; ++ nodeCache.put(pos2, upd2); ++ identifyNode(worldIn, upd2); ++ } ++ ++ // For non-redstone blocks, any of the 24 neighboring positions ++ // should receive a block update. However, some block coordinates ++ // may contain a redstone wire that does not directly connect to the ++ // one being expanded. To avoid redundant calculations and confusing ++ // cross-talk, those neighboring positions are not included. ++ if (update_redstone[i] || upd2.type != UpdateNode.Type.REDSTONE) { ++ neighbor_nodes[i] = upd2; ++ } ++ } ++ ++ // Determine the directions from which the redstone signal may have come from. This ++ // checks for redstone wire at the same Y level and also Y+1 and Y-1, relative to the ++ // block being expanded. ++ final boolean fromWest = (neighbor_nodes[0].visited || neighbor_nodes[7].visited || neighbor_nodes[8].visited); ++ final boolean fromEast = (neighbor_nodes[1].visited || neighbor_nodes[12].visited || neighbor_nodes[13].visited); ++ final boolean fromNorth = (neighbor_nodes[4].visited || neighbor_nodes[17].visited || neighbor_nodes[20].visited); ++ final boolean fromSouth = (neighbor_nodes[5].visited || neighbor_nodes[18].visited || neighbor_nodes[21].visited); ++ ++ int cx = 0, cz = 0; ++ if (fromWest) cx += 1; ++ if (fromEast) cx -= 1; ++ if (fromNorth) cz += 1; ++ if (fromSouth) cz -= 1; ++ ++ int heading; ++ if (cx==0 && cz==0) { ++ // If there is no clear direction, try to inherit the heading from ancestor nodes. ++ heading = computeHeading(upd1.xbias, upd1.zbias); ++ ++ // Propagate that heading to descendant nodes. ++ for (int i=0; i<24; i++) { ++ final UpdateNode nn = neighbor_nodes[i]; ++ if (nn != null) { ++ nn.xbias = upd1.xbias; ++ nn.zbias = upd1.zbias; ++ } ++ } ++ } else { ++ if (cx != 0 && cz != 0) { ++ // If the heading is somewhat ambiguous, try to disambiguate based on ++ // ancestor nodes. ++ if (upd1.xbias != 0) cz = 0; ++ if (upd1.zbias != 0) cx = 0; ++ } ++ heading = computeHeading(cx, cz); ++ ++ // Propagate that heading to descendant nodes. ++ for (int i=0; i<24; i++) { ++ final UpdateNode nn = neighbor_nodes[i]; ++ if (nn != null) { ++ nn.xbias = cx; ++ nn.zbias = cz; ++ } ++ } ++ } ++ ++ // Reorder neighboring UpdateNode objects according to the forward direction ++ // determined above. ++ orientNeighbors(neighbor_nodes, upd1.neighbor_nodes, heading); ++ } ++ ++ /* ++ * For any redstone wire block in layer N, inform neighbors to recompute their states ++ * in layers N+1 and N+2; ++ */ ++ private void propagateChanges(final Level worldIn, final UpdateNode upd1, final int layer) { ++ if (upd1.neighbor_nodes == null) { ++ // If this node has not been expanded yet, find its neighbors ++ findNeighbors(worldIn, upd1); ++ } ++ ++ final BlockPos pos = upd1.self; ++ ++ // All neighbors may be scheduled for layer N+1 ++ final int layer1 = layer + 1; ++ ++ // If the node being updated (upd1) has already been expanded, then merely ++ // schedule updates to its neighbors. ++ for (int i = 0; i < 24; i++) { ++ final UpdateNode upd2 = upd1.neighbor_nodes[i]; ++ ++ // This test ensures that an UpdateNode is never scheduled to the same layer ++ // more than once. Also, skip non-connecting redstone wire blocks ++ if (upd2 != null && layer1 > upd2.layer) { ++ upd2.layer = layer1; ++ updateQueue1.add(upd2); ++ ++ // Keep track of which block updated this neighbor ++ upd2.parent = pos; ++ } ++ } ++ ++ // Nodes above and below are scheduled ALSO for layer N+2 ++ final int layer2 = layer + 2; ++ ++ // Repeat of the loop above, but only for the first four (above and below) neighbors ++ // and for layer N+2; ++ for (int i = 0; i < 4; i++) { ++ final UpdateNode upd2 = upd1.neighbor_nodes[i]; ++ if (upd2 != null && layer2 > upd2.layer) { ++ upd2.layer = layer2; ++ updateQueue2.add(upd2); ++ upd2.parent = pos; ++ } ++ } ++ } ++ ++ // The breadth-first search below will send block updates to blocks ++ // that are not redstone wire. If one of those updates results in ++ // a distant redstone wire getting an update, then this.neighborChanged ++ // will get called. This would be a reentrant call, and ++ // it is necessary to properly integrate those updates into the ++ // on-going search through redstone wire. Thus, we make the layer ++ // currently being processed visible at the object level. ++ ++ // The current layer being processed by the breadth-first search ++ private int currentWalkLayer = 0; ++ ++ private void shiftQueue() { ++ final List t = updateQueue0; ++ t.clear(); ++ updateQueue0 = updateQueue1; ++ updateQueue1 = updateQueue2; ++ updateQueue2 = t; ++ } ++ ++ /* ++ * Perform a breadth-first (layer by layer) traversal through redstone ++ * wire blocks, propagating value changes to neighbors in an order ++ * that is a function of distance from the initial call to ++ * this.neighborChanged. ++ */ ++ private void breadthFirstWalk(final Level worldIn) { ++ shiftQueue(); ++ currentWalkLayer = 1; ++ ++ // Loop over all layers ++ while (updateQueue0.size()>0 || updateQueue1.size()>0) { ++ // Get the set of blocks in this layer ++ final List thisLayer = updateQueue0; ++ ++ // Loop over all blocks in the layer. Recall that ++ // this is a List, preserving the insertion order of ++ // left-to-right based on direction of information flow. ++ for (UpdateNode upd : thisLayer) { ++ if (upd.type == UpdateNode.Type.REDSTONE) { ++ // If the node is is redstone wire, ++ // schedule updates to neighbors if its value ++ // has changed. ++ updateNode(worldIn, upd, currentWalkLayer); ++ } else { ++ // If this block is not redstone wire, send a block update. ++ // Redstone wire blocks get state updates, but they don't ++ // need block updates. Only non-redstone neighbors need updates. ++ ++ // World.neighborChanged is called from ++ // World.notifyNeighborsOfStateChange, and ++ // notifyNeighborsOfStateExcept. We don't use ++ // World.notifyNeighborsOfStateChange here, since we are ++ // already keeping track of all of the neighbor positions ++ // that need to be updated. All on its own, handling neighbors ++ // this way reduces block updates by 1/3 (24 instead of 36). ++// worldIn.neighborChanged(upd.self, wire, upd.parent); ++ ++ // [Space Walker] ++ // The neighbor update system got a significant overhaul in 1.19. ++ // Shape and block updates are now added to a stack before being ++ // processed. These changes make it so any neighbor updates emitted ++ // by this accelerator will not be processed until after the entire ++ // wire network has updated. This has a significant impact on the ++ // behavior and introduces Vanilla parity issues. ++ // To circumvent this issue we bypass the neighbor update stack and ++ // call BlockStateBase#neighborChanged directly. This change mostly ++ // restores old behavior, at the cost of bypassing the ++ // max-chained-neighbor-updates server property. ++ worldIn.getBlockState(upd.self).handleNeighborChanged(worldIn, upd.self, wire, upd.parent, false); ++ } ++ } ++ ++ // Move on to the next layer ++ shiftQueue(); ++ currentWalkLayer++; ++ } ++ ++ currentWalkLayer = 0; ++ } ++ ++ /* ++ * Normally, when Minecraft is computing redstone wire power changes, and a wire power level ++ * change sends a block update to a neighboring functional component (e.g. piston, repeater, etc.), ++ * those updates are queued. Only once all redstone wire updates are complete will any component ++ * action generate any further block updates to redstone wire. Instant repeater lines, for instance, ++ * will process all wire updates for one redstone line, after which the pistons will zero-tick, ++ * after which the next redstone line performs all of its updates. Thus, each wire is processed in its ++ * own discrete wave. ++ * ++ * However, there are some corner cases where this pattern breaks, with a proof of concept discovered ++ * by Rays Works, which works the same in vanilla. The scenario is as follows: ++ * (1) A redstone wire is conducting a signal. ++ * (2) Part-way through that wave of updates, a neighbor is updated that causes an update to a completely ++ * separate redstone wire. ++ * (3) This results in a call to BlockRedstoneWire.neighborChanged for that other wire, in the middle of ++ * an already on-going propagation through the first wire. ++ * ++ * The vanilla code, being depth-first, would end up fully processing the second wire before going back ++ * to finish processing the first one. (Although technically, vanilla has no special concept of "being ++ * in the middle" of processing updates to a wire.) For the breadth-first algorithm, we give this ++ * situation special handling, where the updates for the second wire are incorporated into the schedule ++ * for the first wire, and then the callstack is allowed to unwind back to the on-going search loop in ++ * order to continue processing both the first and second wire in the order of distance from the initial ++ * trigger. ++ */ ++ private BlockState scheduleReentrantNeighborChanged(final Level worldIn, final BlockPos pos, final BlockState newState, final BlockPos source) { ++ if (source != null) { ++ // If the cause of the redstone wire update is known, we can use that to help determine ++ // direction of information flow. ++ UpdateNode src = nodeCache.get(source); ++ if (src == null) { ++ src = new UpdateNode(); ++ src.self = source; ++ src.parent = source; ++ src.visited = true; ++ identifyNode(worldIn, src); ++ nodeCache.put(source, src); ++ } ++ } ++ ++ // Find or generate a node for the redstone block position receiving the update ++ UpdateNode upd = nodeCache.get(pos); ++ if (upd == null) { ++ upd = new UpdateNode(); ++ upd.self = pos; ++ upd.parent = pos; ++ upd.visited = true; ++ identifyNode(worldIn, upd); ++ nodeCache.put(pos, upd); ++ } ++ upd.currentState = newState; ++ ++ // Receiving this block update may mean something in the world changed. ++ // Therefore we clear the cached block info about all neighbors of ++ // the position receiving the update and then re-identify what they are. ++ if (upd.neighbor_nodes != null) { ++ for (int i=0; i<24; i++) { ++ final UpdateNode upd2 = upd.neighbor_nodes[i]; ++ if (upd2 == null) continue; ++ upd2.type = UpdateNode.Type.UNKNOWN; ++ upd2.currentState = null; ++ identifyNode(worldIn, upd2); ++ } ++ } ++ ++ // The block at 'pos' is a redstone wire and has been updated already by calling ++ // wire.calculateCurrentChanges, so we don't schedule that. However, we do need ++ // to schedule its neighbors. By passing the current value of 'currentWalkLayer' to ++ // propagateChanges, the neighbors of 'pos' are scheduled for layers currentWalkLayer+1 ++ // and currentWalkLayer+2. ++ propagateChanges(worldIn, upd, currentWalkLayer); ++ ++ // Return here. The call stack will unwind back to the first call to ++ // updateSurroundingRedstone, whereupon the new updates just scheduled will ++ // be propagated. This also facilitates elimination of superfluous and ++ // redundant block updates. ++ return newState; ++ } ++ ++ /* ++ * New version of pre-existing updateSurroundingRedstone, which is called from ++ * wire.updateSurroundingRedstone, which is called from wire.neighborChanged and a ++ * few other methods in BlockRedstoneWire. This sets off the breadth-first ++ * walk through all redstone dust connected to the initial position triggered. ++ */ ++ public BlockState updateSurroundingRedstone(final Level worldIn, final BlockPos pos, final BlockState state, final BlockPos source) { ++ // Check this block's neighbors and see if its power level needs to change ++ // Use the calculateCurrentChanges method in BlockRedstoneWire since we have no ++ // cached block states at this point. ++ final BlockState newState = wire.calculateCurrentChanges(worldIn, pos, pos, state); ++ ++ // If no change, exit ++ if (newState == state) { ++ return state; ++ } ++ ++ // Check to see if this update was received during an on-going breadth first search ++ if (currentWalkLayer > 0 || nodeCache.size() > 0) { ++ // As breadthFirstWalk progresses, it sends block updates to neighbors. Some of those ++ // neighbors may affect the world so as to cause yet another redstone wire block to receive ++ // an update. If that happens, we need to integrate those redstone wire updates into the ++ // already on-going graph walk being performed by breadthFirstWalk. ++ return scheduleReentrantNeighborChanged(worldIn, pos, newState, source); ++ } ++ // If there are no on-going walks through redstone wire, then start a new walk. ++ ++ // If the source of the block update to the redstone wire at 'pos' is known, we can use ++ // that to help determine the direction of information flow. ++ if (source != null) { ++ final UpdateNode src = new UpdateNode(); ++ src.self = source; ++ src.parent = source; ++ src.visited = true; ++ nodeCache.put(source, src); ++ identifyNode(worldIn, src); ++ } ++ ++ // Create a node representing the block at 'pos', and then propagate updates ++ // to its neighbors. As stated above, the call to wire.calculateCurrentChanges ++ // already performs the update to the block at 'pos', so it is not added to the schedule. ++ final UpdateNode upd = new UpdateNode(); ++ upd.self = pos; ++ upd.parent = source!=null ? source : pos; ++ upd.currentState = newState; ++ upd.type = UpdateNode.Type.REDSTONE; ++ upd.visited = true; ++ nodeCache.put(pos, upd); ++ propagateChanges(worldIn, upd, 0); ++ ++ // Perform the walk over all directly reachable redstone wire blocks, propagating wire value ++ // updates in a breadth first order out from the initial update received for the block at 'pos'. ++ breadthFirstWalk(worldIn); ++ ++ // With the whole search completed, clear the list of all known blocks. ++ // We do not want to keep around state information that may be changed by other code. ++ // In theory, we could cache the neighbor block positions, but that is a separate ++ // optimization. ++ nodeCache.clear(); ++ ++ return newState; ++ } ++ ++ // For any array of neighbors in an UpdateNode object, these are always ++ // the indices of the four immediate neighbors at the same Y coordinate. ++ private static final int[] rs_neighbors = {4, 5, 6, 7}; ++ private static final int[] rs_neighbors_up = {9, 11, 13, 15}; ++ private static final int[] rs_neighbors_dn = {8, 10, 12, 14}; ++ ++ /* ++ * Updated calculateCurrentChanges that is optimized for speed and uses ++ * the UpdateNode's neighbor array to find the redstone states of neighbors ++ * that might power it. ++ */ ++ private BlockState calculateCurrentChanges(final Level worldIn, final UpdateNode upd) { ++ BlockState state = upd.currentState; ++ final int i = state.getValue(RedStoneWireBlock.POWER).intValue(); ++ int j = 0; ++ j = getMaxCurrentStrength(upd, j); ++ int l = 0; ++ ++ wire.shouldSignal = false; ++ // Unfortunately, World.isBlockIndirectlyGettingPowered is complicated, ++ // and I'm not ready to try to replicate even more functionality from ++ // elsewhere in Minecraft into this accelerator. So sadly, we must ++ // suffer the performance hit of this very expensive call. If there ++ // is consistency to what this call returns, we may be able to cache it. ++ final int k = worldIn.getBestNeighborSignal(upd.self); ++ wire.shouldSignal = true; ++ ++ // The variable 'k' holds the maximum redstone power value of any adjacent blocks. ++ // If 'k' has the highest level of all neighbors, then the power level of this ++ // redstone wire will be set to 'k'. If 'k' is already 15, then nothing inside the ++ // following loop can affect the power level of the wire. Therefore, the loop is ++ // skipped if k is already 15. ++ if (k < 15) { ++ if (upd.neighbor_nodes == null) { ++ // If this node's neighbors are not known, expand the node ++ findNeighbors(worldIn, upd); ++ } ++ ++ // These remain constant, so pull them out of the loop. ++ // Regardless of which direction is forward, the UpdateNode for the ++ // position directly above the node being calculated is always ++ // at index 1. ++ UpdateNode center_up = upd.neighbor_nodes[1]; ++ boolean center_up_is_cube = center_up.currentState.isRedstoneConductor(worldIn, center_up.self); // TODO ++ ++ for (int m = 0; m < 4; m++) { ++ // Get the neighbor array index of each of the four cardinal ++ // neighbors. ++ int n = rs_neighbors[m]; ++ ++ // Get the max redstone power level of each of the cardinal ++ // neighbors ++ UpdateNode neighbor = upd.neighbor_nodes[n]; ++ l = getMaxCurrentStrength(neighbor, l); ++ ++ // Also check the positions above and below the cardinal ++ // neighbors ++ boolean neighbor_is_cube = neighbor.currentState.isRedstoneConductor(worldIn, neighbor.self); // TODO ++ if (!neighbor_is_cube) { ++ UpdateNode neighbor_down = upd.neighbor_nodes[rs_neighbors_dn[m]]; ++ l = getMaxCurrentStrength(neighbor_down, l); ++ } else ++ if (!center_up_is_cube) { ++ UpdateNode neighbor_up = upd.neighbor_nodes[rs_neighbors_up[m]]; ++ l = getMaxCurrentStrength(neighbor_up, l); ++ } ++ } ++ } ++ ++ // The new code sets this RedstoneWire block's power level to the highest neighbor ++ // minus 1. This usually results in wire power levels dropping by 2 at a time. ++ // This optimization alone has no impact on update order, only the number of updates. ++ j = l - 1; ++ ++ // If 'l' turns out to be zero, then j will be set to -1, but then since 'k' will ++ // always be in the range of 0 to 15, the following if will correct that. ++ if (k > j) j = k; ++ ++ // egg82's amendment ++ // Adding Bukkit's BlockRedstoneEvent - er.. event. ++ if (i != j) { ++ BlockRedstoneEvent event = new BlockRedstoneEvent(CraftBlock.at(worldIn, upd.self), i, j); ++ worldIn.getCraftServer().getPluginManager().callEvent(event); ++ j = event.getNewCurrent(); ++ } ++ ++ if (i != j) { ++ // If the power level has changed from its previous value, compute a new state ++ // and set it in the world. ++ // Possible optimization: Don't commit state changes to the world until they ++ // need to be known by some nearby non-redstone-wire block. ++ BlockPos pos = new BlockPos(upd.self.getX(), upd.self.getY(), upd.self.getZ()); ++ if (wire.canSurvive(null, worldIn, pos)) { ++ state = state.setValue(RedStoneWireBlock.POWER, Integer.valueOf(j)); ++ // [Space Walker] suppress shape updates and emit those manually to ++ // bypass the new neighbor update stack. ++ if (worldIn.setBlock(upd.self, state, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS)) ++ updateNeighborShapes(worldIn, upd.self, state); ++ } ++ } ++ ++ return state; ++ } ++ ++ private static final Direction[] UPDATE_SHAPE_ORDER = { Direction.WEST, Direction.EAST, Direction.NORTH, Direction.SOUTH, Direction.DOWN, Direction.UP }; ++ ++ /* ++ * [Space Walker] ++ * This method emits shape updates around the given block, ++ * bypassing the new neighbor update stack. Diagonal shape ++ * updates are omitted, as they are mostly unnecessary. ++ * Diagonal shape updates are emitted exclusively to other ++ * redstone wires, in order to update their connection properties. ++ * Wire connections should never change as a result of power ++ * changes, so the only behavioral change will be in scenarios ++ * where earlier shape updates have been suppressed to keep a ++ * redstone wire in an invalid state. ++ */ ++ public void updateNeighborShapes(Level level, BlockPos pos, BlockState state) { ++ // these updates will be added to the stack and processed after the entire network has updated ++ state.updateIndirectNeighbourShapes(level, pos, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS); ++ ++ for (Direction dir : UPDATE_SHAPE_ORDER) { ++ BlockPos neighborPos = pos.relative(dir); ++ BlockState neighborState = level.getBlockState(neighborPos); ++ ++ BlockState newState = neighborState.updateShape(dir.getOpposite(), state, level, neighborPos, pos); ++ Block.updateOrDestroy(neighborState, newState, level, neighborPos, Block.UPDATE_CLIENTS); ++ } ++ } ++ ++ /* ++ * Optimized function to compute a redstone wire's power level based on cached ++ * state. ++ */ ++ private static int getMaxCurrentStrength(final UpdateNode upd, final int strength) { ++ if (upd.type != UpdateNode.Type.REDSTONE) return strength; ++ final int i = upd.currentState.getValue(RedStoneWireBlock.POWER).intValue(); ++ return i > strength ? i : strength; ++ } ++} +diff --git a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java +index c73bf0b36252796ca93c500affa2be568e3f6c9e..18ed178223cca85dbba65b1e07741622e266d318 100644 +--- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java +@@ -258,6 +258,116 @@ public class RedStoneWireBlock extends Block { + return floor.isFaceSturdy(world, pos, Direction.UP) || floor.is(Blocks.HOPPER); + } + ++ // Paper start - Optimize redstone ++ // The bulk of the new functionality is found in RedstoneWireTurbo.java ++ com.destroystokyo.paper.util.RedstoneWireTurbo turbo = new com.destroystokyo.paper.util.RedstoneWireTurbo(this); ++ ++ /* ++ * Modified version of pre-existing updateSurroundingRedstone, which is called from ++ * this.neighborChanged and a few other methods in this class. ++ * Note: Added 'source' argument so as to help determine direction of information flow ++ */ ++ private void updateSurroundingRedstone(Level worldIn, BlockPos pos, BlockState state, BlockPos source) { ++ if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.EIGENCRAFT) { ++ turbo.updateSurroundingRedstone(worldIn, pos, state, source); ++ return; ++ } ++ updatePowerStrength(worldIn, pos, state); ++ } ++ ++ /* ++ * Slightly modified method to compute redstone wire power levels from neighboring blocks. ++ * Modifications cut the number of power level changes by about 45% from vanilla, and this ++ * optimization synergizes well with the breadth-first search implemented in ++ * RedstoneWireTurbo. ++ * Note: RedstoneWireTurbo contains a faster version of this code. ++ * Note: Made this public so that RedstoneWireTurbo can access it. ++ */ ++ public BlockState calculateCurrentChanges(Level worldIn, BlockPos pos1, BlockPos pos2, BlockState state) { ++ BlockState iblockstate = state; ++ int i = state.getValue(POWER); ++ int j = 0; ++ j = this.getPower(j, worldIn.getBlockState(pos2)); ++ this.shouldSignal = false; ++ int k = worldIn.getBestNeighborSignal(pos1); ++ this.shouldSignal = true; ++ ++ if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA) { ++ // This code is totally redundant to if statements just below the loop. ++ if (k > 0 && k > j - 1) { ++ j = k; ++ } ++ } ++ ++ int l = 0; ++ ++ // The variable 'k' holds the maximum redstone power value of any adjacent blocks. ++ // If 'k' has the highest level of all neighbors, then the power level of this ++ // redstone wire will be set to 'k'. If 'k' is already 15, then nothing inside the ++ // following loop can affect the power level of the wire. Therefore, the loop is ++ // skipped if k is already 15. ++ if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA || k < 15) { ++ for (Direction enumfacing : Direction.Plane.HORIZONTAL) { ++ BlockPos blockpos = pos1.relative(enumfacing); ++ boolean flag = blockpos.getX() != pos2.getX() || blockpos.getZ() != pos2.getZ(); ++ ++ if (flag) { ++ l = this.getPower(l, worldIn.getBlockState(blockpos)); ++ } ++ ++ if (worldIn.getBlockState(blockpos).isRedstoneConductor(worldIn, blockpos) && !worldIn.getBlockState(pos1.above()).isRedstoneConductor(worldIn, pos1)) { ++ if (flag && pos1.getY() >= pos2.getY()) { ++ l = this.getPower(l, worldIn.getBlockState(blockpos.above())); ++ } ++ } else if (!worldIn.getBlockState(blockpos).isRedstoneConductor(worldIn, blockpos) && flag && pos1.getY() <= pos2.getY()) { ++ l = this.getPower(l, worldIn.getBlockState(blockpos.below())); ++ } ++ } ++ } ++ ++ if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA) { ++ // The old code would decrement the wire value only by 1 at a time. ++ if (l > j) { ++ j = l - 1; ++ } else if (j > 0) { ++ --j; ++ } else { ++ j = 0; ++ } ++ ++ if (k > j - 1) { ++ j = k; ++ } ++ } else { ++ // The new code sets this RedstoneWire block's power level to the highest neighbor ++ // minus 1. This usually results in wire power levels dropping by 2 at a time. ++ // This optimization alone has no impact on update order, only the number of updates. ++ j = l - 1; ++ ++ // If 'l' turns out to be zero, then j will be set to -1, but then since 'k' will ++ // always be in the range of 0 to 15, the following if will correct that. ++ if (k > j) j = k; ++ } ++ ++ if (i != j) { ++ org.bukkit.event.block.BlockRedstoneEvent event = new org.bukkit.event.block.BlockRedstoneEvent(org.bukkit.craftbukkit.block.CraftBlock.at(worldIn, pos1), i, j); ++ worldIn.getCraftServer().getPluginManager().callEvent(event); ++ ++ j = event.getNewCurrent(); ++ state = state.setValue(POWER, j); ++ ++ if (worldIn.getBlockState(pos1) == iblockstate) { ++ // [Space Walker] suppress shape updates and emit those manually to ++ // bypass the new neighbor update stack. ++ if (worldIn.setBlock(pos1, state, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS)) ++ turbo.updateNeighborShapes(worldIn, pos1, state); ++ } ++ } ++ ++ return state; ++ } ++ // Paper end ++ + private void updatePowerStrength(Level world, BlockPos pos, BlockState state) { + int i = this.calculateTargetStrength(world, pos); + +@@ -327,6 +437,7 @@ public class RedStoneWireBlock extends Block { + return Math.max(i, j - 1); + } + ++ private int getPower(int min, BlockState iblockdata) { return Math.max(min, getWireSignal(iblockdata)); } // Paper - Optimize redstone + private int getWireSignal(BlockState state) { + return state.is((Block) this) ? (Integer) state.getValue(RedStoneWireBlock.POWER) : 0; + } +@@ -349,7 +460,7 @@ public class RedStoneWireBlock extends Block { + @Override + protected void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) { + if (!oldState.is(state.getBlock()) && !world.isClientSide) { +- this.updatePowerStrength(world, pos, state); ++ this.updateSurroundingRedstone(world, pos, state, null); // Paper - Optimize redstone + Iterator iterator = Direction.Plane.VERTICAL.iterator(); + + while (iterator.hasNext()) { +@@ -376,7 +487,7 @@ public class RedStoneWireBlock extends Block { + world.updateNeighborsAt(pos.relative(enumdirection), this); + } + +- this.updatePowerStrength(world, pos, state); ++ this.updateSurroundingRedstone(world, pos, state, null); // Paper - Optimize redstone + this.updateNeighborsOfNeighboringWires(world, pos); + } + } +@@ -411,7 +522,7 @@ public class RedStoneWireBlock extends Block { + protected void neighborChanged(BlockState state, Level world, BlockPos pos, Block sourceBlock, BlockPos sourcePos, boolean notify) { + if (!world.isClientSide) { + if (state.canSurvive(world, pos)) { +- this.updatePowerStrength(world, pos, state); ++ this.updateSurroundingRedstone(world, pos, state, sourcePos); // Paper - Optimize redstone + } else { + dropResources(state, world, pos); + world.removeBlock(pos, false); diff --git a/patches/server/1008-Remove-streams-from-hot-code.patch b/patches/server/1008-Remove-streams-from-hot-code.patch deleted file mode 100644 index 4c6d94f7bf..0000000000 --- a/patches/server/1008-Remove-streams-from-hot-code.patch +++ /dev/null @@ -1,215 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: JRoy -Date: Wed, 1 Jul 2020 18:01:49 -0400 -Subject: [PATCH] Remove streams from hot code - -Co-authored-by: Bjarne Koll -Co-authored-by: Spottedleaf - -diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java b/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java -index 6a270c9adb044a6e0b1c8e09b9106d51989fd761..d4581127366736c54f74e4ef7479236b18fb487d 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java -+++ b/src/main/java/net/minecraft/world/entity/ai/behavior/GateBehavior.java -@@ -57,7 +57,7 @@ public class GateBehavior implements BehaviorControl - if (this.hasRequiredMemories(entity)) { - this.status = Behavior.Status.RUNNING; - this.orderPolicy.apply(this.behaviors); -- this.runningPolicy.apply(this.behaviors.stream(), world, entity, time); -+ this.runningPolicy.apply(this.behaviors, world, entity, time); // Paper - Perf: Remove streams from hot code - return true; - } else { - return false; -@@ -66,7 +66,13 @@ public class GateBehavior implements BehaviorControl - - @Override - public final void tickOrStop(ServerLevel world, E entity, long time) { -- this.behaviors.stream().filter(task -> task.getStatus() == Behavior.Status.RUNNING).forEach(task -> task.tickOrStop(world, entity, time)); -+ // Paper start - Perf: Remove streams from hot code -+ for (final BehaviorControl task : this.behaviors) { -+ if (task.getStatus() == Behavior.Status.RUNNING) { -+ task.tickOrStop(world, entity, time); -+ } -+ } -+ // Paper end - Perf: Remove streams from hot code - if (this.behaviors.stream().noneMatch(task -> task.getStatus() == Behavior.Status.RUNNING)) { - this.doStop(world, entity, time); - } -@@ -75,8 +81,16 @@ public class GateBehavior implements BehaviorControl - @Override - public final void doStop(ServerLevel world, E entity, long time) { - this.status = Behavior.Status.STOPPED; -- this.behaviors.stream().filter(task -> task.getStatus() == Behavior.Status.RUNNING).forEach(task -> task.doStop(world, entity, time)); -- this.exitErasedMemories.forEach(entity.getBrain()::eraseMemory); -+ // Paper start - Perf: Remove streams from hot code -+ for (final BehaviorControl task : this.behaviors) { -+ if (task.getStatus() == Behavior.Status.RUNNING) { -+ task.doStop(world, entity, time); -+ } -+ } -+ for (final MemoryModuleType exitErasedMemory : this.exitErasedMemories) { -+ entity.getBrain().eraseMemory(exitErasedMemory); -+ } -+ // Paper end - Perf: Remove streams from hot code - } - - @Override -@@ -111,18 +125,30 @@ public class GateBehavior implements BehaviorControl - - public static enum RunningPolicy { - RUN_ONE { -+ // Paper start - Perf: Remove streams from hot code - @Override -- public void apply(Stream> tasks, ServerLevel world, E entity, long time) { -- tasks.filter(task -> task.getStatus() == Behavior.Status.STOPPED).filter(task -> task.tryStart(world, entity, time)).findFirst(); -+ public void apply(ShufflingList> tasks, ServerLevel world, E entity, long time) { -+ for (final BehaviorControl task : tasks) { -+ if (task.getStatus() == Behavior.Status.STOPPED && task.tryStart(world, entity, time)) { -+ break; -+ } -+ } -+ // Paper end - Perf: Remove streams from hot code - } - }, - TRY_ALL { -+ // Paper start - Perf: Remove streams from hot code - @Override -- public void apply(Stream> tasks, ServerLevel world, E entity, long time) { -- tasks.filter(task -> task.getStatus() == Behavior.Status.STOPPED).forEach(task -> task.tryStart(world, entity, time)); -+ public void apply(ShufflingList> tasks, ServerLevel world, E entity, long time) { -+ for (final BehaviorControl task : tasks) { -+ if (task.getStatus() == Behavior.Status.STOPPED) { -+ task.tryStart(world, entity, time); -+ } -+ } -+ // Paper end - Perf: Remove streams from hot code - } - }; - -- public abstract void apply(Stream> tasks, ServerLevel world, E entity, long time); -+ public abstract void apply(ShufflingList> tasks, ServerLevel world, E entity, long time); // Paper - Perf: Remove streams from hot code - } - } -diff --git a/src/main/java/net/minecraft/world/entity/ai/gossip/GossipContainer.java b/src/main/java/net/minecraft/world/entity/ai/gossip/GossipContainer.java -index aa32804bc9affe9a615d3ffaa513f6f09aab3f32..c7f012674361a323c1efeca4660cd3f46d308ee1 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/gossip/GossipContainer.java -+++ b/src/main/java/net/minecraft/world/entity/ai/gossip/GossipContainer.java -@@ -59,8 +59,22 @@ public class GossipContainer { - return this.gossips.entrySet().stream().flatMap(entry -> entry.getValue().unpack(entry.getKey())); - } - -+ // Paper start - Perf: Remove streams from hot code -+ private List decompress() { -+ List list = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>(); -+ for (Map.Entry entry : this.gossips.entrySet()) { -+ for (GossipContainer.GossipEntry cur : entry.getValue().decompress(entry.getKey())) { -+ if (cur.weightedValue() != 0) { -+ list.add(cur); -+ } -+ } -+ } -+ return list; -+ } -+ // Paper end - Perf: Remove streams from hot code -+ - private Collection selectGossipsForTransfer(RandomSource random, int count) { -- List list = this.unpack().toList(); -+ List list = this.decompress(); // Paper - Perf: Remove streams from hot code - if (list.isEmpty()) { - return Collections.emptyList(); - } else { -@@ -145,7 +159,7 @@ public class GossipContainer { - - public T store(DynamicOps ops) { - return GossipContainer.GossipEntry.LIST_CODEC -- .encodeStart(ops, this.unpack().toList()) -+ .encodeStart(ops, this.decompress()) // Paper - Perf: Remove streams from hot code - .resultOrPartial(error -> LOGGER.warn("Failed to serialize gossips: {}", error)) - .orElseGet(ops::emptyList); - } -@@ -172,12 +186,23 @@ public class GossipContainer { - final Object2IntMap entries = new Object2IntOpenHashMap<>(); - - public int weightedValue(Predicate gossipTypeFilter) { -- return this.entries -- .object2IntEntrySet() -- .stream() -- .filter(entry -> gossipTypeFilter.test(entry.getKey())) -- .mapToInt(entry -> entry.getIntValue() * entry.getKey().weight) -- .sum(); -+ // Paper start - Perf: Remove streams from hot code -+ int weight = 0; -+ for (Object2IntMap.Entry entry : entries.object2IntEntrySet()) { -+ if (gossipTypeFilter.test(entry.getKey())) { -+ weight += entry.getIntValue() * entry.getKey().weight; -+ } -+ } -+ return weight; -+ } -+ -+ public List decompress(UUID uuid) { -+ List list = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>(); -+ for (Object2IntMap.Entry entry : entries.object2IntEntrySet()) { -+ list.add(new GossipContainer.GossipEntry(uuid, entry.getKey(), entry.getIntValue())); -+ } -+ return list; -+ // Paper end - Perf: Remove streams from hot code - } - - public Stream unpack(UUID target) { -diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java -index 28fd073ddad358e087e8c78985a97cad21be81b7..a5bd308d1b3ea5db185c06a287167d1d8894a987 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java -+++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java -@@ -24,13 +24,17 @@ public class NearestItemSensor extends Sensor { - @Override - protected void doTick(ServerLevel world, Mob entity) { - Brain brain = entity.getBrain(); -- List list = world.getEntitiesOfClass(ItemEntity.class, entity.getBoundingBox().inflate(32.0, 16.0, 32.0), itemEntity -> true); -+ List list = world.getEntitiesOfClass(ItemEntity.class, entity.getBoundingBox().inflate(32.0, 16.0, 32.0), itemEntity -> itemEntity.closerThan(entity, MAX_DISTANCE_TO_WANTED_ITEM) && entity.wantsToPickUp(itemEntity.getItem())); // Paper - Perf: Move predicate into getEntities - list.sort(Comparator.comparingDouble(entity::distanceToSqr)); -- Optional optional = list.stream() -- .filter(itemEntity -> entity.wantsToPickUp(itemEntity.getItem())) -- .filter(itemEntity -> itemEntity.closerThan(entity, 32.0)) -- .filter(entity::hasLineOfSight) -- .findFirst(); -- brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_WANTED_ITEM, optional); -+ // Paper start - Perf: remove streams from hot code -+ ItemEntity nearest = null; -+ for (ItemEntity entityItem : list) { -+ if (entity.hasLineOfSight(entityItem)) { // Paper - Perf: Move predicate into getEntities -+ nearest = entityItem; -+ break; -+ } -+ } -+ brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_WANTED_ITEM, Optional.ofNullable(nearest)); -+ // Paper end - Perf: remove streams from hot code - } - } -diff --git a/src/main/java/net/minecraft/world/level/levelgen/Beardifier.java b/src/main/java/net/minecraft/world/level/levelgen/Beardifier.java -index 04552e2cf3de2dc41dfe7e7a2e88b200eaaf280b..ca93a97256350789ca56f910862c9d717ca7670b 100644 ---- a/src/main/java/net/minecraft/world/level/levelgen/Beardifier.java -+++ b/src/main/java/net/minecraft/world/level/levelgen/Beardifier.java -@@ -35,9 +35,10 @@ public class Beardifier implements DensityFunctions.BeardifierOrMarker { - int j = pos.getMinBlockZ(); - ObjectList objectList = new ObjectArrayList<>(10); - ObjectList objectList2 = new ObjectArrayList<>(32); -- world.startsForStructure(pos, structure -> structure.terrainAdaptation() != TerrainAdjustment.NONE) -- .forEach( -- start -> { -+ // Paper start - Perf: Remove streams from hot code -+ for (net.minecraft.world.level.levelgen.structure.StructureStart start : world.startsForStructure(pos, (structure) -> { -+ return structure.terrainAdaptation() != TerrainAdjustment.NONE; -+ })) { // Paper end - Perf: Remove streams from hot code - TerrainAdjustment terrainAdjustment = start.getStructure().terrainAdaptation(); - - for (StructurePiece structurePiece : start.getPieces()) { -@@ -65,8 +66,7 @@ public class Beardifier implements DensityFunctions.BeardifierOrMarker { - } - } - } -- } -- ); -+ } // Paper - Perf: Remove streams from hot code - return new Beardifier(objectList.iterator(), objectList2.iterator()); - } - diff --git a/patches/server/1009-Eigencraft-redstone-implementation.patch b/patches/server/1009-Eigencraft-redstone-implementation.patch deleted file mode 100644 index b3e3db6504..0000000000 --- a/patches/server/1009-Eigencraft-redstone-implementation.patch +++ /dev/null @@ -1,1145 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: theosib -Date: Thu, 27 Sep 2018 01:43:35 -0600 -Subject: [PATCH] Eigencraft redstone implementation - -Author: theosib - -Original license: MIT - -This patch implements theosib's redstone algorithms to completely overhaul the way redstone works. -The new algorithms should be many times faster than current vanilla ones. -From the original author's comments, it looks like it shouldn't interfere with any redstone save for very extreme edge-cases. - -Surprisingly, not a lot was touched aside from a few obfuscation helpers and BlockRedstoneWire. -A lot of this code is self-contained in a helper class. - -Aside from making the obvious class/function renames and obfhelpers I didn't need to modify much. -Just added Bukkit's event system and took a few liberties with dead code and comment misspellings. - -== AT == -public net.minecraft.world.level.block.RedStoneWireBlock shouldSignal -public net.minecraft.world.level.block.RedStoneWireBlock canSurvive(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/LevelReader;Lnet/minecraft/core/BlockPos;)Z - -Co-authored-by: egg82 - -diff --git a/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java -new file mode 100644 -index 0000000000000000000000000000000000000000..9f17170179cc99d84ad25a1e838aff3d8cc66f93 ---- /dev/null -+++ b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java -@@ -0,0 +1,958 @@ -+package com.destroystokyo.paper.util; -+ -+import java.util.List; -+import java.util.Map; -+import java.util.concurrent.ThreadLocalRandom; -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.Direction; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.item.Items; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.block.Block; -+import net.minecraft.world.level.block.RedStoneWireBlock; -+import net.minecraft.world.level.block.state.BlockState; -+import org.bukkit.craftbukkit.block.CraftBlock; -+import org.bukkit.event.block.BlockRedstoneEvent; -+ -+import com.google.common.collect.Lists; -+import com.google.common.collect.Maps; -+ -+/** -+ * Used for the faster redstone algorithm. -+ * Original author: theosib -+ * Original license: MIT -+ * -+ * Ported to Paper and updated to 1.13 by egg82 -+ */ -+public class RedstoneWireTurbo { -+ /* -+ * This is Helper class for BlockRedstoneWire. It implements a minimally-invasive -+ * bolt-on accelerator that performs a breadth-first search through redstone wire blocks -+ * in order to more efficiently and deterministically compute new redstone wire power levels -+ * and determine the order in which other blocks should be updated. -+ * -+ * Features: -+ * - Changes to BlockRedstoneWire are very limited, no other classes are affected, and the -+ * choice between old and new redstone wire update algorithms is switchable on-line. -+ * - The vanilla implementation relied on World.notifyNeighborsOfStateChange for redstone -+ * wire blocks to communicate power level changes to each other, generating 36 block -+ * updates per call. This improved implementation propagates power level changes directly -+ * between redstone wire blocks. Redstone wire power levels are therefore computed more quickly, -+ * and block updates are sent only to non-redstone blocks, many of which may perform an -+ * action when informed of a change in redstone power level. (Note: Block updates are not -+ * the same as state changes to redstone wire. Wire block states are updated as soon -+ * as they are computed.) -+ * - Of the 36 block updates generated by a call to World.notifyNeighborsOfStateChange, -+ * 12 of them are obviously redundant (e.g. the west neighbor of the east neighbor). -+ * These are eliminated. -+ * - Updates to redstone wire and other connected blocks are propagated in a breath-first -+ * manner, radiating out from the initial trigger (a block update to a redstone wire -+ * from something other than redstone wire). -+ * - Updates are scheduled both deterministically and in an intuitive order, addressing bug -+ * MC-11193. -+ * - All redstone behavior that used to be locational now works the same in all locations. -+ * - All behaviors of redstone wire that used to be orientational now work the same in all -+ * orientations, as long as orientation can be determined; random otherwise. Some other -+ * redstone components still update directionally (e.g. switches), and this code can't -+ * compensate for that. -+ * - Information that is otherwise computed over and over again or which is expensive to -+ * to compute is cached for faster lookup. This includes coordinates of block position -+ * neighbors and block states that won't change behind our backs during the execution of -+ * this search algorithm. -+ * - Redundant block updates (both to redstone wire and to other blocks) are heavily -+ * consolidated. For worst-case scenarios (depowering of redstone wire) this results -+ * in a reduction of block updates by as much as 95% (factor of 1/21). Due to overheads, -+ * empirical testing shows a speedup better than 10x. This addresses bug MC-81098. -+ * -+ * Extensive testing has been performed to ensure that existing redstone contraptions still -+ * behave as expected. Results of early testing that identified undesirable behavior changes -+ * were addressed. Additionally, real-time performance testing revealed compute inefficiencies -+ * With earlier implementations of this accelerator. Some compatibility adjustments and -+ * performance optimizations resulted in harmless increases in block updates above the -+ * theoretical minimum. -+ * -+ * Only a single redstone machine was found to break: An instant dropper line hack that -+ * relies on powered rails and quasi-connectivity but doesn't work in all directions. The -+ * replacement is to lay redstone wire directly on top of the dropper line, which now works -+ * reliably in any direction. -+ * -+ * There are numerous other optimization that can be made, but those will be provided later in -+ * separate updates. This version is designed to be minimalistic. -+ * -+ * Many thanks to the following individuals for their help in testing this functionality: -+ * - pokechu22, _MethodZz_, WARBEN, NarcolepticFrog, CommandHelper (nessie), ilmango, -+ * OreoLamp, Xcom6000, tryashtar, RedCMD, Smokey95Dog, EDDxample, Rays Works, -+ * Nodnam, BlockyPlays, Grumm, NeunEinser, HelVince. -+ */ -+ -+ /* Reference to BlockRedstoneWire object, which uses this accelerator */ -+ private final RedStoneWireBlock wire; -+ -+ /* -+ * Implementation: -+ * -+ * RedstoneWire Blocks are updated in concentric rings or "layers" radiating out from the -+ * initial block update that came from a call to BlockRedstoneWire.neighborChanged(). -+ * All nodes put in Layer N are those with Manhattan distance N from the trigger -+ * position, reachable through connected redstone wire blocks. -+ * -+ * Layer 0 represents the trigger block position that was input to neighborChanged. -+ * Layer 1 contains the immediate neighbors of that position. -+ * Layer N contains the neighbors of blocks in layer N-1, not including -+ * those in previous layers. -+ * -+ * Layers enforce an update order that is a function of Manhattan distance -+ * from the initial coordinates input to neighborChanged. The same -+ * coordinates may appear in multiple layers, but redundant updates are minimized. -+ * Block updates are sent layer-by-layer. If multiple of a block's neighbors experience -+ * redstone wire changes before its layer is processed, then those updates will be merged. -+ * If a block's update has been sent, but its neighboring redstone changes -+ * after that, then another update will be sent. This preserves compatibility with -+ * machines that rely on zero-tick behavior, except that the new functionality is non- -+ * locational. -+ * -+ * Within each layer, updates are ordered left-to-right relative to the direction of -+ * information flow. This makes the implementation non-orientational. Only when -+ * this direction is ambiguous is randomness applied (intentionally). -+ */ -+ private List updateQueue0 = Lists.newArrayList(); -+ private List updateQueue1 = Lists.newArrayList(); -+ private List updateQueue2 = Lists.newArrayList(); -+ -+ public RedstoneWireTurbo(RedStoneWireBlock wire) { -+ this.wire = wire; -+ } -+ -+ /* -+ * Compute neighbors of a block. When a redstone wire value changes, previously it called -+ * World.notifyNeighborsOfStateChange. That lists immediately neighboring blocks in -+ * west, east, down, up, north, south order. For each of those neighbors, their own -+ * neighbors are updated in the same order. This generates 36 updates, but 12 of them are -+ * redundant; for instance the west neighbor of a block's east neighbor. -+ * -+ * Note that this ordering is only used to create the initial list of neighbors. Once -+ * the direction of signal flow is identified, the ordering of updates is completely -+ * reorganized. -+ */ -+ public static BlockPos[] computeAllNeighbors(final BlockPos pos) { -+ final int x = pos.getX(); -+ final int y = pos.getY(); -+ final int z = pos.getZ(); -+ final BlockPos[] n = new BlockPos[24]; -+ -+ // Immediate neighbors, in the same order as -+ // World.notifyNeighborsOfStateChange, etc.: -+ // west, east, down, up, north, south -+ n[0] = new BlockPos(x - 1, y, z); -+ n[1] = new BlockPos(x + 1, y, z); -+ n[2] = new BlockPos(x, y - 1, z); -+ n[3] = new BlockPos(x, y + 1, z); -+ n[4] = new BlockPos(x, y, z - 1); -+ n[5] = new BlockPos(x, y, z + 1); -+ -+ // Neighbors of neighbors, in the same order, -+ // except that duplicates are not included -+ n[6] = new BlockPos(x - 2, y, z); -+ n[7] = new BlockPos(x - 1, y - 1, z); -+ n[8] = new BlockPos(x - 1, y + 1, z); -+ n[9] = new BlockPos(x - 1, y, z - 1); -+ n[10] = new BlockPos(x - 1, y, z + 1); -+ n[11] = new BlockPos(x + 2, y, z); -+ n[12] = new BlockPos(x + 1, y - 1, z); -+ n[13] = new BlockPos(x + 1, y + 1, z); -+ n[14] = new BlockPos(x + 1, y, z - 1); -+ n[15] = new BlockPos(x + 1, y, z + 1); -+ n[16] = new BlockPos(x, y - 2, z); -+ n[17] = new BlockPos(x, y - 1, z - 1); -+ n[18] = new BlockPos(x, y - 1, z + 1); -+ n[19] = new BlockPos(x, y + 2, z); -+ n[20] = new BlockPos(x, y + 1, z - 1); -+ n[21] = new BlockPos(x, y + 1, z + 1); -+ n[22] = new BlockPos(x, y, z - 2); -+ n[23] = new BlockPos(x, y, z + 2); -+ return n; -+ } -+ -+ /* -+ * We only want redstone wires to update redstone wires that are -+ * immediately adjacent. Some more distant updates can result -+ * in cross-talk that (a) wastes time and (b) can make the update -+ * order unintuitive. Therefore (relative to the neighbor order -+ * computed by computeAllNeighbors), updates are not scheduled -+ * for redstone wire in those non-connecting positions. On the -+ * other hand, updates will always be sent to *other* types of blocks -+ * in any of the 24 neighboring positions. -+ */ -+ private static final boolean[] update_redstone = { -+ true, true, false, false, true, true, // 0 to 5 -+ false, true, true, false, false, false, // 6 to 11 -+ true, true, false, false, false, true, // 12 to 17 -+ true, false, true, true, false, false // 18 to 23 -+ }; -+ -+ // Internal numbering for cardinal directions -+ private static final int North = 0; -+ private static final int East = 1; -+ private static final int South = 2; -+ private static final int West = 3; -+ -+ /* -+ * These lookup tables completely remap neighbor positions into a left-to-right -+ * ordering, based on the cardinal direction that is determined to be forward. -+ * See below for more explanation. -+ */ -+ private static final int[] forward_is_north = {2, 3, 16, 19, 0, 4, 1, 5, 7, 8, 17, 20, 12, 13, 18, 21, 6, 9, 22, 14, 11, 10, 23, 15}; -+ private static final int[] forward_is_east = {2, 3, 16, 19, 4, 1, 5, 0, 17, 20, 12, 13, 18, 21, 7, 8, 22, 14, 11, 15, 23, 9, 6, 10}; -+ private static final int[] forward_is_south = {2, 3, 16, 19, 1, 5, 0, 4, 12, 13, 18, 21, 7, 8, 17, 20, 11, 15, 23, 10, 6, 14, 22, 9}; -+ private static final int[] forward_is_west = {2, 3, 16, 19, 5, 0, 4, 1, 18, 21, 7, 8, 17, 20, 12, 13, 23, 10, 6, 9, 22, 15, 11, 14}; -+ -+ /* For any orientation, we end up with the update order defined below. This order is relative to any redstone wire block -+ * that is itself having an update computed, and this center position is marked with C. -+ * - The update position marked 0 is computed first, and the one marked 23 is last. -+ * - Forward is determined by the local direction of information flow into position C from prior updates. -+ * - The first updates are scheduled for the four positions below and above C. -+ * - Then updates are scheduled for the four horizontal neighbors of C, followed by the positions below and above those neighbors. -+ * - Finally, updates are scheduled for the remaining positions with Manhattan distance 2 from C (at the same Y coordinate). -+ * - For a given horizontal distance from C, updates are scheduled starting from directly left and stepping clockwise to directly -+ * right. The remaining positions behind C are scheduled counterclockwise so as to maintain the left-to-right ordering. -+ * - If C is in layer N of the update schedule, then all 24 positions may be scheduled for layer N+1. For redstone wire, no -+ * updates are scheduled for positions that cannot directly connect. Additionally, the four positions above and below C -+ * are ALSO scheduled for layer N+2. -+ * - This update order was selected after experimenting with a number of alternative schedules, based on its compatibility -+ * with existing redstone designs and behaviors that were considered to be intuitive by various testers. WARBEN in particular -+ * made some of the most challenging test cases, but the 3-tick clocks (made by RedCMD) were also challenging to fix, -+ * along with the rail-based instant dropper line built by ilmango. Numerous others made test cases as well, including -+ * NarcolepticFrog, nessie, and Pokechu22. -+ * -+ * - The forward direction is determined locally. So when there are branches in the redstone wire, the left one will get updated -+ * before the right one. Each branch can have its own relative forward direction, resulting in the left side of a left branch -+ * having priority over the right branch of a left branch, which has priority over the left branch of a right branch, followed -+ * by the right branch of a right branch. And so forth. Since redstone power reduces to zero after a path distance of 15, -+ * that imposes a practical limit on the branching. Note that the branching is not tracked explicitly -- relative forward -+ * directions dictate relative sort order, which maintains the proper global ordering. This also makes it unnecessary to be -+ * concerned about branches meeting up with each other. -+ * -+ * ^ -+ * | -+ * Forward -+ * <-- Left Right --> -+ * -+ * 18 -+ * 10 17 5 19 11 -+ * 2 8 0 12 16 4 C 6 20 9 1 13 3 -+ * 14 21 7 23 15 -+ * Further 22 Further -+ * Down Down Up Up -+ * -+ * Backward -+ * | -+ * V -+ */ -+ -+ // This allows the above remapping tables to be looked up by cardial direction index -+ private static final int[][] reordering = { forward_is_north, forward_is_east, forward_is_south, forward_is_west }; -+ -+ /* -+ * Input: Array of UpdateNode objects in an order corresponding to the positions -+ * computed by computeAllNeighbors above. -+ * Output: Array of UpdateNode objects oriented using the above remapping tables -+ * corresponding to the identified heading (direction of information flow). -+ */ -+ private static void orientNeighbors(final UpdateNode[] src, final UpdateNode[] dst, final int heading) { -+ final int[] re = reordering[heading]; -+ for (int i = 0; i < 24; i++) { -+ dst[i] = src[re[i]]; -+ } -+ } -+ -+ /* -+ * Structure to keep track of redstone wire blocks and -+ * neighbors that will receive updates. -+ */ -+ private static class UpdateNode { -+ public static enum Type { -+ UNKNOWN, REDSTONE, OTHER -+ } -+ -+ BlockState currentState; // Keep track of redstone wire value -+ UpdateNode[] neighbor_nodes; // References to neighbors (directed graph edges) -+ BlockPos self; // UpdateNode's own position -+ BlockPos parent; // Which block pos spawned/updated this node -+ Type type = Type.UNKNOWN; // unknown, redstone wire, other type of block -+ int layer; // Highest layer this node is scheduled in -+ boolean visited; // To keep track of information flow direction, visited restone wire is marked -+ int xbias, zbias; // Remembers directionality of ancestor nodes; helps eliminate directional ambiguities. -+ } -+ -+ /* -+ * Keep track of all block positions discovered during search and their current states. -+ * We want to remember one entry for each position. -+ */ -+ private final Map nodeCache = Maps.newHashMap(); -+ -+ /* -+ * For a newly created UpdateNode object, determine what type of block it is. -+ */ -+ private void identifyNode(final Level worldIn, final UpdateNode upd1) { -+ final BlockPos pos = upd1.self; -+ final BlockState oldState = worldIn.getBlockState(pos); -+ upd1.currentState = oldState; -+ -+ // Some neighbors of redstone wire are other kinds of blocks. -+ // These need to receive block updates to inform them that -+ // redstone wire values have changed. -+ final Block block = oldState.getBlock(); -+ if (block != wire) { -+ // Mark this block as not redstone wire and therefore -+ // requiring updates -+ upd1.type = UpdateNode.Type.OTHER; -+ -+ // Non-redstone blocks may propagate updates, but those updates -+ // are not handled by this accelerator. Therefore, we do not -+ // expand this position's neighbors. -+ return; -+ } -+ -+ // One job of BlockRedstoneWire.neighborChanged is to convert -+ // redstone wires to items if the block beneath was removed. -+ // With this accelerator, BlockRedstoneWire.neighborChanged -+ // is only typically called for a single wire block, while -+ // others are processed internally by the breadth first search -+ // algorithm. To preserve this game behavior, this check must -+ // be replicated here. -+ if (!wire.canSurvive(null, worldIn, pos)) { -+ // Pop off the redstone dust -+ Block.popResource(worldIn, pos, new ItemStack(Items.REDSTONE)); // TODO -+ worldIn.removeBlock(pos, false); -+ -+ // Mark this position as not being redstone wire -+ upd1.type = UpdateNode.Type.OTHER; -+ -+ // Note: Sending updates to air blocks leads to an empty method. -+ // Testing shows this to be faster than explicitly avoiding updates to -+ // air blocks. -+ return; -+ } -+ -+ // If the above conditions fail, then this is a redstone wire block. -+ upd1.type = UpdateNode.Type.REDSTONE; -+ } -+ -+ /* -+ * Given which redstone wire blocks have been visited and not visited -+ * around the position currently being updated, compute the cardinal -+ * direction that is "forward." -+ * -+ * rx is the forward direction along the West/East axis -+ * rz is the forward direction along the North/South axis -+ */ -+ static private int computeHeading(final int rx, final int rz) { -+ // rx and rz can only take on values -1, 0, and 1, so we can -+ // compute a code number that allows us to use a single switch -+ // to determine the heading. -+ final int code = (rx + 1) + 3 * (rz + 1); -+ switch (code) { -+ case 0: { -+ // Both rx and rz are -1 (northwest) -+ // Randomly choose one to be forward. -+ final int j = ThreadLocalRandom.current().nextInt(0, 1); -+ return (j == 0) ? North : West; -+ } -+ case 1: { -+ // rx=0, rz=-1 -+ // Definitively North -+ return North; -+ } -+ case 2: { -+ // rx=1, rz=-1 (northeast) -+ // Choose randomly between north and east -+ final int j = ThreadLocalRandom.current().nextInt(0, 1); -+ return (j == 0) ? North : East; -+ } -+ case 3: { -+ // rx=-1, rz=0 -+ // Definitively West -+ return West; -+ } -+ case 4: { -+ // rx=0, rz=0 -+ // Heading is completely ambiguous. Choose -+ // randomly among the four cardinal directions. -+ return ThreadLocalRandom.current().nextInt(0, 4); -+ } -+ case 5: { -+ // rx=1, rz=0 -+ // Definitively East -+ return East; -+ } -+ case 6: { -+ // rx=-1, rz=1 (southwest) -+ // Choose randomly between south and west -+ final int j = ThreadLocalRandom.current().nextInt(0, 1); -+ return (j == 0) ? South : West; -+ } -+ case 7: { -+ // rx=0, rz=1 -+ // Definitively South -+ return South; -+ } -+ case 8: { -+ // rx=1, rz=1 (southeast) -+ // Choose randomly between south and east -+ final int j = ThreadLocalRandom.current().nextInt(0, 1); -+ return (j == 0) ? South : East; -+ } -+ } -+ -+ // We should never get here -+ return ThreadLocalRandom.current().nextInt(0, 4); -+ } -+ -+ // Select whether to use updateSurroundingRedstone from BlockRedstoneWire (old) -+ // or this helper class (new) -+ private static final boolean old_current_change = false; -+ -+ /* -+ * Process a node whose neighboring redstone wire has experienced value changes. -+ */ -+ private void updateNode(final Level worldIn, final UpdateNode upd1, final int layer) { -+ final BlockPos pos = upd1.self; -+ -+ // Mark this redstone wire as having been visited so that it can be used -+ // to calculate direction of information flow. -+ upd1.visited = true; -+ -+ // Look up the last known state. -+ // Due to the way other redstone components are updated, we do not -+ // have to worry about a state changing behind our backs. The rare -+ // exception is handled by scheduleReentrantNeighborChanged. -+ final BlockState oldState = upd1.currentState; -+ -+ // Ask the wire block to compute its power level from its neighbors. -+ // This will also update the wire's power level and return a new -+ // state if it has changed. When a wire power level is changed, -+ // calculateCurrentChanges will immediately update the block state in the world -+ // and return the same value here to be cached in the corresponding -+ // UpdateNode object. -+ BlockState newState; -+ if (old_current_change) { -+ newState = wire.calculateCurrentChanges(worldIn, pos, pos, oldState); -+ } else { -+ // Looking up block state is slow. This accelerator includes a version of -+ // calculateCurrentChanges that uses cahed wire values for a -+ // significant performance boost. -+ newState = this.calculateCurrentChanges(worldIn, upd1); -+ } -+ -+ // Only inform neighbors if the state has changed -+ if (newState != oldState) { -+ // Store the new state -+ upd1.currentState = newState; -+ -+ // Inform neighbors of the change -+ propagateChanges(worldIn, upd1, layer); -+ } -+ } -+ -+ /* -+ * This identifies the neighboring positions of a new UpdateNode object, -+ * determines their types, and links those to into the graph. Then based on -+ * what nodes in the redstone wire graph have been visited, the neighbors -+ * are reordered left-to-right relative to the direction of information flow. -+ */ -+ private void findNeighbors(final Level worldIn, final UpdateNode upd1) { -+ final BlockPos pos = upd1.self; -+ -+ // Get the list of neighbor coordinates -+ final BlockPos[] neighbors = computeAllNeighbors(pos); -+ -+ // Temporary array of neighbors in cardinal ordering -+ final UpdateNode[] neighbor_nodes = new UpdateNode[24]; -+ -+ // Target array of neighbors sorted left-to-right -+ upd1.neighbor_nodes = new UpdateNode[24]; -+ -+ for (int i=0; i<24; i++) { -+ // Look up each neighbor in the node cache -+ final BlockPos pos2 = neighbors[i]; -+ UpdateNode upd2 = nodeCache.get(pos2); -+ if (upd2 == null) { -+ // If this is a previously unreached position, create -+ // a new update node, add it to the cache, and identify what it is. -+ upd2 = new UpdateNode(); -+ upd2.self = pos2; -+ upd2.parent = pos; -+ nodeCache.put(pos2, upd2); -+ identifyNode(worldIn, upd2); -+ } -+ -+ // For non-redstone blocks, any of the 24 neighboring positions -+ // should receive a block update. However, some block coordinates -+ // may contain a redstone wire that does not directly connect to the -+ // one being expanded. To avoid redundant calculations and confusing -+ // cross-talk, those neighboring positions are not included. -+ if (update_redstone[i] || upd2.type != UpdateNode.Type.REDSTONE) { -+ neighbor_nodes[i] = upd2; -+ } -+ } -+ -+ // Determine the directions from which the redstone signal may have come from. This -+ // checks for redstone wire at the same Y level and also Y+1 and Y-1, relative to the -+ // block being expanded. -+ final boolean fromWest = (neighbor_nodes[0].visited || neighbor_nodes[7].visited || neighbor_nodes[8].visited); -+ final boolean fromEast = (neighbor_nodes[1].visited || neighbor_nodes[12].visited || neighbor_nodes[13].visited); -+ final boolean fromNorth = (neighbor_nodes[4].visited || neighbor_nodes[17].visited || neighbor_nodes[20].visited); -+ final boolean fromSouth = (neighbor_nodes[5].visited || neighbor_nodes[18].visited || neighbor_nodes[21].visited); -+ -+ int cx = 0, cz = 0; -+ if (fromWest) cx += 1; -+ if (fromEast) cx -= 1; -+ if (fromNorth) cz += 1; -+ if (fromSouth) cz -= 1; -+ -+ int heading; -+ if (cx==0 && cz==0) { -+ // If there is no clear direction, try to inherit the heading from ancestor nodes. -+ heading = computeHeading(upd1.xbias, upd1.zbias); -+ -+ // Propagate that heading to descendant nodes. -+ for (int i=0; i<24; i++) { -+ final UpdateNode nn = neighbor_nodes[i]; -+ if (nn != null) { -+ nn.xbias = upd1.xbias; -+ nn.zbias = upd1.zbias; -+ } -+ } -+ } else { -+ if (cx != 0 && cz != 0) { -+ // If the heading is somewhat ambiguous, try to disambiguate based on -+ // ancestor nodes. -+ if (upd1.xbias != 0) cz = 0; -+ if (upd1.zbias != 0) cx = 0; -+ } -+ heading = computeHeading(cx, cz); -+ -+ // Propagate that heading to descendant nodes. -+ for (int i=0; i<24; i++) { -+ final UpdateNode nn = neighbor_nodes[i]; -+ if (nn != null) { -+ nn.xbias = cx; -+ nn.zbias = cz; -+ } -+ } -+ } -+ -+ // Reorder neighboring UpdateNode objects according to the forward direction -+ // determined above. -+ orientNeighbors(neighbor_nodes, upd1.neighbor_nodes, heading); -+ } -+ -+ /* -+ * For any redstone wire block in layer N, inform neighbors to recompute their states -+ * in layers N+1 and N+2; -+ */ -+ private void propagateChanges(final Level worldIn, final UpdateNode upd1, final int layer) { -+ if (upd1.neighbor_nodes == null) { -+ // If this node has not been expanded yet, find its neighbors -+ findNeighbors(worldIn, upd1); -+ } -+ -+ final BlockPos pos = upd1.self; -+ -+ // All neighbors may be scheduled for layer N+1 -+ final int layer1 = layer + 1; -+ -+ // If the node being updated (upd1) has already been expanded, then merely -+ // schedule updates to its neighbors. -+ for (int i = 0; i < 24; i++) { -+ final UpdateNode upd2 = upd1.neighbor_nodes[i]; -+ -+ // This test ensures that an UpdateNode is never scheduled to the same layer -+ // more than once. Also, skip non-connecting redstone wire blocks -+ if (upd2 != null && layer1 > upd2.layer) { -+ upd2.layer = layer1; -+ updateQueue1.add(upd2); -+ -+ // Keep track of which block updated this neighbor -+ upd2.parent = pos; -+ } -+ } -+ -+ // Nodes above and below are scheduled ALSO for layer N+2 -+ final int layer2 = layer + 2; -+ -+ // Repeat of the loop above, but only for the first four (above and below) neighbors -+ // and for layer N+2; -+ for (int i = 0; i < 4; i++) { -+ final UpdateNode upd2 = upd1.neighbor_nodes[i]; -+ if (upd2 != null && layer2 > upd2.layer) { -+ upd2.layer = layer2; -+ updateQueue2.add(upd2); -+ upd2.parent = pos; -+ } -+ } -+ } -+ -+ // The breadth-first search below will send block updates to blocks -+ // that are not redstone wire. If one of those updates results in -+ // a distant redstone wire getting an update, then this.neighborChanged -+ // will get called. This would be a reentrant call, and -+ // it is necessary to properly integrate those updates into the -+ // on-going search through redstone wire. Thus, we make the layer -+ // currently being processed visible at the object level. -+ -+ // The current layer being processed by the breadth-first search -+ private int currentWalkLayer = 0; -+ -+ private void shiftQueue() { -+ final List t = updateQueue0; -+ t.clear(); -+ updateQueue0 = updateQueue1; -+ updateQueue1 = updateQueue2; -+ updateQueue2 = t; -+ } -+ -+ /* -+ * Perform a breadth-first (layer by layer) traversal through redstone -+ * wire blocks, propagating value changes to neighbors in an order -+ * that is a function of distance from the initial call to -+ * this.neighborChanged. -+ */ -+ private void breadthFirstWalk(final Level worldIn) { -+ shiftQueue(); -+ currentWalkLayer = 1; -+ -+ // Loop over all layers -+ while (updateQueue0.size()>0 || updateQueue1.size()>0) { -+ // Get the set of blocks in this layer -+ final List thisLayer = updateQueue0; -+ -+ // Loop over all blocks in the layer. Recall that -+ // this is a List, preserving the insertion order of -+ // left-to-right based on direction of information flow. -+ for (UpdateNode upd : thisLayer) { -+ if (upd.type == UpdateNode.Type.REDSTONE) { -+ // If the node is is redstone wire, -+ // schedule updates to neighbors if its value -+ // has changed. -+ updateNode(worldIn, upd, currentWalkLayer); -+ } else { -+ // If this block is not redstone wire, send a block update. -+ // Redstone wire blocks get state updates, but they don't -+ // need block updates. Only non-redstone neighbors need updates. -+ -+ // World.neighborChanged is called from -+ // World.notifyNeighborsOfStateChange, and -+ // notifyNeighborsOfStateExcept. We don't use -+ // World.notifyNeighborsOfStateChange here, since we are -+ // already keeping track of all of the neighbor positions -+ // that need to be updated. All on its own, handling neighbors -+ // this way reduces block updates by 1/3 (24 instead of 36). -+// worldIn.neighborChanged(upd.self, wire, upd.parent); -+ -+ // [Space Walker] -+ // The neighbor update system got a significant overhaul in 1.19. -+ // Shape and block updates are now added to a stack before being -+ // processed. These changes make it so any neighbor updates emitted -+ // by this accelerator will not be processed until after the entire -+ // wire network has updated. This has a significant impact on the -+ // behavior and introduces Vanilla parity issues. -+ // To circumvent this issue we bypass the neighbor update stack and -+ // call BlockStateBase#neighborChanged directly. This change mostly -+ // restores old behavior, at the cost of bypassing the -+ // max-chained-neighbor-updates server property. -+ worldIn.getBlockState(upd.self).handleNeighborChanged(worldIn, upd.self, wire, upd.parent, false); -+ } -+ } -+ -+ // Move on to the next layer -+ shiftQueue(); -+ currentWalkLayer++; -+ } -+ -+ currentWalkLayer = 0; -+ } -+ -+ /* -+ * Normally, when Minecraft is computing redstone wire power changes, and a wire power level -+ * change sends a block update to a neighboring functional component (e.g. piston, repeater, etc.), -+ * those updates are queued. Only once all redstone wire updates are complete will any component -+ * action generate any further block updates to redstone wire. Instant repeater lines, for instance, -+ * will process all wire updates for one redstone line, after which the pistons will zero-tick, -+ * after which the next redstone line performs all of its updates. Thus, each wire is processed in its -+ * own discrete wave. -+ * -+ * However, there are some corner cases where this pattern breaks, with a proof of concept discovered -+ * by Rays Works, which works the same in vanilla. The scenario is as follows: -+ * (1) A redstone wire is conducting a signal. -+ * (2) Part-way through that wave of updates, a neighbor is updated that causes an update to a completely -+ * separate redstone wire. -+ * (3) This results in a call to BlockRedstoneWire.neighborChanged for that other wire, in the middle of -+ * an already on-going propagation through the first wire. -+ * -+ * The vanilla code, being depth-first, would end up fully processing the second wire before going back -+ * to finish processing the first one. (Although technically, vanilla has no special concept of "being -+ * in the middle" of processing updates to a wire.) For the breadth-first algorithm, we give this -+ * situation special handling, where the updates for the second wire are incorporated into the schedule -+ * for the first wire, and then the callstack is allowed to unwind back to the on-going search loop in -+ * order to continue processing both the first and second wire in the order of distance from the initial -+ * trigger. -+ */ -+ private BlockState scheduleReentrantNeighborChanged(final Level worldIn, final BlockPos pos, final BlockState newState, final BlockPos source) { -+ if (source != null) { -+ // If the cause of the redstone wire update is known, we can use that to help determine -+ // direction of information flow. -+ UpdateNode src = nodeCache.get(source); -+ if (src == null) { -+ src = new UpdateNode(); -+ src.self = source; -+ src.parent = source; -+ src.visited = true; -+ identifyNode(worldIn, src); -+ nodeCache.put(source, src); -+ } -+ } -+ -+ // Find or generate a node for the redstone block position receiving the update -+ UpdateNode upd = nodeCache.get(pos); -+ if (upd == null) { -+ upd = new UpdateNode(); -+ upd.self = pos; -+ upd.parent = pos; -+ upd.visited = true; -+ identifyNode(worldIn, upd); -+ nodeCache.put(pos, upd); -+ } -+ upd.currentState = newState; -+ -+ // Receiving this block update may mean something in the world changed. -+ // Therefore we clear the cached block info about all neighbors of -+ // the position receiving the update and then re-identify what they are. -+ if (upd.neighbor_nodes != null) { -+ for (int i=0; i<24; i++) { -+ final UpdateNode upd2 = upd.neighbor_nodes[i]; -+ if (upd2 == null) continue; -+ upd2.type = UpdateNode.Type.UNKNOWN; -+ upd2.currentState = null; -+ identifyNode(worldIn, upd2); -+ } -+ } -+ -+ // The block at 'pos' is a redstone wire and has been updated already by calling -+ // wire.calculateCurrentChanges, so we don't schedule that. However, we do need -+ // to schedule its neighbors. By passing the current value of 'currentWalkLayer' to -+ // propagateChanges, the neighbors of 'pos' are scheduled for layers currentWalkLayer+1 -+ // and currentWalkLayer+2. -+ propagateChanges(worldIn, upd, currentWalkLayer); -+ -+ // Return here. The call stack will unwind back to the first call to -+ // updateSurroundingRedstone, whereupon the new updates just scheduled will -+ // be propagated. This also facilitates elimination of superfluous and -+ // redundant block updates. -+ return newState; -+ } -+ -+ /* -+ * New version of pre-existing updateSurroundingRedstone, which is called from -+ * wire.updateSurroundingRedstone, which is called from wire.neighborChanged and a -+ * few other methods in BlockRedstoneWire. This sets off the breadth-first -+ * walk through all redstone dust connected to the initial position triggered. -+ */ -+ public BlockState updateSurroundingRedstone(final Level worldIn, final BlockPos pos, final BlockState state, final BlockPos source) { -+ // Check this block's neighbors and see if its power level needs to change -+ // Use the calculateCurrentChanges method in BlockRedstoneWire since we have no -+ // cached block states at this point. -+ final BlockState newState = wire.calculateCurrentChanges(worldIn, pos, pos, state); -+ -+ // If no change, exit -+ if (newState == state) { -+ return state; -+ } -+ -+ // Check to see if this update was received during an on-going breadth first search -+ if (currentWalkLayer > 0 || nodeCache.size() > 0) { -+ // As breadthFirstWalk progresses, it sends block updates to neighbors. Some of those -+ // neighbors may affect the world so as to cause yet another redstone wire block to receive -+ // an update. If that happens, we need to integrate those redstone wire updates into the -+ // already on-going graph walk being performed by breadthFirstWalk. -+ return scheduleReentrantNeighborChanged(worldIn, pos, newState, source); -+ } -+ // If there are no on-going walks through redstone wire, then start a new walk. -+ -+ // If the source of the block update to the redstone wire at 'pos' is known, we can use -+ // that to help determine the direction of information flow. -+ if (source != null) { -+ final UpdateNode src = new UpdateNode(); -+ src.self = source; -+ src.parent = source; -+ src.visited = true; -+ nodeCache.put(source, src); -+ identifyNode(worldIn, src); -+ } -+ -+ // Create a node representing the block at 'pos', and then propagate updates -+ // to its neighbors. As stated above, the call to wire.calculateCurrentChanges -+ // already performs the update to the block at 'pos', so it is not added to the schedule. -+ final UpdateNode upd = new UpdateNode(); -+ upd.self = pos; -+ upd.parent = source!=null ? source : pos; -+ upd.currentState = newState; -+ upd.type = UpdateNode.Type.REDSTONE; -+ upd.visited = true; -+ nodeCache.put(pos, upd); -+ propagateChanges(worldIn, upd, 0); -+ -+ // Perform the walk over all directly reachable redstone wire blocks, propagating wire value -+ // updates in a breadth first order out from the initial update received for the block at 'pos'. -+ breadthFirstWalk(worldIn); -+ -+ // With the whole search completed, clear the list of all known blocks. -+ // We do not want to keep around state information that may be changed by other code. -+ // In theory, we could cache the neighbor block positions, but that is a separate -+ // optimization. -+ nodeCache.clear(); -+ -+ return newState; -+ } -+ -+ // For any array of neighbors in an UpdateNode object, these are always -+ // the indices of the four immediate neighbors at the same Y coordinate. -+ private static final int[] rs_neighbors = {4, 5, 6, 7}; -+ private static final int[] rs_neighbors_up = {9, 11, 13, 15}; -+ private static final int[] rs_neighbors_dn = {8, 10, 12, 14}; -+ -+ /* -+ * Updated calculateCurrentChanges that is optimized for speed and uses -+ * the UpdateNode's neighbor array to find the redstone states of neighbors -+ * that might power it. -+ */ -+ private BlockState calculateCurrentChanges(final Level worldIn, final UpdateNode upd) { -+ BlockState state = upd.currentState; -+ final int i = state.getValue(RedStoneWireBlock.POWER).intValue(); -+ int j = 0; -+ j = getMaxCurrentStrength(upd, j); -+ int l = 0; -+ -+ wire.shouldSignal = false; -+ // Unfortunately, World.isBlockIndirectlyGettingPowered is complicated, -+ // and I'm not ready to try to replicate even more functionality from -+ // elsewhere in Minecraft into this accelerator. So sadly, we must -+ // suffer the performance hit of this very expensive call. If there -+ // is consistency to what this call returns, we may be able to cache it. -+ final int k = worldIn.getBestNeighborSignal(upd.self); -+ wire.shouldSignal = true; -+ -+ // The variable 'k' holds the maximum redstone power value of any adjacent blocks. -+ // If 'k' has the highest level of all neighbors, then the power level of this -+ // redstone wire will be set to 'k'. If 'k' is already 15, then nothing inside the -+ // following loop can affect the power level of the wire. Therefore, the loop is -+ // skipped if k is already 15. -+ if (k < 15) { -+ if (upd.neighbor_nodes == null) { -+ // If this node's neighbors are not known, expand the node -+ findNeighbors(worldIn, upd); -+ } -+ -+ // These remain constant, so pull them out of the loop. -+ // Regardless of which direction is forward, the UpdateNode for the -+ // position directly above the node being calculated is always -+ // at index 1. -+ UpdateNode center_up = upd.neighbor_nodes[1]; -+ boolean center_up_is_cube = center_up.currentState.isRedstoneConductor(worldIn, center_up.self); // TODO -+ -+ for (int m = 0; m < 4; m++) { -+ // Get the neighbor array index of each of the four cardinal -+ // neighbors. -+ int n = rs_neighbors[m]; -+ -+ // Get the max redstone power level of each of the cardinal -+ // neighbors -+ UpdateNode neighbor = upd.neighbor_nodes[n]; -+ l = getMaxCurrentStrength(neighbor, l); -+ -+ // Also check the positions above and below the cardinal -+ // neighbors -+ boolean neighbor_is_cube = neighbor.currentState.isRedstoneConductor(worldIn, neighbor.self); // TODO -+ if (!neighbor_is_cube) { -+ UpdateNode neighbor_down = upd.neighbor_nodes[rs_neighbors_dn[m]]; -+ l = getMaxCurrentStrength(neighbor_down, l); -+ } else -+ if (!center_up_is_cube) { -+ UpdateNode neighbor_up = upd.neighbor_nodes[rs_neighbors_up[m]]; -+ l = getMaxCurrentStrength(neighbor_up, l); -+ } -+ } -+ } -+ -+ // The new code sets this RedstoneWire block's power level to the highest neighbor -+ // minus 1. This usually results in wire power levels dropping by 2 at a time. -+ // This optimization alone has no impact on update order, only the number of updates. -+ j = l - 1; -+ -+ // If 'l' turns out to be zero, then j will be set to -1, but then since 'k' will -+ // always be in the range of 0 to 15, the following if will correct that. -+ if (k > j) j = k; -+ -+ // egg82's amendment -+ // Adding Bukkit's BlockRedstoneEvent - er.. event. -+ if (i != j) { -+ BlockRedstoneEvent event = new BlockRedstoneEvent(CraftBlock.at(worldIn, upd.self), i, j); -+ worldIn.getCraftServer().getPluginManager().callEvent(event); -+ j = event.getNewCurrent(); -+ } -+ -+ if (i != j) { -+ // If the power level has changed from its previous value, compute a new state -+ // and set it in the world. -+ // Possible optimization: Don't commit state changes to the world until they -+ // need to be known by some nearby non-redstone-wire block. -+ BlockPos pos = new BlockPos(upd.self.getX(), upd.self.getY(), upd.self.getZ()); -+ if (wire.canSurvive(null, worldIn, pos)) { -+ state = state.setValue(RedStoneWireBlock.POWER, Integer.valueOf(j)); -+ // [Space Walker] suppress shape updates and emit those manually to -+ // bypass the new neighbor update stack. -+ if (worldIn.setBlock(upd.self, state, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS)) -+ updateNeighborShapes(worldIn, upd.self, state); -+ } -+ } -+ -+ return state; -+ } -+ -+ private static final Direction[] UPDATE_SHAPE_ORDER = { Direction.WEST, Direction.EAST, Direction.NORTH, Direction.SOUTH, Direction.DOWN, Direction.UP }; -+ -+ /* -+ * [Space Walker] -+ * This method emits shape updates around the given block, -+ * bypassing the new neighbor update stack. Diagonal shape -+ * updates are omitted, as they are mostly unnecessary. -+ * Diagonal shape updates are emitted exclusively to other -+ * redstone wires, in order to update their connection properties. -+ * Wire connections should never change as a result of power -+ * changes, so the only behavioral change will be in scenarios -+ * where earlier shape updates have been suppressed to keep a -+ * redstone wire in an invalid state. -+ */ -+ public void updateNeighborShapes(Level level, BlockPos pos, BlockState state) { -+ // these updates will be added to the stack and processed after the entire network has updated -+ state.updateIndirectNeighbourShapes(level, pos, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS); -+ -+ for (Direction dir : UPDATE_SHAPE_ORDER) { -+ BlockPos neighborPos = pos.relative(dir); -+ BlockState neighborState = level.getBlockState(neighborPos); -+ -+ BlockState newState = neighborState.updateShape(dir.getOpposite(), state, level, neighborPos, pos); -+ Block.updateOrDestroy(neighborState, newState, level, neighborPos, Block.UPDATE_CLIENTS); -+ } -+ } -+ -+ /* -+ * Optimized function to compute a redstone wire's power level based on cached -+ * state. -+ */ -+ private static int getMaxCurrentStrength(final UpdateNode upd, final int strength) { -+ if (upd.type != UpdateNode.Type.REDSTONE) return strength; -+ final int i = upd.currentState.getValue(RedStoneWireBlock.POWER).intValue(); -+ return i > strength ? i : strength; -+ } -+} -diff --git a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java -index c73bf0b36252796ca93c500affa2be568e3f6c9e..18ed178223cca85dbba65b1e07741622e266d318 100644 ---- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java -@@ -258,6 +258,116 @@ public class RedStoneWireBlock extends Block { - return floor.isFaceSturdy(world, pos, Direction.UP) || floor.is(Blocks.HOPPER); - } - -+ // Paper start - Optimize redstone -+ // The bulk of the new functionality is found in RedstoneWireTurbo.java -+ com.destroystokyo.paper.util.RedstoneWireTurbo turbo = new com.destroystokyo.paper.util.RedstoneWireTurbo(this); -+ -+ /* -+ * Modified version of pre-existing updateSurroundingRedstone, which is called from -+ * this.neighborChanged and a few other methods in this class. -+ * Note: Added 'source' argument so as to help determine direction of information flow -+ */ -+ private void updateSurroundingRedstone(Level worldIn, BlockPos pos, BlockState state, BlockPos source) { -+ if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.EIGENCRAFT) { -+ turbo.updateSurroundingRedstone(worldIn, pos, state, source); -+ return; -+ } -+ updatePowerStrength(worldIn, pos, state); -+ } -+ -+ /* -+ * Slightly modified method to compute redstone wire power levels from neighboring blocks. -+ * Modifications cut the number of power level changes by about 45% from vanilla, and this -+ * optimization synergizes well with the breadth-first search implemented in -+ * RedstoneWireTurbo. -+ * Note: RedstoneWireTurbo contains a faster version of this code. -+ * Note: Made this public so that RedstoneWireTurbo can access it. -+ */ -+ public BlockState calculateCurrentChanges(Level worldIn, BlockPos pos1, BlockPos pos2, BlockState state) { -+ BlockState iblockstate = state; -+ int i = state.getValue(POWER); -+ int j = 0; -+ j = this.getPower(j, worldIn.getBlockState(pos2)); -+ this.shouldSignal = false; -+ int k = worldIn.getBestNeighborSignal(pos1); -+ this.shouldSignal = true; -+ -+ if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA) { -+ // This code is totally redundant to if statements just below the loop. -+ if (k > 0 && k > j - 1) { -+ j = k; -+ } -+ } -+ -+ int l = 0; -+ -+ // The variable 'k' holds the maximum redstone power value of any adjacent blocks. -+ // If 'k' has the highest level of all neighbors, then the power level of this -+ // redstone wire will be set to 'k'. If 'k' is already 15, then nothing inside the -+ // following loop can affect the power level of the wire. Therefore, the loop is -+ // skipped if k is already 15. -+ if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA || k < 15) { -+ for (Direction enumfacing : Direction.Plane.HORIZONTAL) { -+ BlockPos blockpos = pos1.relative(enumfacing); -+ boolean flag = blockpos.getX() != pos2.getX() || blockpos.getZ() != pos2.getZ(); -+ -+ if (flag) { -+ l = this.getPower(l, worldIn.getBlockState(blockpos)); -+ } -+ -+ if (worldIn.getBlockState(blockpos).isRedstoneConductor(worldIn, blockpos) && !worldIn.getBlockState(pos1.above()).isRedstoneConductor(worldIn, pos1)) { -+ if (flag && pos1.getY() >= pos2.getY()) { -+ l = this.getPower(l, worldIn.getBlockState(blockpos.above())); -+ } -+ } else if (!worldIn.getBlockState(blockpos).isRedstoneConductor(worldIn, blockpos) && flag && pos1.getY() <= pos2.getY()) { -+ l = this.getPower(l, worldIn.getBlockState(blockpos.below())); -+ } -+ } -+ } -+ -+ if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA) { -+ // The old code would decrement the wire value only by 1 at a time. -+ if (l > j) { -+ j = l - 1; -+ } else if (j > 0) { -+ --j; -+ } else { -+ j = 0; -+ } -+ -+ if (k > j - 1) { -+ j = k; -+ } -+ } else { -+ // The new code sets this RedstoneWire block's power level to the highest neighbor -+ // minus 1. This usually results in wire power levels dropping by 2 at a time. -+ // This optimization alone has no impact on update order, only the number of updates. -+ j = l - 1; -+ -+ // If 'l' turns out to be zero, then j will be set to -1, but then since 'k' will -+ // always be in the range of 0 to 15, the following if will correct that. -+ if (k > j) j = k; -+ } -+ -+ if (i != j) { -+ org.bukkit.event.block.BlockRedstoneEvent event = new org.bukkit.event.block.BlockRedstoneEvent(org.bukkit.craftbukkit.block.CraftBlock.at(worldIn, pos1), i, j); -+ worldIn.getCraftServer().getPluginManager().callEvent(event); -+ -+ j = event.getNewCurrent(); -+ state = state.setValue(POWER, j); -+ -+ if (worldIn.getBlockState(pos1) == iblockstate) { -+ // [Space Walker] suppress shape updates and emit those manually to -+ // bypass the new neighbor update stack. -+ if (worldIn.setBlock(pos1, state, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS)) -+ turbo.updateNeighborShapes(worldIn, pos1, state); -+ } -+ } -+ -+ return state; -+ } -+ // Paper end -+ - private void updatePowerStrength(Level world, BlockPos pos, BlockState state) { - int i = this.calculateTargetStrength(world, pos); - -@@ -327,6 +437,7 @@ public class RedStoneWireBlock extends Block { - return Math.max(i, j - 1); - } - -+ private int getPower(int min, BlockState iblockdata) { return Math.max(min, getWireSignal(iblockdata)); } // Paper - Optimize redstone - private int getWireSignal(BlockState state) { - return state.is((Block) this) ? (Integer) state.getValue(RedStoneWireBlock.POWER) : 0; - } -@@ -349,7 +460,7 @@ public class RedStoneWireBlock extends Block { - @Override - protected void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) { - if (!oldState.is(state.getBlock()) && !world.isClientSide) { -- this.updatePowerStrength(world, pos, state); -+ this.updateSurroundingRedstone(world, pos, state, null); // Paper - Optimize redstone - Iterator iterator = Direction.Plane.VERTICAL.iterator(); - - while (iterator.hasNext()) { -@@ -376,7 +487,7 @@ public class RedStoneWireBlock extends Block { - world.updateNeighborsAt(pos.relative(enumdirection), this); - } - -- this.updatePowerStrength(world, pos, state); -+ this.updateSurroundingRedstone(world, pos, state, null); // Paper - Optimize redstone - this.updateNeighborsOfNeighboringWires(world, pos); - } - } -@@ -411,7 +522,7 @@ public class RedStoneWireBlock extends Block { - protected void neighborChanged(BlockState state, Level world, BlockPos pos, Block sourceBlock, BlockPos sourcePos, boolean notify) { - if (!world.isClientSide) { - if (state.canSurvive(world, pos)) { -- this.updatePowerStrength(world, pos, state); -+ this.updateSurroundingRedstone(world, pos, state, sourcePos); // Paper - Optimize redstone - } else { - dropResources(state, world, pos); - world.removeBlock(pos, false); diff --git a/patches/server/1009-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch b/patches/server/1009-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch new file mode 100644 index 0000000000..6fc4445bbf --- /dev/null +++ b/patches/server/1009-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch @@ -0,0 +1,133 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Tue, 4 Aug 2020 22:24:15 +0200 +Subject: [PATCH] Optimize Pathfinder - Remove Streams / Optimized collections + +I utilized the IDE to convert streams to non streams code, so shouldn't +be any risk of behavior change. Only did minor optimization of the +generated code set to remove unnecessary things. + +I expect us to just drop this patch on next major update and re-apply +it with the IDE again and re-apply the collections optimization. + +Optimize collection by creating a list instead of a set of the key and value. + +This lets us get faster foreach iteration, as well as avoids map lookups on +the values when needed. + +diff --git a/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java b/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java +index 8a483c8d2a54617d78af19de32fa0ded71b3223a..18bbb3f8f99849333ff4bc020c8ce758a69312a5 100644 +--- a/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java ++++ b/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java +@@ -38,8 +38,12 @@ public class PathFinder { + if (node == null) { + return null; + } else { +- Map map = positions.stream() +- .collect(Collectors.toMap(pos -> this.nodeEvaluator.getTarget((double)pos.getX(), (double)pos.getY(), (double)pos.getZ()), Function.identity())); ++ // Paper start - Perf: remove streams and optimize collection ++ List> map = Lists.newArrayList(); ++ for (final BlockPos pos : positions) { ++ map.add(new java.util.AbstractMap.SimpleEntry<>(this.nodeEvaluator.getTarget(pos.getX(), pos.getY(), pos.getZ()), pos)); ++ } ++ // Paper end - Perf: remove streams and optimize collection + Path path = this.findPath(world.getProfiler(), node, map, followRange, distance, rangeMultiplier); + this.nodeEvaluator.done(); + return path; +@@ -47,18 +51,19 @@ public class PathFinder { + } + + @Nullable +- private Path findPath(ProfilerFiller profiler, Node startNode, Map positions, float followRange, int distance, float rangeMultiplier) { ++ // Paper start - Perf: remove streams and optimize collection ++ private Path findPath(ProfilerFiller profiler, Node startNode, List> positions, float followRange, int distance, float rangeMultiplier) { + profiler.push("find_path"); + profiler.markForCharting(MetricCategory.PATH_FINDING); +- Set set = positions.keySet(); ++ // Set set = positions.keySet(); + startNode.g = 0.0F; +- startNode.h = this.getBestH(startNode, set); ++ startNode.h = this.getBestH(startNode, positions); // Paper - optimize collection + startNode.f = startNode.h; + this.openSet.clear(); + this.openSet.insert(startNode); +- Set set2 = ImmutableSet.of(); ++ // Set set2 = ImmutableSet.of(); // Paper - unused - diff on change + int i = 0; +- Set set3 = Sets.newHashSetWithExpectedSize(set.size()); ++ List> entryList = Lists.newArrayListWithExpectedSize(positions.size()); // Paper - optimize collection + int j = (int)((float)this.maxVisitedNodes * rangeMultiplier); + + while (!this.openSet.isEmpty()) { +@@ -69,14 +74,18 @@ public class PathFinder { + Node node = this.openSet.pop(); + node.closed = true; + +- for (Target target : set) { ++ // Paper start - optimize collection ++ for (int i1 = 0; i1 < positions.size(); i1++) { ++ final Map.Entry entry = positions.get(i1); ++ Target target = entry.getKey(); + if (node.distanceManhattan(target) <= (float)distance) { + target.setReached(); +- set3.add(target); ++ entryList.add(entry); ++ // Paper end - Perf: remove streams and optimize collection + } + } + +- if (!set3.isEmpty()) { ++ if (!entryList.isEmpty()) { // Paper - Perf: remove streams and optimize collection; rename + break; + } + +@@ -91,7 +100,7 @@ public class PathFinder { + if (node2.walkedDistance < followRange && (!node2.inOpenSet() || g < node2.g)) { + node2.cameFrom = node; + node2.g = g; +- node2.h = this.getBestH(node2, set) * 1.5F; ++ node2.h = this.getBestH(node2, positions) * 1.5F; // Paper - Perf: remove streams and optimize collection + if (node2.inOpenSet()) { + this.openSet.changeCost(node2, node2.g + node2.h); + } else { +@@ -103,23 +112,32 @@ public class PathFinder { + } + } + +- Optional optional = !set3.isEmpty() +- ? set3.stream().map(node -> this.reconstructPath(node.getBestNode(), positions.get(node), true)).min(Comparator.comparingInt(Path::getNodeCount)) +- : set.stream() +- .map(targetx -> this.reconstructPath(targetx.getBestNode(), positions.get(targetx), false)) +- .min(Comparator.comparingDouble(Path::getDistToTarget).thenComparingInt(Path::getNodeCount)); ++ // Paper start - Perf: remove streams and optimize collection ++ Path best = null; ++ boolean entryListIsEmpty = entryList.isEmpty(); ++ Comparator comparator = entryListIsEmpty ? Comparator.comparingInt(Path::getNodeCount) ++ : Comparator.comparingDouble(Path::getDistToTarget).thenComparingInt(Path::getNodeCount); ++ for (Map.Entry entry : entryListIsEmpty ? positions : entryList) { ++ Path path = this.reconstructPath(entry.getKey().getBestNode(), entry.getValue(), !entryListIsEmpty); ++ if (best == null || comparator.compare(path, best) < 0) ++ best = path; ++ } + profiler.pop(); +- return optional.isEmpty() ? null : optional.get(); ++ return best; ++ // Paper end - Perf: remove streams and optimize collection + } + + protected float distance(Node a, Node b) { + return a.distanceTo(b); + } + +- private float getBestH(Node node, Set targets) { ++ private float getBestH(Node node, List> targets) { // Paper - Perf: remove streams and optimize collection; Set -> List> + float f = Float.MAX_VALUE; + +- for (Target target : targets) { ++ // Paper start - Perf: remove streams and optimize collection ++ for (int i = 0, targetsSize = targets.size(); i < targetsSize; i++) { ++ final Target target = targets.get(i).getKey(); ++ // Paper end - Perf: remove streams and optimize collection + float g = node.distanceTo(target); + target.updateBest(g, node); + f = Math.min(g, f); diff --git a/patches/server/1010-Improve-boat-collision-performance.patch b/patches/server/1010-Improve-boat-collision-performance.patch new file mode 100644 index 0000000000..3cd32de1e5 --- /dev/null +++ b/patches/server/1010-Improve-boat-collision-performance.patch @@ -0,0 +1,68 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Mon, 2 Aug 2021 10:10:40 +0200 +Subject: [PATCH] Improve boat collision performance + + +diff --git a/src/main/java/net/minecraft/Util.java b/src/main/java/net/minecraft/Util.java +index 2cd0a4dc4f0baa08bd7f5a053303bb63733f0bab..0bd367235f80c1f0d319a6aa5130d82ad82d895c 100644 +--- a/src/main/java/net/minecraft/Util.java ++++ b/src/main/java/net/minecraft/Util.java +@@ -125,6 +125,7 @@ public class Util { + .filter(fileSystemProvider -> fileSystemProvider.getScheme().equalsIgnoreCase("jar")) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No jar file system provider found")); ++ public static final double COLLISION_EPSILON = 1.0E-7; // Paper - Improve boat collision performance + private static Consumer thePauser = message -> { + }; + +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index 5854e58a014b5581fa065d1db256ffc0aa947ce2..75e01c1e01e0782fc8af48777bbe4716d37aeafa 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -1459,7 +1459,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + if (!source.is(DamageTypeTags.IS_PROJECTILE)) { + Entity entity = source.getDirectEntity(); + +- if (entity instanceof LivingEntity) { ++ if (entity instanceof LivingEntity && entity.distanceToSqr(this) <= (200.0D * 200.0D)) { // Paper - Improve boat collision performance + LivingEntity entityliving = (LivingEntity) entity; + + this.blockUsingShield(entityliving); +@@ -1553,11 +1553,12 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + if (entity1 != null && !source.is(DamageTypeTags.NO_KNOCKBACK)) { +- double d0 = entity1.getX() - this.getX(); ++ final boolean far = entity1.distanceToSqr(this) > (200.0 * 200.0); // Paper - Improve boat collision performance ++ double d0 = far ? (Math.random() - Math.random()) : entity1.getX() - this.getX(); // Paper - Improve boat collision performance + + double d1; + +- for (d1 = entity1.getZ() - this.getZ(); d0 * d0 + d1 * d1 < 1.0E-4D; d1 = (Math.random() - Math.random()) * 0.01D) { ++ for (d1 = far ? Math.random() - Math.random() : entity1.getZ() - this.getZ(); d0 * d0 + d1 * d1 < 1.0E-4D; d1 = (Math.random() - Math.random()) * 0.01D) { // Paper - Improve boat collision performance + d0 = (Math.random() - Math.random()) * 0.01D; + } + +@@ -2334,7 +2335,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.hurtCurrentlyUsedShield((float) -event.getDamage(DamageModifier.BLOCKING)); + Entity entity = damagesource.getDirectEntity(); + +- if (entity instanceof LivingEntity) { ++ if (entity instanceof LivingEntity && entity.distanceToSqr(this) <= (200.0D * 200.0D)) { // Paper - Improve boat collision performance + this.blockUsingShield((LivingEntity) entity); + } + } +diff --git a/src/main/java/net/minecraft/world/entity/vehicle/Boat.java b/src/main/java/net/minecraft/world/entity/vehicle/Boat.java +index 549202706396fee91c23f6e34f7faa7672e4b03f..b068cff9b5aa457d65b679529956e8210296d799 100644 +--- a/src/main/java/net/minecraft/world/entity/vehicle/Boat.java ++++ b/src/main/java/net/minecraft/world/entity/vehicle/Boat.java +@@ -686,7 +686,7 @@ public class Boat extends VehicleEntity implements VariantHolder { + this.invFriction = 0.05F; + if (this.oldStatus == Boat.Status.IN_AIR && this.status != Boat.Status.IN_AIR && this.status != Boat.Status.ON_LAND) { + this.waterLevel = this.getY(1.0D); +- this.setPos(this.getX(), (double) (this.getWaterLevelAbove() - this.getBbHeight()) + 0.101D, this.getZ()); ++ this.move(MoverType.SELF, new Vec3(0.0, ((double) (this.getWaterLevelAbove() - this.getBbHeight()) + 0.101D) - this.getY(), 0.0)); // Paper - Improve boat collision performance + this.setDeltaMovement(this.getDeltaMovement().multiply(1.0D, 0.0D, 1.0D)); + this.lastYd = 0.0D; + this.status = Boat.Status.IN_WATER; diff --git a/patches/server/1010-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch b/patches/server/1010-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch deleted file mode 100644 index 6fc4445bbf..0000000000 --- a/patches/server/1010-Optimize-Pathfinder-Remove-Streams-Optimized-collect.patch +++ /dev/null @@ -1,133 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Tue, 4 Aug 2020 22:24:15 +0200 -Subject: [PATCH] Optimize Pathfinder - Remove Streams / Optimized collections - -I utilized the IDE to convert streams to non streams code, so shouldn't -be any risk of behavior change. Only did minor optimization of the -generated code set to remove unnecessary things. - -I expect us to just drop this patch on next major update and re-apply -it with the IDE again and re-apply the collections optimization. - -Optimize collection by creating a list instead of a set of the key and value. - -This lets us get faster foreach iteration, as well as avoids map lookups on -the values when needed. - -diff --git a/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java b/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java -index 8a483c8d2a54617d78af19de32fa0ded71b3223a..18bbb3f8f99849333ff4bc020c8ce758a69312a5 100644 ---- a/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java -+++ b/src/main/java/net/minecraft/world/level/pathfinder/PathFinder.java -@@ -38,8 +38,12 @@ public class PathFinder { - if (node == null) { - return null; - } else { -- Map map = positions.stream() -- .collect(Collectors.toMap(pos -> this.nodeEvaluator.getTarget((double)pos.getX(), (double)pos.getY(), (double)pos.getZ()), Function.identity())); -+ // Paper start - Perf: remove streams and optimize collection -+ List> map = Lists.newArrayList(); -+ for (final BlockPos pos : positions) { -+ map.add(new java.util.AbstractMap.SimpleEntry<>(this.nodeEvaluator.getTarget(pos.getX(), pos.getY(), pos.getZ()), pos)); -+ } -+ // Paper end - Perf: remove streams and optimize collection - Path path = this.findPath(world.getProfiler(), node, map, followRange, distance, rangeMultiplier); - this.nodeEvaluator.done(); - return path; -@@ -47,18 +51,19 @@ public class PathFinder { - } - - @Nullable -- private Path findPath(ProfilerFiller profiler, Node startNode, Map positions, float followRange, int distance, float rangeMultiplier) { -+ // Paper start - Perf: remove streams and optimize collection -+ private Path findPath(ProfilerFiller profiler, Node startNode, List> positions, float followRange, int distance, float rangeMultiplier) { - profiler.push("find_path"); - profiler.markForCharting(MetricCategory.PATH_FINDING); -- Set set = positions.keySet(); -+ // Set set = positions.keySet(); - startNode.g = 0.0F; -- startNode.h = this.getBestH(startNode, set); -+ startNode.h = this.getBestH(startNode, positions); // Paper - optimize collection - startNode.f = startNode.h; - this.openSet.clear(); - this.openSet.insert(startNode); -- Set set2 = ImmutableSet.of(); -+ // Set set2 = ImmutableSet.of(); // Paper - unused - diff on change - int i = 0; -- Set set3 = Sets.newHashSetWithExpectedSize(set.size()); -+ List> entryList = Lists.newArrayListWithExpectedSize(positions.size()); // Paper - optimize collection - int j = (int)((float)this.maxVisitedNodes * rangeMultiplier); - - while (!this.openSet.isEmpty()) { -@@ -69,14 +74,18 @@ public class PathFinder { - Node node = this.openSet.pop(); - node.closed = true; - -- for (Target target : set) { -+ // Paper start - optimize collection -+ for (int i1 = 0; i1 < positions.size(); i1++) { -+ final Map.Entry entry = positions.get(i1); -+ Target target = entry.getKey(); - if (node.distanceManhattan(target) <= (float)distance) { - target.setReached(); -- set3.add(target); -+ entryList.add(entry); -+ // Paper end - Perf: remove streams and optimize collection - } - } - -- if (!set3.isEmpty()) { -+ if (!entryList.isEmpty()) { // Paper - Perf: remove streams and optimize collection; rename - break; - } - -@@ -91,7 +100,7 @@ public class PathFinder { - if (node2.walkedDistance < followRange && (!node2.inOpenSet() || g < node2.g)) { - node2.cameFrom = node; - node2.g = g; -- node2.h = this.getBestH(node2, set) * 1.5F; -+ node2.h = this.getBestH(node2, positions) * 1.5F; // Paper - Perf: remove streams and optimize collection - if (node2.inOpenSet()) { - this.openSet.changeCost(node2, node2.g + node2.h); - } else { -@@ -103,23 +112,32 @@ public class PathFinder { - } - } - -- Optional optional = !set3.isEmpty() -- ? set3.stream().map(node -> this.reconstructPath(node.getBestNode(), positions.get(node), true)).min(Comparator.comparingInt(Path::getNodeCount)) -- : set.stream() -- .map(targetx -> this.reconstructPath(targetx.getBestNode(), positions.get(targetx), false)) -- .min(Comparator.comparingDouble(Path::getDistToTarget).thenComparingInt(Path::getNodeCount)); -+ // Paper start - Perf: remove streams and optimize collection -+ Path best = null; -+ boolean entryListIsEmpty = entryList.isEmpty(); -+ Comparator comparator = entryListIsEmpty ? Comparator.comparingInt(Path::getNodeCount) -+ : Comparator.comparingDouble(Path::getDistToTarget).thenComparingInt(Path::getNodeCount); -+ for (Map.Entry entry : entryListIsEmpty ? positions : entryList) { -+ Path path = this.reconstructPath(entry.getKey().getBestNode(), entry.getValue(), !entryListIsEmpty); -+ if (best == null || comparator.compare(path, best) < 0) -+ best = path; -+ } - profiler.pop(); -- return optional.isEmpty() ? null : optional.get(); -+ return best; -+ // Paper end - Perf: remove streams and optimize collection - } - - protected float distance(Node a, Node b) { - return a.distanceTo(b); - } - -- private float getBestH(Node node, Set targets) { -+ private float getBestH(Node node, List> targets) { // Paper - Perf: remove streams and optimize collection; Set -> List> - float f = Float.MAX_VALUE; - -- for (Target target : targets) { -+ // Paper start - Perf: remove streams and optimize collection -+ for (int i = 0, targetsSize = targets.size(); i < targetsSize; i++) { -+ final Target target = targets.get(i).getKey(); -+ // Paper end - Perf: remove streams and optimize collection - float g = node.distanceTo(target); - target.updateBest(g, node); - f = Math.min(g, f); diff --git a/patches/server/1011-Improve-boat-collision-performance.patch b/patches/server/1011-Improve-boat-collision-performance.patch deleted file mode 100644 index 3cd32de1e5..0000000000 --- a/patches/server/1011-Improve-boat-collision-performance.patch +++ /dev/null @@ -1,68 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Mon, 2 Aug 2021 10:10:40 +0200 -Subject: [PATCH] Improve boat collision performance - - -diff --git a/src/main/java/net/minecraft/Util.java b/src/main/java/net/minecraft/Util.java -index 2cd0a4dc4f0baa08bd7f5a053303bb63733f0bab..0bd367235f80c1f0d319a6aa5130d82ad82d895c 100644 ---- a/src/main/java/net/minecraft/Util.java -+++ b/src/main/java/net/minecraft/Util.java -@@ -125,6 +125,7 @@ public class Util { - .filter(fileSystemProvider -> fileSystemProvider.getScheme().equalsIgnoreCase("jar")) - .findFirst() - .orElseThrow(() -> new IllegalStateException("No jar file system provider found")); -+ public static final double COLLISION_EPSILON = 1.0E-7; // Paper - Improve boat collision performance - private static Consumer thePauser = message -> { - }; - -diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index 5854e58a014b5581fa065d1db256ffc0aa947ce2..75e01c1e01e0782fc8af48777bbe4716d37aeafa 100644 ---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -1459,7 +1459,7 @@ public abstract class LivingEntity extends Entity implements Attackable { - if (!source.is(DamageTypeTags.IS_PROJECTILE)) { - Entity entity = source.getDirectEntity(); - -- if (entity instanceof LivingEntity) { -+ if (entity instanceof LivingEntity && entity.distanceToSqr(this) <= (200.0D * 200.0D)) { // Paper - Improve boat collision performance - LivingEntity entityliving = (LivingEntity) entity; - - this.blockUsingShield(entityliving); -@@ -1553,11 +1553,12 @@ public abstract class LivingEntity extends Entity implements Attackable { - } - - if (entity1 != null && !source.is(DamageTypeTags.NO_KNOCKBACK)) { -- double d0 = entity1.getX() - this.getX(); -+ final boolean far = entity1.distanceToSqr(this) > (200.0 * 200.0); // Paper - Improve boat collision performance -+ double d0 = far ? (Math.random() - Math.random()) : entity1.getX() - this.getX(); // Paper - Improve boat collision performance - - double d1; - -- for (d1 = entity1.getZ() - this.getZ(); d0 * d0 + d1 * d1 < 1.0E-4D; d1 = (Math.random() - Math.random()) * 0.01D) { -+ for (d1 = far ? Math.random() - Math.random() : entity1.getZ() - this.getZ(); d0 * d0 + d1 * d1 < 1.0E-4D; d1 = (Math.random() - Math.random()) * 0.01D) { // Paper - Improve boat collision performance - d0 = (Math.random() - Math.random()) * 0.01D; - } - -@@ -2334,7 +2335,7 @@ public abstract class LivingEntity extends Entity implements Attackable { - this.hurtCurrentlyUsedShield((float) -event.getDamage(DamageModifier.BLOCKING)); - Entity entity = damagesource.getDirectEntity(); - -- if (entity instanceof LivingEntity) { -+ if (entity instanceof LivingEntity && entity.distanceToSqr(this) <= (200.0D * 200.0D)) { // Paper - Improve boat collision performance - this.blockUsingShield((LivingEntity) entity); - } - } -diff --git a/src/main/java/net/minecraft/world/entity/vehicle/Boat.java b/src/main/java/net/minecraft/world/entity/vehicle/Boat.java -index 549202706396fee91c23f6e34f7faa7672e4b03f..b068cff9b5aa457d65b679529956e8210296d799 100644 ---- a/src/main/java/net/minecraft/world/entity/vehicle/Boat.java -+++ b/src/main/java/net/minecraft/world/entity/vehicle/Boat.java -@@ -686,7 +686,7 @@ public class Boat extends VehicleEntity implements VariantHolder { - this.invFriction = 0.05F; - if (this.oldStatus == Boat.Status.IN_AIR && this.status != Boat.Status.IN_AIR && this.status != Boat.Status.ON_LAND) { - this.waterLevel = this.getY(1.0D); -- this.setPos(this.getX(), (double) (this.getWaterLevelAbove() - this.getBbHeight()) + 0.101D, this.getZ()); -+ this.move(MoverType.SELF, new Vec3(0.0, ((double) (this.getWaterLevelAbove() - this.getBbHeight()) + 0.101D) - this.getY(), 0.0)); // Paper - Improve boat collision performance - this.setDeltaMovement(this.getDeltaMovement().multiply(1.0D, 0.0D, 1.0D)); - this.lastYd = 0.0D; - this.status = Boat.Status.IN_WATER; diff --git a/patches/server/1011-Optimise-general-POI-access.patch b/patches/server/1011-Optimise-general-POI-access.patch new file mode 100644 index 0000000000..118734c246 --- /dev/null +++ b/patches/server/1011-Optimise-general-POI-access.patch @@ -0,0 +1,1060 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sun, 31 Jan 2021 02:29:24 -0800 +Subject: [PATCH] Optimise general POI access + +There are a couple of problems with mojang's POI code. +Firstly, it's all streams. Unsurprisingly, stacking +streams on top of each other is horrible for performance +and ultimately took up half of a villager's tick! + +Secondly, sometime's the search radius is large and there are +a significant number of poi entries per chunk section. Even +removing streams at this point doesn't help much. The only solution +is to start at the search point and iterate outwards. This +type of approach shows massive gains for portals, simply because +we can avoid sync loading a large area of chunks. I also tested +a massive farm I found in JellySquid's discord, which showed +to benefit significantly simply because the farm had so many +portal blocks that searching through them all was very slow. + +Great care has been taken so that behavior remains identical to +vanilla, however I cannot account for oddball Stream API +implementations, if they even exist (streams can technically +be loose with iteration order in a sorted stream given its +source stream is not tagged with ordered, and mojang does not +tag the source stream as ordered). However in my testing on openjdk +there showed no difference, as expected. + +This patch also specifically optimises other areas of code to +use PoiAccess. For example, some villager AI and portaling code +had to be specifically modified. + +diff --git a/src/main/java/io/papermc/paper/util/PoiAccess.java b/src/main/java/io/papermc/paper/util/PoiAccess.java +new file mode 100644 +index 0000000000000000000000000000000000000000..69be1761b3b5ba7b496c1c10a4db897e6212d671 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/PoiAccess.java +@@ -0,0 +1,804 @@ ++package io.papermc.paper.util; ++ ++import com.mojang.datafixers.util.Pair; ++import it.unimi.dsi.fastutil.doubles.Double2ObjectMap; ++import it.unimi.dsi.fastutil.doubles.Double2ObjectRBTreeMap; ++import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; ++import it.unimi.dsi.fastutil.longs.LongOpenHashSet; ++import java.util.function.BiPredicate; ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.Holder; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.ai.village.poi.PoiManager; ++import net.minecraft.world.entity.ai.village.poi.PoiRecord; ++import net.minecraft.world.entity.ai.village.poi.PoiSection; ++import net.minecraft.world.entity.ai.village.poi.PoiType; ++import java.util.ArrayList; ++import java.util.HashSet; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Map; ++import java.util.Optional; ++import java.util.Set; ++import java.util.function.Predicate; ++ ++/** ++ * Provides optimised access to POI data. All returned values will be identical to vanilla. ++ */ ++public final class PoiAccess { ++ ++ protected static double clamp(final double val, final double min, final double max) { ++ return (val < min ? min : (val > max ? max : val)); ++ } ++ ++ protected static double getSmallestDistanceSquared(final double boxMinX, final double boxMinY, final double boxMinZ, ++ final double boxMaxX, final double boxMaxY, final double boxMaxZ, ++ ++ final double circleX, final double circleY, final double circleZ) { ++ // is the circle center inside the box? ++ if (circleX >= boxMinX && circleX <= boxMaxX && circleY >= boxMinY && circleY <= boxMaxY && circleZ >= boxMinZ && circleZ <= boxMaxZ) { ++ return 0.0; ++ } ++ ++ final double boxWidthX = (boxMaxX - boxMinX) / 2.0; ++ final double boxWidthY = (boxMaxY - boxMinY) / 2.0; ++ final double boxWidthZ = (boxMaxZ - boxMinZ) / 2.0; ++ ++ final double boxCenterX = (boxMinX + boxMaxX) / 2.0; ++ final double boxCenterY = (boxMinY + boxMaxY) / 2.0; ++ final double boxCenterZ = (boxMinZ + boxMaxZ) / 2.0; ++ ++ double centerDiffX = circleX - boxCenterX; ++ double centerDiffY = circleY - boxCenterY; ++ double centerDiffZ = circleZ - boxCenterZ; ++ ++ centerDiffX = circleX - (clamp(centerDiffX, -boxWidthX, boxWidthX) + boxCenterX); ++ centerDiffY = circleY - (clamp(centerDiffY, -boxWidthY, boxWidthY) + boxCenterY); ++ centerDiffZ = circleZ - (clamp(centerDiffZ, -boxWidthZ, boxWidthZ) + boxCenterZ); ++ ++ return (centerDiffX * centerDiffX) + (centerDiffY * centerDiffY) + (centerDiffZ * centerDiffZ); ++ } ++ ++ ++ // key is: ++ // upper 32 bits: ++ // upper 16 bits: max y section ++ // lower 16 bits: min y section ++ // lower 32 bits: ++ // upper 16 bits: section ++ // lower 16 bits: radius ++ protected static long getKey(final int minSection, final int maxSection, final int section, final int radius) { ++ return ( ++ (maxSection & 0xFFFFL) << (64 - 16) ++ | (minSection & 0xFFFFL) << (64 - 32) ++ | (section & 0xFFFFL) << (64 - 48) ++ | (radius & 0xFFFFL) << (64 - 64) ++ ); ++ } ++ ++ // only includes x/z axis ++ // finds the closest poi data by distance. ++ public static BlockPos findClosestPoiDataPosition(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final PoiRecord ret = findClosestPoiDataRecord( ++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load ++ ); ++ ++ return ret == null ? null : ret.getPos(); ++ } ++ ++ // only includes x/z axis ++ // finds the closest poi data by distance. ++ public static Pair, BlockPos> findClosestPoiDataTypeAndPosition(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final PoiRecord ret = findClosestPoiDataRecord( ++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load ++ ); ++ ++ return ret == null ? null : Pair.of(ret.getPoiType(), ret.getPos()); ++ } ++ ++ // only includes x/z axis ++ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned. ++ public static void findClosestPoiDataPositions(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final Set ret) { ++ final Set positions = new HashSet<>(); ++ // pos predicate is last thing that runs before adding to ret. ++ final Predicate newPredicate = (final BlockPos pos) -> { ++ if (positionPredicate != null && !positionPredicate.test(pos)) { ++ return false; ++ } ++ return positions.add(pos.immutable()); ++ }; ++ ++ final List toConvert = new ArrayList<>(); ++ findClosestPoiDataRecords( ++ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, toConvert ++ ); ++ ++ for (final PoiRecord record : toConvert) { ++ ret.add(record.getPos()); ++ } ++ } ++ ++ // only includes x/z axis ++ // finds the closest poi data by distance. ++ public static PoiRecord findClosestPoiDataRecord(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final List ret = new ArrayList<>(); ++ findClosestPoiDataRecords( ++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret ++ ); ++ return ret.isEmpty() ? null : ret.get(0); ++ } ++ ++ // only includes x/z axis ++ // finds the closest poi data by distance. ++ public static PoiRecord findClosestPoiDataRecord(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final BiPredicate, BlockPos> predicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final List ret = new ArrayList<>(); ++ findClosestPoiDataRecords( ++ poiStorage, villagePlaceType, predicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret ++ ); ++ return ret.isEmpty() ? null : ret.get(0); ++ } ++ ++ // only includes x/z axis ++ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned. ++ public static void findClosestPoiDataRecords(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final List ret) { ++ final BiPredicate, BlockPos> predicate = positionPredicate != null ? (type, pos) -> positionPredicate.test(pos) : null; ++ findClosestPoiDataRecords(poiStorage, villagePlaceType, predicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret); ++ } ++ ++ public static void findClosestPoiDataRecords(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final BiPredicate, BlockPos> predicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final List ret) { ++ final Predicate occupancyFilter = occupancy.getTest(); ++ ++ final List closestRecords = new ArrayList<>(); ++ double closestDistanceSquared = maxDistanceSquared; ++ ++ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; ++ final int lowerY = WorldUtil.getMinSection(poiStorage.world); ++ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; ++ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; ++ final int upperY = WorldUtil.getMaxSection(poiStorage.world); ++ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; ++ ++ final int centerX = sourcePosition.getX() >> 4; ++ final int centerY = Mth.clamp(sourcePosition.getY() >> 4, lowerY, upperY); ++ final int centerZ = sourcePosition.getZ() >> 4; ++ final long centerKey = CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ); ++ ++ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); ++ final LongOpenHashSet seen = new LongOpenHashSet(); ++ seen.add(centerKey); ++ queue.enqueue(centerKey); ++ ++ while (!queue.isEmpty()) { ++ final long key = queue.dequeueLong(); ++ final int sectionX = CoordinateUtils.getChunkSectionX(key); ++ final int sectionY = CoordinateUtils.getChunkSectionY(key); ++ final int sectionZ = CoordinateUtils.getChunkSectionZ(key); ++ ++ if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) { ++ // out of bound chunk ++ continue; ++ } ++ ++ final double sectionDistanceSquared = getSmallestDistanceSquared( ++ (sectionX << 4) + 0.5, ++ (sectionY << 4) + 0.5, ++ (sectionZ << 4) + 0.5, ++ (sectionX << 4) + 15.5, ++ (sectionY << 4) + 15.5, ++ (sectionZ << 4) + 15.5, ++ (double)sourcePosition.getX(), (double)sourcePosition.getY(), (double)sourcePosition.getZ() ++ ); ++ if (sectionDistanceSquared > closestDistanceSquared) { ++ continue; ++ } ++ ++ // queue all neighbours ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ for (int dy = -1; dy <= 1; ++dy) { ++ // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many ++ // values are set. we only care about cardinal neighbours, so, we only care if one value is set ++ if ((dx & 1) + (dy & 1) + (dz & 1) != 1) { ++ continue; ++ } ++ ++ final int neighbourX = sectionX + dx; ++ final int neighbourY = sectionY + dy; ++ final int neighbourZ = sectionZ + dz; ++ ++ final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ); ++ if (seen.add(neighbourKey)) { ++ queue.enqueue(neighbourKey); ++ } ++ } ++ } ++ } ++ ++ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key); ++ ++ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) { ++ continue; ++ } ++ ++ final PoiSection poiSection = poiSectionOptional.get(); ++ ++ final Map, Set> sectionData = poiSection.getData(); ++ if (sectionData.isEmpty()) { ++ continue; ++ } ++ ++ // now we search the section data ++ for (final Map.Entry, Set> entry : sectionData.entrySet()) { ++ if (!villagePlaceType.test(entry.getKey())) { ++ // filter out by poi type ++ continue; ++ } ++ ++ // now we can look at the poi data ++ for (final PoiRecord poiData : entry.getValue()) { ++ if (!occupancyFilter.test(poiData)) { ++ // filter by occupancy ++ continue; ++ } ++ ++ final BlockPos poiPosition = poiData.getPos(); ++ ++ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range ++ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { ++ // out of range for square radius ++ continue; ++ } ++ ++ // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped! ++ final double dataRange = poiPosition.distSqr(sourcePosition); ++ ++ if (dataRange > closestDistanceSquared) { ++ // out of range for distance check ++ continue; ++ } ++ ++ if (predicate != null && !predicate.test(poiData.getPoiType(), poiPosition)) { ++ // filter by position ++ continue; ++ } ++ ++ if (dataRange < closestDistanceSquared) { ++ closestRecords.clear(); ++ closestDistanceSquared = dataRange; ++ } ++ closestRecords.add(poiData); ++ } ++ } ++ } ++ ++ // uh oh! we might have multiple records that match the distance sorting! ++ // we need to re-order our results by the way vanilla would have iterated over them. ++ closestRecords.sort((record1, record2) -> { ++ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section ++ // is fine and should be preserved (this sort is stable so we're good there) ++ // but they iterate sections by x then by z (like the following) ++ // for (int x = -dx; x <= dx; ++x) ++ // for (int z = -dz; z <= dz; ++z) ++ // .... ++ // so we need to reorder such that records with lower chunk z, then lower chunk x come first ++ final BlockPos pos1 = record1.getPos(); ++ final BlockPos pos2 = record2.getPos(); ++ ++ final int cx1 = pos1.getX() >> 4; ++ final int cz1 = pos1.getZ() >> 4; ++ ++ final int cx2 = pos2.getX() >> 4; ++ final int cz2 = pos2.getZ() >> 4; ++ ++ if (cz2 != cz1) { ++ // want smaller z ++ return Integer.compare(cz1, cz2); ++ } ++ ++ if (cx2 != cx1) { ++ // want smaller x ++ return Integer.compare(cx1, cx2); ++ } ++ ++ // same chunk ++ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y ++ // so now we just compare section y, wanting smaller y ++ ++ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4); ++ }); ++ ++ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully). ++ ret.addAll(closestRecords); ++ } ++ ++ // finds the closest poi entry pos. ++ public static BlockPos findNearestPoiPosition(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final PoiRecord ret = findNearestPoiRecord( ++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load ++ ); ++ return ret == null ? null : ret.getPos(); ++ } ++ ++ // finds the closest `max` poi entry positions. ++ public static void findNearestPoiPositions(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final int max, ++ final List, BlockPos>> ret) { ++ final Set positions = new HashSet<>(); ++ // pos predicate is last thing that runs before adding to ret. ++ final Predicate newPredicate = (final BlockPos pos) -> { ++ if (positionPredicate != null && !positionPredicate.test(pos)) { ++ return false; ++ } ++ return positions.add(pos.immutable()); ++ }; ++ ++ final List toConvert = new ArrayList<>(); ++ findNearestPoiRecords( ++ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, max, toConvert ++ ); ++ ++ for (final PoiRecord record : toConvert) { ++ ret.add(Pair.of(record.getPoiType(), record.getPos())); ++ } ++ } ++ ++ // finds the closest poi entry. ++ public static PoiRecord findNearestPoiRecord(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final List ret = new ArrayList<>(); ++ findNearestPoiRecords( ++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ++ 1, ret ++ ); ++ return ret.isEmpty() ? null : ret.get(0); ++ } ++ ++ // finds the closest `max` poi entries. ++ public static void findNearestPoiRecords(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ // position predicate must not modify chunk POI ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final double maxDistanceSquared, ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final int max, ++ final List ret) { ++ final Predicate occupancyFilter = occupancy.getTest(); ++ ++ final Double2ObjectRBTreeMap> closestRecords = new Double2ObjectRBTreeMap<>(); ++ int totalRecords = 0; ++ double furthestDistanceSquared = maxDistanceSquared; ++ ++ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; ++ final int lowerY = WorldUtil.getMinSection(poiStorage.world); ++ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; ++ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; ++ final int upperY = WorldUtil.getMaxSection(poiStorage.world); ++ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; ++ ++ final int centerX = sourcePosition.getX() >> 4; ++ final int centerY = Mth.clamp(sourcePosition.getY() >> 4, lowerY, upperY); ++ final int centerZ = sourcePosition.getZ() >> 4; ++ final long centerKey = CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ); ++ ++ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); ++ final LongOpenHashSet seen = new LongOpenHashSet(); ++ seen.add(centerKey); ++ queue.enqueue(centerKey); ++ ++ while (!queue.isEmpty()) { ++ final long key = queue.dequeueLong(); ++ final int sectionX = CoordinateUtils.getChunkSectionX(key); ++ final int sectionY = CoordinateUtils.getChunkSectionY(key); ++ final int sectionZ = CoordinateUtils.getChunkSectionZ(key); ++ ++ if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) { ++ // out of bound chunk ++ continue; ++ } ++ ++ final double sectionDistanceSquared = getSmallestDistanceSquared( ++ (sectionX << 4) + 0.5, ++ (sectionY << 4) + 0.5, ++ (sectionZ << 4) + 0.5, ++ (sectionX << 4) + 15.5, ++ (sectionY << 4) + 15.5, ++ (sectionZ << 4) + 15.5, ++ (double) sourcePosition.getX(), (double) sourcePosition.getY(), (double) sourcePosition.getZ() ++ ); ++ ++ if (sectionDistanceSquared > (totalRecords >= max ? furthestDistanceSquared : maxDistanceSquared)) { ++ continue; ++ } ++ ++ // queue all neighbours ++ for (int dz = -1; dz <= 1; ++dz) { ++ for (int dx = -1; dx <= 1; ++dx) { ++ for (int dy = -1; dy <= 1; ++dy) { ++ // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many ++ // values are set. we only care about cardinal neighbours, so, we only care if one value is set ++ if ((dx & 1) + (dy & 1) + (dz & 1) != 1) { ++ continue; ++ } ++ ++ final int neighbourX = sectionX + dx; ++ final int neighbourY = sectionY + dy; ++ final int neighbourZ = sectionZ + dz; ++ ++ final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ); ++ if (seen.add(neighbourKey)) { ++ queue.enqueue(neighbourKey); ++ } ++ } ++ } ++ } ++ ++ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key); ++ ++ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) { ++ continue; ++ } ++ ++ final PoiSection poiSection = poiSectionOptional.get(); ++ ++ final Map, Set> sectionData = poiSection.getData(); ++ if (sectionData.isEmpty()) { ++ continue; ++ } ++ ++ // now we search the section data ++ for (final Map.Entry, Set> entry : sectionData.entrySet()) { ++ if (!villagePlaceType.test(entry.getKey())) { ++ // filter out by poi type ++ continue; ++ } ++ ++ // now we can look at the poi data ++ for (final PoiRecord poiData : entry.getValue()) { ++ if (!occupancyFilter.test(poiData)) { ++ // filter by occupancy ++ continue; ++ } ++ ++ final BlockPos poiPosition = poiData.getPos(); ++ ++ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range ++ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { ++ // out of range for square radius ++ continue; ++ } ++ ++ // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped! ++ final double dataRange = poiPosition.distSqr(sourcePosition); ++ ++ if (dataRange > maxDistanceSquared) { ++ // out of range for distance check ++ continue; ++ } ++ ++ if (dataRange > furthestDistanceSquared && totalRecords >= max) { ++ // out of range for distance check ++ continue; ++ } ++ ++ if (positionPredicate != null && !positionPredicate.test(poiPosition)) { ++ // filter by position ++ continue; ++ } ++ ++ if (dataRange > furthestDistanceSquared) { ++ // we know totalRecords < max, so this entry is now our furthest ++ furthestDistanceSquared = dataRange; ++ } ++ ++ closestRecords.computeIfAbsent(dataRange, (final double unused) -> { ++ return new ArrayList<>(); ++ }).add(poiData); ++ ++ if (++totalRecords >= max) { ++ if (closestRecords.size() >= 2) { ++ int entriesInClosest = 0; ++ final Iterator>> iterator = closestRecords.double2ObjectEntrySet().iterator(); ++ double nextFurthestDistanceSquared = 0.0; ++ ++ for (int i = 0, len = closestRecords.size() - 1; i < len; ++i) { ++ final Double2ObjectMap.Entry> recordEntry = iterator.next(); ++ entriesInClosest += recordEntry.getValue().size(); ++ nextFurthestDistanceSquared = recordEntry.getDoubleKey(); ++ } ++ ++ if (entriesInClosest >= max) { ++ // the last set of entries at range wont even be considered for sure... nuke em ++ final Double2ObjectMap.Entry> recordEntry = iterator.next(); ++ totalRecords -= recordEntry.getValue().size(); ++ iterator.remove(); ++ ++ furthestDistanceSquared = nextFurthestDistanceSquared; ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ final List closestRecordsUnsorted = new ArrayList<>(); ++ ++ // we're done here, so now just flatten the map and sort it. ++ ++ for (final List records : closestRecords.values()) { ++ closestRecordsUnsorted.addAll(records); ++ } ++ ++ // uh oh! we might have multiple records that match the distance sorting! ++ // we need to re-order our results by the way vanilla would have iterated over them. ++ closestRecordsUnsorted.sort((record1, record2) -> { ++ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section ++ // is fine and should be preserved (this sort is stable so we're good there) ++ // but they iterate sections by x then by z (like the following) ++ // for (int x = -dx; x <= dx; ++x) ++ // for (int z = -dz; z <= dz; ++z) ++ // .... ++ // so we need to reorder such that records with lower chunk z, then lower chunk x come first ++ final BlockPos pos1 = record1.getPos(); ++ final BlockPos pos2 = record2.getPos(); ++ ++ final int cx1 = pos1.getX() >> 4; ++ final int cz1 = pos1.getZ() >> 4; ++ ++ final int cx2 = pos2.getX() >> 4; ++ final int cz2 = pos2.getZ() >> 4; ++ ++ if (cz2 != cz1) { ++ // want smaller z ++ return Integer.compare(cz1, cz2); ++ } ++ ++ if (cx2 != cx1) { ++ // want smaller x ++ return Integer.compare(cx1, cx2); ++ } ++ ++ // same chunk ++ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y ++ // so now we just compare section y, wanting smaller section y ++ ++ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4); ++ }); ++ ++ // trim out any entries exceeding our maximum ++ for (int i = closestRecordsUnsorted.size() - 1; i >= max; --i) { ++ closestRecordsUnsorted.remove(i); ++ } ++ ++ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully). ++ ret.addAll(closestRecordsUnsorted); ++ } ++ ++ public static BlockPos findAnyPoiPosition(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final PoiRecord ret = findAnyPoiRecord( ++ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load ++ ); ++ ++ return ret == null ? null : ret.getPos(); ++ } ++ ++ public static void findAnyPoiPositions(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final int max, ++ final List, BlockPos>> ret) { ++ final Set positions = new HashSet<>(); ++ // pos predicate is last thing that runs before adding to ret. ++ final Predicate newPredicate = (final BlockPos pos) -> { ++ if (positionPredicate != null && !positionPredicate.test(pos)) { ++ return false; ++ } ++ return positions.add(pos.immutable()); ++ }; ++ ++ final List toConvert = new ArrayList<>(); ++ findAnyPoiRecords( ++ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, occupancy, load, max, toConvert ++ ); ++ ++ for (final PoiRecord record : toConvert) { ++ ret.add(Pair.of(record.getPoiType(), record.getPos())); ++ } ++ } ++ ++ public static PoiRecord findAnyPoiRecord(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final PoiManager.Occupancy occupancy, ++ final boolean load) { ++ final List ret = new ArrayList<>(); ++ findAnyPoiRecords(poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load, 1, ret); ++ return ret.isEmpty() ? null : ret.get(0); ++ } ++ ++ public static void findAnyPoiRecords(final PoiManager poiStorage, ++ final Predicate> villagePlaceType, ++ final Predicate positionPredicate, ++ final BlockPos sourcePosition, ++ final int range, // distance on x y z axis ++ final PoiManager.Occupancy occupancy, ++ final boolean load, ++ final int max, ++ final List 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 ++ final Predicate occupancyFilter = occupancy.getTest(); ++ final double rangeSquared = range * range; ++ ++ int added = 0; ++ ++ // First up, we need to iterate the chunks ++ // all the values here are in chunk sections ++ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; ++ final int lowerY = Math.max(WorldUtil.getMinSection(poiStorage.world), Mth.floor(sourcePosition.getY() - range) >> 4); ++ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; ++ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; ++ final int upperY = Math.min(WorldUtil.getMaxSection(poiStorage.world), Mth.floor(sourcePosition.getY() + range) >> 4); ++ final int upperZ = Mth.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 ++ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(CoordinateUtils.getChunkSectionKey(currX, currY, currZ)) : ++ poiStorage.get(CoordinateUtils.getChunkSectionKey(currX, currY, currZ)); ++ final PoiSection poiSection = poiSectionOptional == null ? null : poiSectionOptional.orElse(null); ++ if (poiSection == null) { ++ continue; ++ } ++ ++ final Map, Set> sectionData = poiSection.getData(); ++ if (sectionData.isEmpty()) { ++ continue; ++ } ++ ++ // now we search the section data ++ for (final Map.Entry, Set> entry : sectionData.entrySet()) { ++ if (!villagePlaceType.test(entry.getKey())) { ++ // filter out by poi type ++ continue; ++ } ++ ++ // now we can look at the poi data ++ for (final PoiRecord poiData : entry.getValue()) { ++ if (!occupancyFilter.test(poiData)) { ++ // filter by occupancy ++ continue; ++ } ++ ++ final BlockPos poiPosition = poiData.getPos(); ++ ++ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range ++ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { ++ // out of range for square radius ++ continue; ++ } ++ ++ if (poiPosition.distSqr(sourcePosition) > rangeSquared) { ++ // out of range for distance check ++ continue; ++ } ++ ++ if (positionPredicate != null && !positionPredicate.test(poiPosition)) { ++ // filter by position ++ continue; ++ } ++ ++ // found one! ++ ret.add(poiData); ++ if (++added >= max) { ++ return; ++ } ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ private PoiAccess() { ++ throw new RuntimeException(); ++ } ++} +diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java +index e8aa27547e3fa1a42720889c7038d4fb0273e7b5..e1b6fe9ecda25f86431baf414f1bfd3a26a8b2bd 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java ++++ b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java +@@ -71,11 +71,11 @@ public class AcquirePoi { + return true; + } + }; +- Set, BlockPos>> set = poiManager.findAllClosestFirstWithType( +- poiPredicate, predicate2, entity.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE +- ) +- .limit(5L) +- .collect(Collectors.toSet()); ++ // Paper start - optimise POI access ++ java.util.List, BlockPos>> poiposes = new java.util.ArrayList<>(); ++ io.papermc.paper.util.PoiAccess.findNearestPoiPositions(poiManager, poiPredicate, predicate2, entity.blockPosition(), 48, 48*48, PoiManager.Occupancy.HAS_SPACE, false, 5, poiposes); ++ Set, BlockPos>> set = new java.util.HashSet<>(poiposes); ++ // Paper end - optimise POI access + Path path = findPathToPois(entity, set); + if (path != null && path.canReach()) { + BlockPos blockPos = path.getTarget(); +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java +index d5a549f08b98c80a5cf0eef02cb8a389c32dfecb..92731b6b593289e9f583c9b705b219e81fcd8e73 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java +@@ -53,11 +53,12 @@ public class NearestBedSensor extends Sensor { + return true; + } + }; +- Set, BlockPos>> set = poiManager.findAllWithType( +- holder -> holder.is(PoiTypes.HOME), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY +- ) +- .collect(Collectors.toSet()); +- Path path = AcquirePoi.findPathToPois(entity, set); ++ // Paper start - optimise POI access ++ java.util.List, BlockPos>> poiposes = new java.util.ArrayList<>(); ++ // don't ask me why it's unbounded. ask mojang. ++ io.papermc.paper.util.PoiAccess.findAnyPoiPositions(poiManager, type -> type.is(PoiTypes.HOME), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY, false, Integer.MAX_VALUE, poiposes); ++ Path path = AcquirePoi.findPathToPois(entity, new java.util.HashSet<>(poiposes)); ++ // Paper end - optimise POI access + if (path != null && path.canReach()) { + BlockPos blockPos = path.getTarget(); + Optional> optional = poiManager.getType(blockPos); +diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +index 7b52b0507cbda76aee1db954641f397bef51f94d..c6f193339fdcbcc938d4eafdcad0b112cf1698d5 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java ++++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +@@ -138,36 +138,45 @@ public class PoiManager extends SectionStorage { + public Optional find( + Predicate> typePredicate, Predicate posPredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus + ) { +- return this.findAll(typePredicate, posPredicate, pos, radius, occupationStatus).findFirst(); ++ // Paper start - re-route to faster logic ++ BlockPos ret = io.papermc.paper.util.PoiAccess.findAnyPoiPosition(this, typePredicate, posPredicate, pos, radius, occupationStatus, false); ++ return Optional.ofNullable(ret); ++ // Paper end + } + + public Optional findClosest(Predicate> typePredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus) { +- return this.getInRange(typePredicate, pos, radius, occupationStatus) +- .map(PoiRecord::getPos) +- .min(Comparator.comparingDouble(blockPos2 -> blockPos2.distSqr(pos))); ++ // Paper start - re-route to faster logic ++ BlockPos ret = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, null, pos, radius, radius * radius, occupationStatus, false); ++ return Optional.ofNullable(ret); ++ // Paper end - re-route to faster logic + } + + public Optional, BlockPos>> findClosestWithType( + Predicate> typePredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus + ) { +- return this.getInRange(typePredicate, pos, radius, occupationStatus) +- .min(Comparator.comparingDouble(poi -> poi.getPos().distSqr(pos))) +- .map(poi -> Pair.of(poi.getPoiType(), poi.getPos())); ++ // Paper start - re-route to faster logic ++ return Optional.ofNullable(io.papermc.paper.util.PoiAccess.findClosestPoiDataTypeAndPosition( ++ this, typePredicate, null, pos, radius, radius * radius, occupationStatus, false ++ )); ++ // Paper end - re-route to faster logic + } + + public Optional findClosest( + Predicate> typePredicate, Predicate posPredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus + ) { +- return this.getInRange(typePredicate, pos, radius, occupationStatus) +- .map(PoiRecord::getPos) +- .filter(posPredicate) +- .min(Comparator.comparingDouble(blockPos2 -> blockPos2.distSqr(pos))); ++ // Paper start - re-route to faster logic ++ BlockPos ret = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, posPredicate, pos, radius, radius * radius, occupationStatus, false); ++ return Optional.ofNullable(ret); ++ // Paper end - re-route to faster logic + } + + public Optional take(Predicate> typePredicate, BiPredicate, BlockPos> biPredicate, BlockPos pos, int radius) { +- return this.getInRange(typePredicate, pos, radius, PoiManager.Occupancy.HAS_SPACE) +- .filter(poi -> biPredicate.test(poi.getPoiType(), poi.getPos())) +- .findFirst() ++ // Paper start - re-route to faster logic ++ final @javax.annotation.Nullable PoiRecord closest = io.papermc.paper.util.PoiAccess.findClosestPoiDataRecord( ++ this, typePredicate, biPredicate, pos, radius, radius * radius, Occupancy.HAS_SPACE, false ++ ); ++ return Optional.ofNullable(closest) ++ // Paper end - re-route to faster logic + .map(poi -> { + poi.acquireTicket(); + return poi.getPos(); +@@ -182,8 +191,21 @@ public class PoiManager extends SectionStorage { + int radius, + RandomSource random + ) { +- List list = Util.toShuffledList(this.getInRange(typePredicate, pos, radius, occupationStatus), random); +- return list.stream().filter(poi -> positionPredicate.test(poi.getPos())).findFirst().map(PoiRecord::getPos); ++ // Paper start - re-route to faster logic ++ List list = new java.util.ArrayList<>(); ++ io.papermc.paper.util.PoiAccess.findAnyPoiRecords( ++ this, typePredicate, positionPredicate, pos, radius, occupationStatus, false, Integer.MAX_VALUE, list ++ ); ++ ++ // the old method shuffled the list and then tried to find the first element in it that ++ // matched positionPredicate, however we moved positionPredicate into the poi search. This means we can avoid a ++ // shuffle entirely, and just pick a random element from list ++ if (list.isEmpty()) { ++ return Optional.empty(); ++ } ++ ++ return Optional.of(list.get(random.nextInt(list.size())).getPos()); ++ // Paper end - re-route to faster logic + } + + public boolean release(BlockPos pos) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java +index 5b7deae326228e482b218aeebd857a59b7434eaf..4ee7d75c56d9f9ff3607276857dde84410ba3f2a 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java ++++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java +@@ -26,7 +26,7 @@ import org.slf4j.Logger; + public class PoiSection { + private static final Logger LOGGER = LogUtils.getLogger(); + private final Short2ObjectMap records = new Short2ObjectOpenHashMap<>(); +- private final Map, Set> byType = Maps.newHashMap(); ++ private final Map, Set> byType = Maps.newHashMap(); public final Map, Set> getData() { return this.byType; } // Paper - public accessor + private final Runnable setDirty; + private boolean isValid; + public final Optional noAllocateOptional = Optional.of(this); // Paper - rewrite chunk system +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java +index 883fbe5c81e3be27007a1a0489f80ba1863e5a04..a4a919d8373f1535e336de7e648d41a07efb1cba 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java +@@ -74,11 +74,11 @@ public class SectionStorage extends RegionFileStorage implements AutoCloseabl + } + + @Nullable +- protected Optional get(long pos) { ++ public Optional get(long pos) { // Paper - public + return this.storage.get(pos); + } + +- protected Optional getOrLoad(long pos) { ++ public Optional getOrLoad(long pos) { // Paper - public + if (this.outsideStoredRange(pos)) { + return Optional.empty(); + } else { +diff --git a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java +index a61959700d5e00739a79eaa617ac383160335f26..2407d93c11b806701fc7d192f39d535128281e80 100644 +--- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java ++++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java +@@ -51,18 +51,39 @@ public class PortalForcer { + // int i = flag ? 16 : 128; + // CraftBukkit end + +- villageplace.ensureLoadedAndValid(this.level, blockposition, i); +- Optional optional = villageplace.getInSquare((holder) -> { +- return holder.is(PoiTypes.NETHER_PORTAL); +- }, blockposition, i, PoiManager.Occupancy.ANY).filter((villageplacerecord) -> { +- return worldborder.isWithinBounds(villageplacerecord.getPos()) && !(this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> villageplacerecord.getPos().getY() >= v)); // Paper - Configurable nether ceiling damage +- }).sorted(Comparator.comparingDouble((PoiRecord villageplacerecord) -> { // CraftBukkit - decompile error +- return villageplacerecord.getPos().distSqr(blockposition); +- }).thenComparingInt((villageplacerecord) -> { +- return villageplacerecord.getPos().getY(); +- })).filter((villageplacerecord) -> { +- return this.level.getBlockState(villageplacerecord.getPos()).hasProperty(BlockStateProperties.HORIZONTAL_AXIS); +- }).findFirst(); ++ // Paper start - optimise portals ++ Optional optional; ++ java.util.List records = new java.util.ArrayList<>(); ++ io.papermc.paper.util.PoiAccess.findClosestPoiDataRecords( ++ villageplace, ++ type -> type.is(PoiTypes.NETHER_PORTAL), ++ (BlockPos pos) -> { ++ net.minecraft.world.level.chunk.ChunkAccess lowest = this.level.getChunk(pos.getX() >> 4, pos.getZ() >> 4, net.minecraft.world.level.chunk.status.ChunkStatus.EMPTY); ++ if (!lowest.getStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.FULL) ++ && (lowest.getBelowZeroRetrogen() == null || !lowest.getBelowZeroRetrogen().targetStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.SPAWN))) { ++ // why would we generate the chunk? ++ return false; ++ } ++ if (!worldborder.isWithinBounds(pos) || (this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> pos.getY() >= v))) { // Paper - Configurable nether ceiling damage ++ return false; ++ } ++ return lowest.getBlockState(pos).hasProperty(BlockStateProperties.HORIZONTAL_AXIS); ++ }, ++ blockposition, i, Double.MAX_VALUE, PoiManager.Occupancy.ANY, true, records ++ ); ++ ++ // this gets us most of the way there, but we bias towards lower y values. ++ PoiRecord lowestYRecord = null; ++ for (PoiRecord record : records) { ++ if (lowestYRecord == null) { ++ lowestYRecord = record; ++ } else if (lowestYRecord.getPos().getY() > record.getPos().getY()) { ++ lowestYRecord = record; ++ } ++ } ++ // now we're done ++ optional = Optional.ofNullable(lowestYRecord); ++ // Paper end - optimise portals + + return optional.map((villageplacerecord) -> { + BlockPos blockposition1 = villageplacerecord.getPos(); diff --git a/patches/server/1012-Custom-table-implementation-for-blockstate-state-loo.patch b/patches/server/1012-Custom-table-implementation-for-blockstate-state-loo.patch new file mode 100644 index 0000000000..96d90abea3 --- /dev/null +++ b/patches/server/1012-Custom-table-implementation-for-blockstate-state-loo.patch @@ -0,0 +1,343 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Thu, 11 Mar 2021 20:05:44 -0800 +Subject: [PATCH] Custom table implementation for blockstate state lookups + +Testing some redstone intensive machines showed to bring about a 10% +improvement. + +diff --git a/src/main/java/io/papermc/paper/util/table/ZeroCollidingReferenceStateTable.java b/src/main/java/io/papermc/paper/util/table/ZeroCollidingReferenceStateTable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..57d0cd3ad6f972e986c72a57f1a6e36003f190c2 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/table/ZeroCollidingReferenceStateTable.java +@@ -0,0 +1,160 @@ ++package io.papermc.paper.util.table; ++ ++import com.google.common.collect.Table; ++import net.minecraft.world.level.block.state.StateHolder; ++import net.minecraft.world.level.block.state.properties.Property; ++import java.util.Collection; ++import java.util.HashSet; ++import java.util.Map; ++import java.util.Set; ++ ++public final class ZeroCollidingReferenceStateTable { ++ ++ // upper 32 bits: starting index ++ // lower 32 bits: bitset for contained ids ++ protected final long[] this_index_table; ++ protected final Comparable[] this_table; ++ protected final StateHolder this_state; ++ ++ protected long[] index_table; ++ protected StateHolder[][] value_table; ++ ++ public ZeroCollidingReferenceStateTable(final StateHolder state, final Map, Comparable> this_map) { ++ this.this_state = state; ++ this.this_index_table = this.create_table(this_map.keySet()); ++ ++ int max_id = -1; ++ for (final Property property : this_map.keySet()) { ++ final int id = lookup_vindex(property, this.this_index_table); ++ if (id > max_id) { ++ max_id = id; ++ } ++ } ++ ++ this.this_table = new Comparable[max_id + 1]; ++ for (final Map.Entry, Comparable> entry : this_map.entrySet()) { ++ this.this_table[lookup_vindex(entry.getKey(), this.this_index_table)] = entry.getValue(); ++ } ++ } ++ ++ public void loadInTable(final Table, Comparable, StateHolder> table, ++ final Map, Comparable> this_map) { ++ final Set> combined = new HashSet<>(table.rowKeySet()); ++ combined.addAll(this_map.keySet()); ++ ++ this.index_table = this.create_table(combined); ++ ++ int max_id = -1; ++ for (final Property property : combined) { ++ final int id = lookup_vindex(property, this.index_table); ++ if (id > max_id) { ++ max_id = id; ++ } ++ } ++ ++ this.value_table = new StateHolder[max_id + 1][]; ++ ++ final Map, Map, StateHolder>> map = table.rowMap(); ++ for (final Property property : map.keySet()) { ++ final Map, StateHolder> propertyMap = map.get(property); ++ ++ final int id = lookup_vindex(property, this.index_table); ++ final StateHolder[] states = this.value_table[id] = new StateHolder[property.getPossibleValues().size()]; ++ ++ for (final Map.Entry, StateHolder> entry : propertyMap.entrySet()) { ++ if (entry.getValue() == null) { ++ // TODO what ++ continue; ++ } ++ ++ states[((Property)property).getIdFor(entry.getKey())] = entry.getValue(); ++ } ++ } ++ ++ ++ for (final Map.Entry, Comparable> entry : this_map.entrySet()) { ++ final Property property = entry.getKey(); ++ final int index = lookup_vindex(property, this.index_table); ++ ++ if (this.value_table[index] == null) { ++ this.value_table[index] = new StateHolder[property.getPossibleValues().size()]; ++ } ++ ++ this.value_table[index][((Property)property).getIdFor(entry.getValue())] = this.this_state; ++ } ++ } ++ ++ ++ protected long[] create_table(final Collection> collection) { ++ int max_id = -1; ++ for (final Property property : collection) { ++ final int id = property.getId(); ++ if (id > max_id) { ++ max_id = id; ++ } ++ } ++ ++ final long[] ret = new long[((max_id + 1) + 31) >>> 5]; // ceil((max_id + 1) / 32) ++ ++ for (final Property property : collection) { ++ final int id = property.getId(); ++ ++ ret[id >>> 5] |= (1L << (id & 31)); ++ } ++ ++ int total = 0; ++ for (int i = 1, len = ret.length; i < len; ++i) { ++ ret[i] |= (long)(total += Long.bitCount(ret[i - 1] & 0xFFFFFFFFL)) << 32; ++ } ++ ++ return ret; ++ } ++ ++ public Comparable get(final Property state) { ++ final Comparable[] table = this.this_table; ++ final int index = lookup_vindex(state, this.this_index_table); ++ ++ if (index < 0 || index >= table.length) { ++ return null; ++ } ++ return table[index]; ++ } ++ ++ public StateHolder get(final Property property, final Comparable with) { ++ final int withId = ((Property)property).getIdFor(with); ++ if (withId < 0) { ++ return null; ++ } ++ ++ final int index = lookup_vindex(property, this.index_table); ++ final StateHolder[][] table = this.value_table; ++ if (index < 0 || index >= table.length) { ++ return null; ++ } ++ ++ final StateHolder[] values = table[index]; ++ ++ if (withId >= values.length) { ++ return null; ++ } ++ ++ return values[withId]; ++ } ++ ++ protected static int lookup_vindex(final Property property, final long[] index_table) { ++ final int id = property.getId(); ++ final long bitset_mask = (1L << (id & 31)); ++ final long lower_mask = bitset_mask - 1; ++ final int index = id >>> 5; ++ if (index >= index_table.length) { ++ return -1; ++ } ++ final long index_value = index_table[index]; ++ final long contains_check = ((index_value & bitset_mask) - 1) >> (Long.SIZE - 1); // -1L if doesn't contain ++ ++ // index = total bits set in lower table values (upper 32 bits of index_value) plus total bits set in lower indices below id ++ // contains_check is 0 if the bitset had id set, else it's -1: so index is unaffected if contains_check == 0, ++ // otherwise it comes out as -1. ++ return (int)(((index_value >>> 32) + Long.bitCount(index_value & lower_mask)) | contains_check); ++ } ++} +diff --git a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java +index daedcfd867ed6171fb61bdcbded417a11c8a5b0f..45744d86e9582a93a0cec26009deea091080fbbe 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java ++++ b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java +@@ -39,11 +39,13 @@ public abstract class StateHolder { + private final Reference2ObjectArrayMap, Comparable> values; + private Table, Comparable, S> neighbours; + protected final MapCodec propertiesCodec; ++ protected final io.papermc.paper.util.table.ZeroCollidingReferenceStateTable optimisedTable; // Paper - optimise state lookup + + protected StateHolder(O owner, Reference2ObjectArrayMap, Comparable> propertyMap, MapCodec codec) { + this.owner = owner; + this.values = propertyMap; + this.propertiesCodec = codec; ++ this.optimisedTable = new io.papermc.paper.util.table.ZeroCollidingReferenceStateTable(this, propertyMap); // Paper - optimise state lookup + } + + public > S cycle(Property property) { +@@ -84,11 +86,11 @@ public abstract class StateHolder { + } + + public > boolean hasProperty(Property property) { +- return this.values.containsKey(property); ++ return this.optimisedTable.get(property) != null; // Paper - optimise state lookup + } + + public > T getValue(Property property) { +- Comparable comparable = this.values.get(property); ++ Comparable comparable = this.optimisedTable.get(property); // Paper - optimise state lookup + if (comparable == null) { + throw new IllegalArgumentException("Cannot get property " + property + " as it does not exist in " + this.owner); + } else { +@@ -97,24 +99,18 @@ public abstract class StateHolder { + } + + public > Optional getOptionalValue(Property property) { +- Comparable comparable = this.values.get(property); ++ Comparable comparable = this.optimisedTable.get(property); // Paper - optimise state lookup + return comparable == null ? Optional.empty() : Optional.of(property.getValueClass().cast(comparable)); + } + + public , V extends T> S setValue(Property property, V value) { +- Comparable comparable = this.values.get(property); +- if (comparable == null) { +- throw new IllegalArgumentException("Cannot set property " + property + " as it does not exist in " + this.owner); +- } else if (comparable.equals(value)) { +- return (S)this; +- } else { +- S object = this.neighbours.get(property, value); +- if (object == null) { +- throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner + ", it is not an allowed value"); +- } else { +- return object; +- } ++ // Paper start - optimise state lookup ++ final S ret = (S)this.optimisedTable.get(property, value); ++ if (ret == null) { ++ throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner + ", it is not an allowed value"); + } ++ return ret; ++ // Paper end - optimise state lookup + } + + public , V extends T> S trySetValue(Property property, V value) { +@@ -147,7 +143,7 @@ public abstract class StateHolder { + } + } + +- this.neighbours = (Table, Comparable, S>)(table.isEmpty() ? table : ArrayTable.create(table)); ++ this.neighbours = (Table, Comparable, S>)(table.isEmpty() ? table : ArrayTable.create(table)); this.optimisedTable.loadInTable((Table)this.neighbours, this.values); // Paper - optimise state lookup + } + } + +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java +index b63116b333b6e06494091a82588acfb639bddb71..ff5fd91257c4554c523682009efe1db83f53fd5b 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java +@@ -7,6 +7,13 @@ import java.util.Optional; + public class BooleanProperty extends Property { + private final ImmutableSet values = ImmutableSet.of(true, false); + ++ // Paper start - optimise iblockdata state lookup ++ @Override ++ public final int getIdFor(final Boolean value) { ++ return value.booleanValue() ? 1 : 0; ++ } ++ // Paper end - optimise iblockdata state lookup ++ + protected BooleanProperty(String name) { + super(name, Boolean.class); + } +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java +index 3097298fe356df98967cf4bdeaaede69dfe8a441..498c5abe0a9d024d77029719c621c1c8485791f3 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java +@@ -15,6 +15,15 @@ public class EnumProperty & StringRepresentable> extends Prope + private final ImmutableSet values; + private final Map names = Maps.newHashMap(); + ++ // Paper start - optimise iblockdata state lookup ++ private int[] idLookupTable; ++ ++ @Override ++ public final int getIdFor(final T value) { ++ return this.idLookupTable[value.ordinal()]; ++ } ++ // Paper end - optimise iblockdata state lookup ++ + protected EnumProperty(String name, Class type, Collection values) { + super(name, type); + this.values = ImmutableSet.copyOf(values); +@@ -27,6 +36,14 @@ public class EnumProperty & StringRepresentable> extends Prope + + this.names.put(string, enum_); + } ++ // Paper start - optimise BlockState lookup ++ int id = 0; ++ this.idLookupTable = new int[type.getEnumConstants().length]; ++ java.util.Arrays.fill(this.idLookupTable, -1); ++ for (final T value : this.getPossibleValues()) { ++ this.idLookupTable[value.ordinal()] = id++; ++ } ++ // Paper end - optimise BlockState lookup + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java +index 3a850321a4bcc68058483b5fd53e829c425a68af..977504f2641d0133a572b0d5de85d058609343bb 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java +@@ -11,6 +11,16 @@ public class IntegerProperty extends Property { + public final int min; + public final int max; + ++ // Paper start - optimise iblockdata state lookup ++ @Override ++ public final int getIdFor(final Integer value) { ++ final int val = value.intValue(); ++ final int ret = val - this.min; ++ ++ return ret | ((this.max - ret) >> 31); ++ } ++ // Paper end - optimise iblockdata state lookup ++ + protected IntegerProperty(String name, int min, int max) { + super(name, Integer.class); + if (min < 0) { +diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java +index 9055f15af0cae55effa6942913a9d7edf3857e07..b9493e3762410aca8e683c32b5aef187c0bee082 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java ++++ b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java +@@ -24,6 +24,17 @@ public abstract class Property> { + ); + private final Codec> valueCodec = this.codec.xmap(this::value, Property.Value::value); + ++ // Paper start - optimise iblockdata state lookup ++ private static final java.util.concurrent.atomic.AtomicInteger ID_GENERATOR = new java.util.concurrent.atomic.AtomicInteger(); ++ private final int id = ID_GENERATOR.getAndIncrement(); ++ ++ public final int getId() { ++ return this.id; ++ } ++ ++ public abstract int getIdFor(final T value); ++ // Paper end - optimise state lookup ++ + protected Property(String name, Class type) { + this.clazz = type; + this.name = name; diff --git a/patches/server/1012-Optimise-general-POI-access.patch b/patches/server/1012-Optimise-general-POI-access.patch deleted file mode 100644 index 118734c246..0000000000 --- a/patches/server/1012-Optimise-general-POI-access.patch +++ /dev/null @@ -1,1060 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sun, 31 Jan 2021 02:29:24 -0800 -Subject: [PATCH] Optimise general POI access - -There are a couple of problems with mojang's POI code. -Firstly, it's all streams. Unsurprisingly, stacking -streams on top of each other is horrible for performance -and ultimately took up half of a villager's tick! - -Secondly, sometime's the search radius is large and there are -a significant number of poi entries per chunk section. Even -removing streams at this point doesn't help much. The only solution -is to start at the search point and iterate outwards. This -type of approach shows massive gains for portals, simply because -we can avoid sync loading a large area of chunks. I also tested -a massive farm I found in JellySquid's discord, which showed -to benefit significantly simply because the farm had so many -portal blocks that searching through them all was very slow. - -Great care has been taken so that behavior remains identical to -vanilla, however I cannot account for oddball Stream API -implementations, if they even exist (streams can technically -be loose with iteration order in a sorted stream given its -source stream is not tagged with ordered, and mojang does not -tag the source stream as ordered). However in my testing on openjdk -there showed no difference, as expected. - -This patch also specifically optimises other areas of code to -use PoiAccess. For example, some villager AI and portaling code -had to be specifically modified. - -diff --git a/src/main/java/io/papermc/paper/util/PoiAccess.java b/src/main/java/io/papermc/paper/util/PoiAccess.java -new file mode 100644 -index 0000000000000000000000000000000000000000..69be1761b3b5ba7b496c1c10a4db897e6212d671 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/util/PoiAccess.java -@@ -0,0 +1,804 @@ -+package io.papermc.paper.util; -+ -+import com.mojang.datafixers.util.Pair; -+import it.unimi.dsi.fastutil.doubles.Double2ObjectMap; -+import it.unimi.dsi.fastutil.doubles.Double2ObjectRBTreeMap; -+import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; -+import it.unimi.dsi.fastutil.longs.LongOpenHashSet; -+import java.util.function.BiPredicate; -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.Holder; -+import net.minecraft.util.Mth; -+import net.minecraft.world.entity.ai.village.poi.PoiManager; -+import net.minecraft.world.entity.ai.village.poi.PoiRecord; -+import net.minecraft.world.entity.ai.village.poi.PoiSection; -+import net.minecraft.world.entity.ai.village.poi.PoiType; -+import java.util.ArrayList; -+import java.util.HashSet; -+import java.util.Iterator; -+import java.util.List; -+import java.util.Map; -+import java.util.Optional; -+import java.util.Set; -+import java.util.function.Predicate; -+ -+/** -+ * Provides optimised access to POI data. All returned values will be identical to vanilla. -+ */ -+public final class PoiAccess { -+ -+ protected static double clamp(final double val, final double min, final double max) { -+ return (val < min ? min : (val > max ? max : val)); -+ } -+ -+ protected static double getSmallestDistanceSquared(final double boxMinX, final double boxMinY, final double boxMinZ, -+ final double boxMaxX, final double boxMaxY, final double boxMaxZ, -+ -+ final double circleX, final double circleY, final double circleZ) { -+ // is the circle center inside the box? -+ if (circleX >= boxMinX && circleX <= boxMaxX && circleY >= boxMinY && circleY <= boxMaxY && circleZ >= boxMinZ && circleZ <= boxMaxZ) { -+ return 0.0; -+ } -+ -+ final double boxWidthX = (boxMaxX - boxMinX) / 2.0; -+ final double boxWidthY = (boxMaxY - boxMinY) / 2.0; -+ final double boxWidthZ = (boxMaxZ - boxMinZ) / 2.0; -+ -+ final double boxCenterX = (boxMinX + boxMaxX) / 2.0; -+ final double boxCenterY = (boxMinY + boxMaxY) / 2.0; -+ final double boxCenterZ = (boxMinZ + boxMaxZ) / 2.0; -+ -+ double centerDiffX = circleX - boxCenterX; -+ double centerDiffY = circleY - boxCenterY; -+ double centerDiffZ = circleZ - boxCenterZ; -+ -+ centerDiffX = circleX - (clamp(centerDiffX, -boxWidthX, boxWidthX) + boxCenterX); -+ centerDiffY = circleY - (clamp(centerDiffY, -boxWidthY, boxWidthY) + boxCenterY); -+ centerDiffZ = circleZ - (clamp(centerDiffZ, -boxWidthZ, boxWidthZ) + boxCenterZ); -+ -+ return (centerDiffX * centerDiffX) + (centerDiffY * centerDiffY) + (centerDiffZ * centerDiffZ); -+ } -+ -+ -+ // key is: -+ // upper 32 bits: -+ // upper 16 bits: max y section -+ // lower 16 bits: min y section -+ // lower 32 bits: -+ // upper 16 bits: section -+ // lower 16 bits: radius -+ protected static long getKey(final int minSection, final int maxSection, final int section, final int radius) { -+ return ( -+ (maxSection & 0xFFFFL) << (64 - 16) -+ | (minSection & 0xFFFFL) << (64 - 32) -+ | (section & 0xFFFFL) << (64 - 48) -+ | (radius & 0xFFFFL) << (64 - 64) -+ ); -+ } -+ -+ // only includes x/z axis -+ // finds the closest poi data by distance. -+ public static BlockPos findClosestPoiDataPosition(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final PoiRecord ret = findClosestPoiDataRecord( -+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load -+ ); -+ -+ return ret == null ? null : ret.getPos(); -+ } -+ -+ // only includes x/z axis -+ // finds the closest poi data by distance. -+ public static Pair, BlockPos> findClosestPoiDataTypeAndPosition(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final PoiRecord ret = findClosestPoiDataRecord( -+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load -+ ); -+ -+ return ret == null ? null : Pair.of(ret.getPoiType(), ret.getPos()); -+ } -+ -+ // only includes x/z axis -+ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned. -+ public static void findClosestPoiDataPositions(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final Set ret) { -+ final Set positions = new HashSet<>(); -+ // pos predicate is last thing that runs before adding to ret. -+ final Predicate newPredicate = (final BlockPos pos) -> { -+ if (positionPredicate != null && !positionPredicate.test(pos)) { -+ return false; -+ } -+ return positions.add(pos.immutable()); -+ }; -+ -+ final List toConvert = new ArrayList<>(); -+ findClosestPoiDataRecords( -+ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, toConvert -+ ); -+ -+ for (final PoiRecord record : toConvert) { -+ ret.add(record.getPos()); -+ } -+ } -+ -+ // only includes x/z axis -+ // finds the closest poi data by distance. -+ public static PoiRecord findClosestPoiDataRecord(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final List ret = new ArrayList<>(); -+ findClosestPoiDataRecords( -+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret -+ ); -+ return ret.isEmpty() ? null : ret.get(0); -+ } -+ -+ // only includes x/z axis -+ // finds the closest poi data by distance. -+ public static PoiRecord findClosestPoiDataRecord(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final BiPredicate, BlockPos> predicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final List ret = new ArrayList<>(); -+ findClosestPoiDataRecords( -+ poiStorage, villagePlaceType, predicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret -+ ); -+ return ret.isEmpty() ? null : ret.get(0); -+ } -+ -+ // only includes x/z axis -+ // finds the closest poi data by distance. if multiple match the same distance, then they all are returned. -+ public static void findClosestPoiDataRecords(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final List ret) { -+ final BiPredicate, BlockPos> predicate = positionPredicate != null ? (type, pos) -> positionPredicate.test(pos) : null; -+ findClosestPoiDataRecords(poiStorage, villagePlaceType, predicate, sourcePosition, range, maxDistanceSquared, occupancy, load, ret); -+ } -+ -+ public static void findClosestPoiDataRecords(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final BiPredicate, BlockPos> predicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final List ret) { -+ final Predicate occupancyFilter = occupancy.getTest(); -+ -+ final List closestRecords = new ArrayList<>(); -+ double closestDistanceSquared = maxDistanceSquared; -+ -+ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; -+ final int lowerY = WorldUtil.getMinSection(poiStorage.world); -+ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; -+ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; -+ final int upperY = WorldUtil.getMaxSection(poiStorage.world); -+ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; -+ -+ final int centerX = sourcePosition.getX() >> 4; -+ final int centerY = Mth.clamp(sourcePosition.getY() >> 4, lowerY, upperY); -+ final int centerZ = sourcePosition.getZ() >> 4; -+ final long centerKey = CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ); -+ -+ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); -+ final LongOpenHashSet seen = new LongOpenHashSet(); -+ seen.add(centerKey); -+ queue.enqueue(centerKey); -+ -+ while (!queue.isEmpty()) { -+ final long key = queue.dequeueLong(); -+ final int sectionX = CoordinateUtils.getChunkSectionX(key); -+ final int sectionY = CoordinateUtils.getChunkSectionY(key); -+ final int sectionZ = CoordinateUtils.getChunkSectionZ(key); -+ -+ if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) { -+ // out of bound chunk -+ continue; -+ } -+ -+ final double sectionDistanceSquared = getSmallestDistanceSquared( -+ (sectionX << 4) + 0.5, -+ (sectionY << 4) + 0.5, -+ (sectionZ << 4) + 0.5, -+ (sectionX << 4) + 15.5, -+ (sectionY << 4) + 15.5, -+ (sectionZ << 4) + 15.5, -+ (double)sourcePosition.getX(), (double)sourcePosition.getY(), (double)sourcePosition.getZ() -+ ); -+ if (sectionDistanceSquared > closestDistanceSquared) { -+ continue; -+ } -+ -+ // queue all neighbours -+ for (int dz = -1; dz <= 1; ++dz) { -+ for (int dx = -1; dx <= 1; ++dx) { -+ for (int dy = -1; dy <= 1; ++dy) { -+ // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many -+ // values are set. we only care about cardinal neighbours, so, we only care if one value is set -+ if ((dx & 1) + (dy & 1) + (dz & 1) != 1) { -+ continue; -+ } -+ -+ final int neighbourX = sectionX + dx; -+ final int neighbourY = sectionY + dy; -+ final int neighbourZ = sectionZ + dz; -+ -+ final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ); -+ if (seen.add(neighbourKey)) { -+ queue.enqueue(neighbourKey); -+ } -+ } -+ } -+ } -+ -+ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key); -+ -+ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) { -+ continue; -+ } -+ -+ final PoiSection poiSection = poiSectionOptional.get(); -+ -+ final Map, Set> sectionData = poiSection.getData(); -+ if (sectionData.isEmpty()) { -+ continue; -+ } -+ -+ // now we search the section data -+ for (final Map.Entry, Set> entry : sectionData.entrySet()) { -+ if (!villagePlaceType.test(entry.getKey())) { -+ // filter out by poi type -+ continue; -+ } -+ -+ // now we can look at the poi data -+ for (final PoiRecord poiData : entry.getValue()) { -+ if (!occupancyFilter.test(poiData)) { -+ // filter by occupancy -+ continue; -+ } -+ -+ final BlockPos poiPosition = poiData.getPos(); -+ -+ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range -+ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { -+ // out of range for square radius -+ continue; -+ } -+ -+ // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped! -+ final double dataRange = poiPosition.distSqr(sourcePosition); -+ -+ if (dataRange > closestDistanceSquared) { -+ // out of range for distance check -+ continue; -+ } -+ -+ if (predicate != null && !predicate.test(poiData.getPoiType(), poiPosition)) { -+ // filter by position -+ continue; -+ } -+ -+ if (dataRange < closestDistanceSquared) { -+ closestRecords.clear(); -+ closestDistanceSquared = dataRange; -+ } -+ closestRecords.add(poiData); -+ } -+ } -+ } -+ -+ // uh oh! we might have multiple records that match the distance sorting! -+ // we need to re-order our results by the way vanilla would have iterated over them. -+ closestRecords.sort((record1, record2) -> { -+ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section -+ // is fine and should be preserved (this sort is stable so we're good there) -+ // but they iterate sections by x then by z (like the following) -+ // for (int x = -dx; x <= dx; ++x) -+ // for (int z = -dz; z <= dz; ++z) -+ // .... -+ // so we need to reorder such that records with lower chunk z, then lower chunk x come first -+ final BlockPos pos1 = record1.getPos(); -+ final BlockPos pos2 = record2.getPos(); -+ -+ final int cx1 = pos1.getX() >> 4; -+ final int cz1 = pos1.getZ() >> 4; -+ -+ final int cx2 = pos2.getX() >> 4; -+ final int cz2 = pos2.getZ() >> 4; -+ -+ if (cz2 != cz1) { -+ // want smaller z -+ return Integer.compare(cz1, cz2); -+ } -+ -+ if (cx2 != cx1) { -+ // want smaller x -+ return Integer.compare(cx1, cx2); -+ } -+ -+ // same chunk -+ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y -+ // so now we just compare section y, wanting smaller y -+ -+ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4); -+ }); -+ -+ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully). -+ ret.addAll(closestRecords); -+ } -+ -+ // finds the closest poi entry pos. -+ public static BlockPos findNearestPoiPosition(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final PoiRecord ret = findNearestPoiRecord( -+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load -+ ); -+ return ret == null ? null : ret.getPos(); -+ } -+ -+ // finds the closest `max` poi entry positions. -+ public static void findNearestPoiPositions(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final int max, -+ final List, BlockPos>> ret) { -+ final Set positions = new HashSet<>(); -+ // pos predicate is last thing that runs before adding to ret. -+ final Predicate newPredicate = (final BlockPos pos) -> { -+ if (positionPredicate != null && !positionPredicate.test(pos)) { -+ return false; -+ } -+ return positions.add(pos.immutable()); -+ }; -+ -+ final List toConvert = new ArrayList<>(); -+ findNearestPoiRecords( -+ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, max, toConvert -+ ); -+ -+ for (final PoiRecord record : toConvert) { -+ ret.add(Pair.of(record.getPoiType(), record.getPos())); -+ } -+ } -+ -+ // finds the closest poi entry. -+ public static PoiRecord findNearestPoiRecord(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final List ret = new ArrayList<>(); -+ findNearestPoiRecords( -+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, maxDistanceSquared, occupancy, load, -+ 1, ret -+ ); -+ return ret.isEmpty() ? null : ret.get(0); -+ } -+ -+ // finds the closest `max` poi entries. -+ public static void findNearestPoiRecords(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ // position predicate must not modify chunk POI -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final double maxDistanceSquared, -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final int max, -+ final List ret) { -+ final Predicate occupancyFilter = occupancy.getTest(); -+ -+ final Double2ObjectRBTreeMap> closestRecords = new Double2ObjectRBTreeMap<>(); -+ int totalRecords = 0; -+ double furthestDistanceSquared = maxDistanceSquared; -+ -+ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; -+ final int lowerY = WorldUtil.getMinSection(poiStorage.world); -+ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; -+ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; -+ final int upperY = WorldUtil.getMaxSection(poiStorage.world); -+ final int upperZ = Mth.floor(sourcePosition.getZ() + range) >> 4; -+ -+ final int centerX = sourcePosition.getX() >> 4; -+ final int centerY = Mth.clamp(sourcePosition.getY() >> 4, lowerY, upperY); -+ final int centerZ = sourcePosition.getZ() >> 4; -+ final long centerKey = CoordinateUtils.getChunkSectionKey(centerX, centerY, centerZ); -+ -+ final LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); -+ final LongOpenHashSet seen = new LongOpenHashSet(); -+ seen.add(centerKey); -+ queue.enqueue(centerKey); -+ -+ while (!queue.isEmpty()) { -+ final long key = queue.dequeueLong(); -+ final int sectionX = CoordinateUtils.getChunkSectionX(key); -+ final int sectionY = CoordinateUtils.getChunkSectionY(key); -+ final int sectionZ = CoordinateUtils.getChunkSectionZ(key); -+ -+ if (sectionX < lowerX || sectionX > upperX || sectionY < lowerY || sectionY > upperY || sectionZ < lowerZ || sectionZ > upperZ) { -+ // out of bound chunk -+ continue; -+ } -+ -+ final double sectionDistanceSquared = getSmallestDistanceSquared( -+ (sectionX << 4) + 0.5, -+ (sectionY << 4) + 0.5, -+ (sectionZ << 4) + 0.5, -+ (sectionX << 4) + 15.5, -+ (sectionY << 4) + 15.5, -+ (sectionZ << 4) + 15.5, -+ (double) sourcePosition.getX(), (double) sourcePosition.getY(), (double) sourcePosition.getZ() -+ ); -+ -+ if (sectionDistanceSquared > (totalRecords >= max ? furthestDistanceSquared : maxDistanceSquared)) { -+ continue; -+ } -+ -+ // queue all neighbours -+ for (int dz = -1; dz <= 1; ++dz) { -+ for (int dx = -1; dx <= 1; ++dx) { -+ for (int dy = -1; dy <= 1; ++dy) { -+ // -1 and 1 have the 1st bit set. so just add up the first bits, and it will tell us how many -+ // values are set. we only care about cardinal neighbours, so, we only care if one value is set -+ if ((dx & 1) + (dy & 1) + (dz & 1) != 1) { -+ continue; -+ } -+ -+ final int neighbourX = sectionX + dx; -+ final int neighbourY = sectionY + dy; -+ final int neighbourZ = sectionZ + dz; -+ -+ final long neighbourKey = CoordinateUtils.getChunkSectionKey(neighbourX, neighbourY, neighbourZ); -+ if (seen.add(neighbourKey)) { -+ queue.enqueue(neighbourKey); -+ } -+ } -+ } -+ } -+ -+ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(key) : poiStorage.get(key); -+ -+ if (poiSectionOptional == null || !poiSectionOptional.isPresent()) { -+ continue; -+ } -+ -+ final PoiSection poiSection = poiSectionOptional.get(); -+ -+ final Map, Set> sectionData = poiSection.getData(); -+ if (sectionData.isEmpty()) { -+ continue; -+ } -+ -+ // now we search the section data -+ for (final Map.Entry, Set> entry : sectionData.entrySet()) { -+ if (!villagePlaceType.test(entry.getKey())) { -+ // filter out by poi type -+ continue; -+ } -+ -+ // now we can look at the poi data -+ for (final PoiRecord poiData : entry.getValue()) { -+ if (!occupancyFilter.test(poiData)) { -+ // filter by occupancy -+ continue; -+ } -+ -+ final BlockPos poiPosition = poiData.getPos(); -+ -+ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range -+ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { -+ // out of range for square radius -+ continue; -+ } -+ -+ // it's important that it's poiPosition.distSqr(source) : the value actually is different IF the values are swapped! -+ final double dataRange = poiPosition.distSqr(sourcePosition); -+ -+ if (dataRange > maxDistanceSquared) { -+ // out of range for distance check -+ continue; -+ } -+ -+ if (dataRange > furthestDistanceSquared && totalRecords >= max) { -+ // out of range for distance check -+ continue; -+ } -+ -+ if (positionPredicate != null && !positionPredicate.test(poiPosition)) { -+ // filter by position -+ continue; -+ } -+ -+ if (dataRange > furthestDistanceSquared) { -+ // we know totalRecords < max, so this entry is now our furthest -+ furthestDistanceSquared = dataRange; -+ } -+ -+ closestRecords.computeIfAbsent(dataRange, (final double unused) -> { -+ return new ArrayList<>(); -+ }).add(poiData); -+ -+ if (++totalRecords >= max) { -+ if (closestRecords.size() >= 2) { -+ int entriesInClosest = 0; -+ final Iterator>> iterator = closestRecords.double2ObjectEntrySet().iterator(); -+ double nextFurthestDistanceSquared = 0.0; -+ -+ for (int i = 0, len = closestRecords.size() - 1; i < len; ++i) { -+ final Double2ObjectMap.Entry> recordEntry = iterator.next(); -+ entriesInClosest += recordEntry.getValue().size(); -+ nextFurthestDistanceSquared = recordEntry.getDoubleKey(); -+ } -+ -+ if (entriesInClosest >= max) { -+ // the last set of entries at range wont even be considered for sure... nuke em -+ final Double2ObjectMap.Entry> recordEntry = iterator.next(); -+ totalRecords -= recordEntry.getValue().size(); -+ iterator.remove(); -+ -+ furthestDistanceSquared = nextFurthestDistanceSquared; -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ final List closestRecordsUnsorted = new ArrayList<>(); -+ -+ // we're done here, so now just flatten the map and sort it. -+ -+ for (final List records : closestRecords.values()) { -+ closestRecordsUnsorted.addAll(records); -+ } -+ -+ // uh oh! we might have multiple records that match the distance sorting! -+ // we need to re-order our results by the way vanilla would have iterated over them. -+ closestRecordsUnsorted.sort((record1, record2) -> { -+ // vanilla iterates the same way we do for data inside sections, so we know the ordering inside a section -+ // is fine and should be preserved (this sort is stable so we're good there) -+ // but they iterate sections by x then by z (like the following) -+ // for (int x = -dx; x <= dx; ++x) -+ // for (int z = -dz; z <= dz; ++z) -+ // .... -+ // so we need to reorder such that records with lower chunk z, then lower chunk x come first -+ final BlockPos pos1 = record1.getPos(); -+ final BlockPos pos2 = record2.getPos(); -+ -+ final int cx1 = pos1.getX() >> 4; -+ final int cz1 = pos1.getZ() >> 4; -+ -+ final int cx2 = pos2.getX() >> 4; -+ final int cz2 = pos2.getZ() >> 4; -+ -+ if (cz2 != cz1) { -+ // want smaller z -+ return Integer.compare(cz1, cz2); -+ } -+ -+ if (cx2 != cx1) { -+ // want smaller x -+ return Integer.compare(cx1, cx2); -+ } -+ -+ // same chunk -+ // once vanilla has the chunk, it will iterate from all of the chunk sections starting from smaller y -+ // so now we just compare section y, wanting smaller section y -+ -+ return Integer.compare(pos1.getY() >> 4, pos2.getY() >> 4); -+ }); -+ -+ // trim out any entries exceeding our maximum -+ for (int i = closestRecordsUnsorted.size() - 1; i >= max; --i) { -+ closestRecordsUnsorted.remove(i); -+ } -+ -+ // now we match perfectly what vanilla would have outputted, without having to search the whole radius (hopefully). -+ ret.addAll(closestRecordsUnsorted); -+ } -+ -+ public static BlockPos findAnyPoiPosition(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final PoiRecord ret = findAnyPoiRecord( -+ poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load -+ ); -+ -+ return ret == null ? null : ret.getPos(); -+ } -+ -+ public static void findAnyPoiPositions(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final int max, -+ final List, BlockPos>> ret) { -+ final Set positions = new HashSet<>(); -+ // pos predicate is last thing that runs before adding to ret. -+ final Predicate newPredicate = (final BlockPos pos) -> { -+ if (positionPredicate != null && !positionPredicate.test(pos)) { -+ return false; -+ } -+ return positions.add(pos.immutable()); -+ }; -+ -+ final List toConvert = new ArrayList<>(); -+ findAnyPoiRecords( -+ poiStorage, villagePlaceType, newPredicate, sourcePosition, range, occupancy, load, max, toConvert -+ ); -+ -+ for (final PoiRecord record : toConvert) { -+ ret.add(Pair.of(record.getPoiType(), record.getPos())); -+ } -+ } -+ -+ public static PoiRecord findAnyPoiRecord(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final PoiManager.Occupancy occupancy, -+ final boolean load) { -+ final List ret = new ArrayList<>(); -+ findAnyPoiRecords(poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, load, 1, ret); -+ return ret.isEmpty() ? null : ret.get(0); -+ } -+ -+ public static void findAnyPoiRecords(final PoiManager poiStorage, -+ final Predicate> villagePlaceType, -+ final Predicate positionPredicate, -+ final BlockPos sourcePosition, -+ final int range, // distance on x y z axis -+ final PoiManager.Occupancy occupancy, -+ final boolean load, -+ final int max, -+ final List 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 -+ final Predicate occupancyFilter = occupancy.getTest(); -+ final double rangeSquared = range * range; -+ -+ int added = 0; -+ -+ // First up, we need to iterate the chunks -+ // all the values here are in chunk sections -+ final int lowerX = Mth.floor(sourcePosition.getX() - range) >> 4; -+ final int lowerY = Math.max(WorldUtil.getMinSection(poiStorage.world), Mth.floor(sourcePosition.getY() - range) >> 4); -+ final int lowerZ = Mth.floor(sourcePosition.getZ() - range) >> 4; -+ final int upperX = Mth.floor(sourcePosition.getX() + range) >> 4; -+ final int upperY = Math.min(WorldUtil.getMaxSection(poiStorage.world), Mth.floor(sourcePosition.getY() + range) >> 4); -+ final int upperZ = Mth.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 -+ final Optional poiSectionOptional = load ? poiStorage.getOrLoad(CoordinateUtils.getChunkSectionKey(currX, currY, currZ)) : -+ poiStorage.get(CoordinateUtils.getChunkSectionKey(currX, currY, currZ)); -+ final PoiSection poiSection = poiSectionOptional == null ? null : poiSectionOptional.orElse(null); -+ if (poiSection == null) { -+ continue; -+ } -+ -+ final Map, Set> sectionData = poiSection.getData(); -+ if (sectionData.isEmpty()) { -+ continue; -+ } -+ -+ // now we search the section data -+ for (final Map.Entry, Set> entry : sectionData.entrySet()) { -+ if (!villagePlaceType.test(entry.getKey())) { -+ // filter out by poi type -+ continue; -+ } -+ -+ // now we can look at the poi data -+ for (final PoiRecord poiData : entry.getValue()) { -+ if (!occupancyFilter.test(poiData)) { -+ // filter by occupancy -+ continue; -+ } -+ -+ final BlockPos poiPosition = poiData.getPos(); -+ -+ if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range -+ || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { -+ // out of range for square radius -+ continue; -+ } -+ -+ if (poiPosition.distSqr(sourcePosition) > rangeSquared) { -+ // out of range for distance check -+ continue; -+ } -+ -+ if (positionPredicate != null && !positionPredicate.test(poiPosition)) { -+ // filter by position -+ continue; -+ } -+ -+ // found one! -+ ret.add(poiData); -+ if (++added >= max) { -+ return; -+ } -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ private PoiAccess() { -+ throw new RuntimeException(); -+ } -+} -diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java -index e8aa27547e3fa1a42720889c7038d4fb0273e7b5..e1b6fe9ecda25f86431baf414f1bfd3a26a8b2bd 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java -+++ b/src/main/java/net/minecraft/world/entity/ai/behavior/AcquirePoi.java -@@ -71,11 +71,11 @@ public class AcquirePoi { - return true; - } - }; -- Set, BlockPos>> set = poiManager.findAllClosestFirstWithType( -- poiPredicate, predicate2, entity.blockPosition(), 48, PoiManager.Occupancy.HAS_SPACE -- ) -- .limit(5L) -- .collect(Collectors.toSet()); -+ // Paper start - optimise POI access -+ java.util.List, BlockPos>> poiposes = new java.util.ArrayList<>(); -+ io.papermc.paper.util.PoiAccess.findNearestPoiPositions(poiManager, poiPredicate, predicate2, entity.blockPosition(), 48, 48*48, PoiManager.Occupancy.HAS_SPACE, false, 5, poiposes); -+ Set, BlockPos>> set = new java.util.HashSet<>(poiposes); -+ // Paper end - optimise POI access - Path path = findPathToPois(entity, set); - if (path != null && path.canReach()) { - BlockPos blockPos = path.getTarget(); -diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java -index d5a549f08b98c80a5cf0eef02cb8a389c32dfecb..92731b6b593289e9f583c9b705b219e81fcd8e73 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java -+++ b/src/main/java/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java -@@ -53,11 +53,12 @@ public class NearestBedSensor extends Sensor { - return true; - } - }; -- Set, BlockPos>> set = poiManager.findAllWithType( -- holder -> holder.is(PoiTypes.HOME), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY -- ) -- .collect(Collectors.toSet()); -- Path path = AcquirePoi.findPathToPois(entity, set); -+ // Paper start - optimise POI access -+ java.util.List, BlockPos>> poiposes = new java.util.ArrayList<>(); -+ // don't ask me why it's unbounded. ask mojang. -+ io.papermc.paper.util.PoiAccess.findAnyPoiPositions(poiManager, type -> type.is(PoiTypes.HOME), predicate, entity.blockPosition(), 48, PoiManager.Occupancy.ANY, false, Integer.MAX_VALUE, poiposes); -+ Path path = AcquirePoi.findPathToPois(entity, new java.util.HashSet<>(poiposes)); -+ // Paper end - optimise POI access - if (path != null && path.canReach()) { - BlockPos blockPos = path.getTarget(); - Optional> optional = poiManager.getType(blockPos); -diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java -index 7b52b0507cbda76aee1db954641f397bef51f94d..c6f193339fdcbcc938d4eafdcad0b112cf1698d5 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java -+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java -@@ -138,36 +138,45 @@ public class PoiManager extends SectionStorage { - public Optional find( - Predicate> typePredicate, Predicate posPredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus - ) { -- return this.findAll(typePredicate, posPredicate, pos, radius, occupationStatus).findFirst(); -+ // Paper start - re-route to faster logic -+ BlockPos ret = io.papermc.paper.util.PoiAccess.findAnyPoiPosition(this, typePredicate, posPredicate, pos, radius, occupationStatus, false); -+ return Optional.ofNullable(ret); -+ // Paper end - } - - public Optional findClosest(Predicate> typePredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus) { -- return this.getInRange(typePredicate, pos, radius, occupationStatus) -- .map(PoiRecord::getPos) -- .min(Comparator.comparingDouble(blockPos2 -> blockPos2.distSqr(pos))); -+ // Paper start - re-route to faster logic -+ BlockPos ret = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, null, pos, radius, radius * radius, occupationStatus, false); -+ return Optional.ofNullable(ret); -+ // Paper end - re-route to faster logic - } - - public Optional, BlockPos>> findClosestWithType( - Predicate> typePredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus - ) { -- return this.getInRange(typePredicate, pos, radius, occupationStatus) -- .min(Comparator.comparingDouble(poi -> poi.getPos().distSqr(pos))) -- .map(poi -> Pair.of(poi.getPoiType(), poi.getPos())); -+ // Paper start - re-route to faster logic -+ return Optional.ofNullable(io.papermc.paper.util.PoiAccess.findClosestPoiDataTypeAndPosition( -+ this, typePredicate, null, pos, radius, radius * radius, occupationStatus, false -+ )); -+ // Paper end - re-route to faster logic - } - - public Optional findClosest( - Predicate> typePredicate, Predicate posPredicate, BlockPos pos, int radius, PoiManager.Occupancy occupationStatus - ) { -- return this.getInRange(typePredicate, pos, radius, occupationStatus) -- .map(PoiRecord::getPos) -- .filter(posPredicate) -- .min(Comparator.comparingDouble(blockPos2 -> blockPos2.distSqr(pos))); -+ // Paper start - re-route to faster logic -+ BlockPos ret = io.papermc.paper.util.PoiAccess.findClosestPoiDataPosition(this, typePredicate, posPredicate, pos, radius, radius * radius, occupationStatus, false); -+ return Optional.ofNullable(ret); -+ // Paper end - re-route to faster logic - } - - public Optional take(Predicate> typePredicate, BiPredicate, BlockPos> biPredicate, BlockPos pos, int radius) { -- return this.getInRange(typePredicate, pos, radius, PoiManager.Occupancy.HAS_SPACE) -- .filter(poi -> biPredicate.test(poi.getPoiType(), poi.getPos())) -- .findFirst() -+ // Paper start - re-route to faster logic -+ final @javax.annotation.Nullable PoiRecord closest = io.papermc.paper.util.PoiAccess.findClosestPoiDataRecord( -+ this, typePredicate, biPredicate, pos, radius, radius * radius, Occupancy.HAS_SPACE, false -+ ); -+ return Optional.ofNullable(closest) -+ // Paper end - re-route to faster logic - .map(poi -> { - poi.acquireTicket(); - return poi.getPos(); -@@ -182,8 +191,21 @@ public class PoiManager extends SectionStorage { - int radius, - RandomSource random - ) { -- List list = Util.toShuffledList(this.getInRange(typePredicate, pos, radius, occupationStatus), random); -- return list.stream().filter(poi -> positionPredicate.test(poi.getPos())).findFirst().map(PoiRecord::getPos); -+ // Paper start - re-route to faster logic -+ List list = new java.util.ArrayList<>(); -+ io.papermc.paper.util.PoiAccess.findAnyPoiRecords( -+ this, typePredicate, positionPredicate, pos, radius, occupationStatus, false, Integer.MAX_VALUE, list -+ ); -+ -+ // the old method shuffled the list and then tried to find the first element in it that -+ // matched positionPredicate, however we moved positionPredicate into the poi search. This means we can avoid a -+ // shuffle entirely, and just pick a random element from list -+ if (list.isEmpty()) { -+ return Optional.empty(); -+ } -+ -+ return Optional.of(list.get(random.nextInt(list.size())).getPos()); -+ // Paper end - re-route to faster logic - } - - public boolean release(BlockPos pos) { -diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java -index 5b7deae326228e482b218aeebd857a59b7434eaf..4ee7d75c56d9f9ff3607276857dde84410ba3f2a 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java -+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java -@@ -26,7 +26,7 @@ import org.slf4j.Logger; - public class PoiSection { - private static final Logger LOGGER = LogUtils.getLogger(); - private final Short2ObjectMap records = new Short2ObjectOpenHashMap<>(); -- private final Map, Set> byType = Maps.newHashMap(); -+ private final Map, Set> byType = Maps.newHashMap(); public final Map, Set> getData() { return this.byType; } // Paper - public accessor - private final Runnable setDirty; - private boolean isValid; - public final Optional noAllocateOptional = Optional.of(this); // Paper - rewrite chunk system -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java -index 883fbe5c81e3be27007a1a0489f80ba1863e5a04..a4a919d8373f1535e336de7e648d41a07efb1cba 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java -@@ -74,11 +74,11 @@ public class SectionStorage extends RegionFileStorage implements AutoCloseabl - } - - @Nullable -- protected Optional get(long pos) { -+ public Optional get(long pos) { // Paper - public - return this.storage.get(pos); - } - -- protected Optional getOrLoad(long pos) { -+ public Optional getOrLoad(long pos) { // Paper - public - if (this.outsideStoredRange(pos)) { - return Optional.empty(); - } else { -diff --git a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java -index a61959700d5e00739a79eaa617ac383160335f26..2407d93c11b806701fc7d192f39d535128281e80 100644 ---- a/src/main/java/net/minecraft/world/level/portal/PortalForcer.java -+++ b/src/main/java/net/minecraft/world/level/portal/PortalForcer.java -@@ -51,18 +51,39 @@ public class PortalForcer { - // int i = flag ? 16 : 128; - // CraftBukkit end - -- villageplace.ensureLoadedAndValid(this.level, blockposition, i); -- Optional optional = villageplace.getInSquare((holder) -> { -- return holder.is(PoiTypes.NETHER_PORTAL); -- }, blockposition, i, PoiManager.Occupancy.ANY).filter((villageplacerecord) -> { -- return worldborder.isWithinBounds(villageplacerecord.getPos()) && !(this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> villageplacerecord.getPos().getY() >= v)); // Paper - Configurable nether ceiling damage -- }).sorted(Comparator.comparingDouble((PoiRecord villageplacerecord) -> { // CraftBukkit - decompile error -- return villageplacerecord.getPos().distSqr(blockposition); -- }).thenComparingInt((villageplacerecord) -> { -- return villageplacerecord.getPos().getY(); -- })).filter((villageplacerecord) -> { -- return this.level.getBlockState(villageplacerecord.getPos()).hasProperty(BlockStateProperties.HORIZONTAL_AXIS); -- }).findFirst(); -+ // Paper start - optimise portals -+ Optional optional; -+ java.util.List records = new java.util.ArrayList<>(); -+ io.papermc.paper.util.PoiAccess.findClosestPoiDataRecords( -+ villageplace, -+ type -> type.is(PoiTypes.NETHER_PORTAL), -+ (BlockPos pos) -> { -+ net.minecraft.world.level.chunk.ChunkAccess lowest = this.level.getChunk(pos.getX() >> 4, pos.getZ() >> 4, net.minecraft.world.level.chunk.status.ChunkStatus.EMPTY); -+ if (!lowest.getStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.FULL) -+ && (lowest.getBelowZeroRetrogen() == null || !lowest.getBelowZeroRetrogen().targetStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.SPAWN))) { -+ // why would we generate the chunk? -+ return false; -+ } -+ if (!worldborder.isWithinBounds(pos) || (this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> pos.getY() >= v))) { // Paper - Configurable nether ceiling damage -+ return false; -+ } -+ return lowest.getBlockState(pos).hasProperty(BlockStateProperties.HORIZONTAL_AXIS); -+ }, -+ blockposition, i, Double.MAX_VALUE, PoiManager.Occupancy.ANY, true, records -+ ); -+ -+ // this gets us most of the way there, but we bias towards lower y values. -+ PoiRecord lowestYRecord = null; -+ for (PoiRecord record : records) { -+ if (lowestYRecord == null) { -+ lowestYRecord = record; -+ } else if (lowestYRecord.getPos().getY() > record.getPos().getY()) { -+ lowestYRecord = record; -+ } -+ } -+ // now we're done -+ optional = Optional.ofNullable(lowestYRecord); -+ // Paper end - optimise portals - - return optional.map((villageplacerecord) -> { - BlockPos blockposition1 = villageplacerecord.getPos(); diff --git a/patches/server/1013-Custom-table-implementation-for-blockstate-state-loo.patch b/patches/server/1013-Custom-table-implementation-for-blockstate-state-loo.patch deleted file mode 100644 index 96d90abea3..0000000000 --- a/patches/server/1013-Custom-table-implementation-for-blockstate-state-loo.patch +++ /dev/null @@ -1,343 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Thu, 11 Mar 2021 20:05:44 -0800 -Subject: [PATCH] Custom table implementation for blockstate state lookups - -Testing some redstone intensive machines showed to bring about a 10% -improvement. - -diff --git a/src/main/java/io/papermc/paper/util/table/ZeroCollidingReferenceStateTable.java b/src/main/java/io/papermc/paper/util/table/ZeroCollidingReferenceStateTable.java -new file mode 100644 -index 0000000000000000000000000000000000000000..57d0cd3ad6f972e986c72a57f1a6e36003f190c2 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/util/table/ZeroCollidingReferenceStateTable.java -@@ -0,0 +1,160 @@ -+package io.papermc.paper.util.table; -+ -+import com.google.common.collect.Table; -+import net.minecraft.world.level.block.state.StateHolder; -+import net.minecraft.world.level.block.state.properties.Property; -+import java.util.Collection; -+import java.util.HashSet; -+import java.util.Map; -+import java.util.Set; -+ -+public final class ZeroCollidingReferenceStateTable { -+ -+ // upper 32 bits: starting index -+ // lower 32 bits: bitset for contained ids -+ protected final long[] this_index_table; -+ protected final Comparable[] this_table; -+ protected final StateHolder this_state; -+ -+ protected long[] index_table; -+ protected StateHolder[][] value_table; -+ -+ public ZeroCollidingReferenceStateTable(final StateHolder state, final Map, Comparable> this_map) { -+ this.this_state = state; -+ this.this_index_table = this.create_table(this_map.keySet()); -+ -+ int max_id = -1; -+ for (final Property property : this_map.keySet()) { -+ final int id = lookup_vindex(property, this.this_index_table); -+ if (id > max_id) { -+ max_id = id; -+ } -+ } -+ -+ this.this_table = new Comparable[max_id + 1]; -+ for (final Map.Entry, Comparable> entry : this_map.entrySet()) { -+ this.this_table[lookup_vindex(entry.getKey(), this.this_index_table)] = entry.getValue(); -+ } -+ } -+ -+ public void loadInTable(final Table, Comparable, StateHolder> table, -+ final Map, Comparable> this_map) { -+ final Set> combined = new HashSet<>(table.rowKeySet()); -+ combined.addAll(this_map.keySet()); -+ -+ this.index_table = this.create_table(combined); -+ -+ int max_id = -1; -+ for (final Property property : combined) { -+ final int id = lookup_vindex(property, this.index_table); -+ if (id > max_id) { -+ max_id = id; -+ } -+ } -+ -+ this.value_table = new StateHolder[max_id + 1][]; -+ -+ final Map, Map, StateHolder>> map = table.rowMap(); -+ for (final Property property : map.keySet()) { -+ final Map, StateHolder> propertyMap = map.get(property); -+ -+ final int id = lookup_vindex(property, this.index_table); -+ final StateHolder[] states = this.value_table[id] = new StateHolder[property.getPossibleValues().size()]; -+ -+ for (final Map.Entry, StateHolder> entry : propertyMap.entrySet()) { -+ if (entry.getValue() == null) { -+ // TODO what -+ continue; -+ } -+ -+ states[((Property)property).getIdFor(entry.getKey())] = entry.getValue(); -+ } -+ } -+ -+ -+ for (final Map.Entry, Comparable> entry : this_map.entrySet()) { -+ final Property property = entry.getKey(); -+ final int index = lookup_vindex(property, this.index_table); -+ -+ if (this.value_table[index] == null) { -+ this.value_table[index] = new StateHolder[property.getPossibleValues().size()]; -+ } -+ -+ this.value_table[index][((Property)property).getIdFor(entry.getValue())] = this.this_state; -+ } -+ } -+ -+ -+ protected long[] create_table(final Collection> collection) { -+ int max_id = -1; -+ for (final Property property : collection) { -+ final int id = property.getId(); -+ if (id > max_id) { -+ max_id = id; -+ } -+ } -+ -+ final long[] ret = new long[((max_id + 1) + 31) >>> 5]; // ceil((max_id + 1) / 32) -+ -+ for (final Property property : collection) { -+ final int id = property.getId(); -+ -+ ret[id >>> 5] |= (1L << (id & 31)); -+ } -+ -+ int total = 0; -+ for (int i = 1, len = ret.length; i < len; ++i) { -+ ret[i] |= (long)(total += Long.bitCount(ret[i - 1] & 0xFFFFFFFFL)) << 32; -+ } -+ -+ return ret; -+ } -+ -+ public Comparable get(final Property state) { -+ final Comparable[] table = this.this_table; -+ final int index = lookup_vindex(state, this.this_index_table); -+ -+ if (index < 0 || index >= table.length) { -+ return null; -+ } -+ return table[index]; -+ } -+ -+ public StateHolder get(final Property property, final Comparable with) { -+ final int withId = ((Property)property).getIdFor(with); -+ if (withId < 0) { -+ return null; -+ } -+ -+ final int index = lookup_vindex(property, this.index_table); -+ final StateHolder[][] table = this.value_table; -+ if (index < 0 || index >= table.length) { -+ return null; -+ } -+ -+ final StateHolder[] values = table[index]; -+ -+ if (withId >= values.length) { -+ return null; -+ } -+ -+ return values[withId]; -+ } -+ -+ protected static int lookup_vindex(final Property property, final long[] index_table) { -+ final int id = property.getId(); -+ final long bitset_mask = (1L << (id & 31)); -+ final long lower_mask = bitset_mask - 1; -+ final int index = id >>> 5; -+ if (index >= index_table.length) { -+ return -1; -+ } -+ final long index_value = index_table[index]; -+ final long contains_check = ((index_value & bitset_mask) - 1) >> (Long.SIZE - 1); // -1L if doesn't contain -+ -+ // index = total bits set in lower table values (upper 32 bits of index_value) plus total bits set in lower indices below id -+ // contains_check is 0 if the bitset had id set, else it's -1: so index is unaffected if contains_check == 0, -+ // otherwise it comes out as -1. -+ return (int)(((index_value >>> 32) + Long.bitCount(index_value & lower_mask)) | contains_check); -+ } -+} -diff --git a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java -index daedcfd867ed6171fb61bdcbded417a11c8a5b0f..45744d86e9582a93a0cec26009deea091080fbbe 100644 ---- a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java -+++ b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java -@@ -39,11 +39,13 @@ public abstract class StateHolder { - private final Reference2ObjectArrayMap, Comparable> values; - private Table, Comparable, S> neighbours; - protected final MapCodec propertiesCodec; -+ protected final io.papermc.paper.util.table.ZeroCollidingReferenceStateTable optimisedTable; // Paper - optimise state lookup - - protected StateHolder(O owner, Reference2ObjectArrayMap, Comparable> propertyMap, MapCodec codec) { - this.owner = owner; - this.values = propertyMap; - this.propertiesCodec = codec; -+ this.optimisedTable = new io.papermc.paper.util.table.ZeroCollidingReferenceStateTable(this, propertyMap); // Paper - optimise state lookup - } - - public > S cycle(Property property) { -@@ -84,11 +86,11 @@ public abstract class StateHolder { - } - - public > boolean hasProperty(Property property) { -- return this.values.containsKey(property); -+ return this.optimisedTable.get(property) != null; // Paper - optimise state lookup - } - - public > T getValue(Property property) { -- Comparable comparable = this.values.get(property); -+ Comparable comparable = this.optimisedTable.get(property); // Paper - optimise state lookup - if (comparable == null) { - throw new IllegalArgumentException("Cannot get property " + property + " as it does not exist in " + this.owner); - } else { -@@ -97,24 +99,18 @@ public abstract class StateHolder { - } - - public > Optional getOptionalValue(Property property) { -- Comparable comparable = this.values.get(property); -+ Comparable comparable = this.optimisedTable.get(property); // Paper - optimise state lookup - return comparable == null ? Optional.empty() : Optional.of(property.getValueClass().cast(comparable)); - } - - public , V extends T> S setValue(Property property, V value) { -- Comparable comparable = this.values.get(property); -- if (comparable == null) { -- throw new IllegalArgumentException("Cannot set property " + property + " as it does not exist in " + this.owner); -- } else if (comparable.equals(value)) { -- return (S)this; -- } else { -- S object = this.neighbours.get(property, value); -- if (object == null) { -- throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner + ", it is not an allowed value"); -- } else { -- return object; -- } -+ // Paper start - optimise state lookup -+ final S ret = (S)this.optimisedTable.get(property, value); -+ if (ret == null) { -+ throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner + ", it is not an allowed value"); - } -+ return ret; -+ // Paper end - optimise state lookup - } - - public , V extends T> S trySetValue(Property property, V value) { -@@ -147,7 +143,7 @@ public abstract class StateHolder { - } - } - -- this.neighbours = (Table, Comparable, S>)(table.isEmpty() ? table : ArrayTable.create(table)); -+ this.neighbours = (Table, Comparable, S>)(table.isEmpty() ? table : ArrayTable.create(table)); this.optimisedTable.loadInTable((Table)this.neighbours, this.values); // Paper - optimise state lookup - } - } - -diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java -index b63116b333b6e06494091a82588acfb639bddb71..ff5fd91257c4554c523682009efe1db83f53fd5b 100644 ---- a/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java -+++ b/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java -@@ -7,6 +7,13 @@ import java.util.Optional; - public class BooleanProperty extends Property { - private final ImmutableSet values = ImmutableSet.of(true, false); - -+ // Paper start - optimise iblockdata state lookup -+ @Override -+ public final int getIdFor(final Boolean value) { -+ return value.booleanValue() ? 1 : 0; -+ } -+ // Paper end - optimise iblockdata state lookup -+ - protected BooleanProperty(String name) { - super(name, Boolean.class); - } -diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java -index 3097298fe356df98967cf4bdeaaede69dfe8a441..498c5abe0a9d024d77029719c621c1c8485791f3 100644 ---- a/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java -+++ b/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java -@@ -15,6 +15,15 @@ public class EnumProperty & StringRepresentable> extends Prope - private final ImmutableSet values; - private final Map names = Maps.newHashMap(); - -+ // Paper start - optimise iblockdata state lookup -+ private int[] idLookupTable; -+ -+ @Override -+ public final int getIdFor(final T value) { -+ return this.idLookupTable[value.ordinal()]; -+ } -+ // Paper end - optimise iblockdata state lookup -+ - protected EnumProperty(String name, Class type, Collection values) { - super(name, type); - this.values = ImmutableSet.copyOf(values); -@@ -27,6 +36,14 @@ public class EnumProperty & StringRepresentable> extends Prope - - this.names.put(string, enum_); - } -+ // Paper start - optimise BlockState lookup -+ int id = 0; -+ this.idLookupTable = new int[type.getEnumConstants().length]; -+ java.util.Arrays.fill(this.idLookupTable, -1); -+ for (final T value : this.getPossibleValues()) { -+ this.idLookupTable[value.ordinal()] = id++; -+ } -+ // Paper end - optimise BlockState lookup - } - - @Override -diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java -index 3a850321a4bcc68058483b5fd53e829c425a68af..977504f2641d0133a572b0d5de85d058609343bb 100644 ---- a/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java -+++ b/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java -@@ -11,6 +11,16 @@ public class IntegerProperty extends Property { - public final int min; - public final int max; - -+ // Paper start - optimise iblockdata state lookup -+ @Override -+ public final int getIdFor(final Integer value) { -+ final int val = value.intValue(); -+ final int ret = val - this.min; -+ -+ return ret | ((this.max - ret) >> 31); -+ } -+ // Paper end - optimise iblockdata state lookup -+ - protected IntegerProperty(String name, int min, int max) { - super(name, Integer.class); - if (min < 0) { -diff --git a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java -index 9055f15af0cae55effa6942913a9d7edf3857e07..b9493e3762410aca8e683c32b5aef187c0bee082 100644 ---- a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java -+++ b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java -@@ -24,6 +24,17 @@ public abstract class Property> { - ); - private final Codec> valueCodec = this.codec.xmap(this::value, Property.Value::value); - -+ // Paper start - optimise iblockdata state lookup -+ private static final java.util.concurrent.atomic.AtomicInteger ID_GENERATOR = new java.util.concurrent.atomic.AtomicInteger(); -+ private final int id = ID_GENERATOR.getAndIncrement(); -+ -+ public final int getId() { -+ return this.id; -+ } -+ -+ public abstract int getIdFor(final T value); -+ // Paper end - optimise state lookup -+ - protected Property(String name, Class type) { - this.clazz = type; - this.name = name; diff --git a/patches/server/1013-Execute-chunk-tasks-mid-tick.patch b/patches/server/1013-Execute-chunk-tasks-mid-tick.patch new file mode 100644 index 0000000000..bc666c1184 --- /dev/null +++ b/patches/server/1013-Execute-chunk-tasks-mid-tick.patch @@ -0,0 +1,176 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Mon, 6 Apr 2020 04:20:44 -0700 +Subject: [PATCH] Execute chunk tasks mid-tick + +This will help the server load chunks if tick times are high. + +diff --git a/src/main/java/co/aikar/timings/MinecraftTimings.java b/src/main/java/co/aikar/timings/MinecraftTimings.java +index 6b3cde6d4d1e63bec01f502f2027ee9fddac08aa..46449728f69ee7d4f78470f8da23c055acd53a3b 100644 +--- a/src/main/java/co/aikar/timings/MinecraftTimings.java ++++ b/src/main/java/co/aikar/timings/MinecraftTimings.java +@@ -48,6 +48,8 @@ public final class MinecraftTimings { + public static final Timing antiXrayObfuscateTimer = Timings.ofSafe("anti-xray - obfuscate"); + public static final Timing scoreboardScoreSearch = Timings.ofSafe("Scoreboard score search"); // Paper - add timings for scoreboard search + ++ public static final Timing midTickChunkTasks = Timings.ofSafe("Mid Tick Chunk Tasks"); ++ + private static final Map, String> taskNameCache = new MapMaker().weakKeys().makeMap(); + + private MinecraftTimings() {} +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 101c465397176d873f7a7871c84c8fb5a9897b0b..81d0a0c79958a137ac8bc703eb97f30cd10864e3 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -1414,8 +1414,79 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop= 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(); ++ } ++ } ++ // Paper end - execute chunk tasks mid tick ++ + private boolean pollTaskInternal() { + if (super.pollTask()) { ++ this.executeMidTickTasks(); // Paper - execute chunk tasks mid tick + return true; + } else { + boolean ret = false; // Paper - force execution of all worlds, do not just bias the first +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index 0b7a38b9e92b19345a34c6226413a9b133264077..18640c8681f6a3b2276123d19e3e8f0a8c630b41 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -566,6 +566,7 @@ public class ServerChunkCache extends ChunkSource { + boolean flag1 = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getLevelData().getGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit + Iterator iterator1 = list.iterator(); + ++ int chunksTicked = 0; // Paper + while (iterator1.hasNext()) { + ServerChunkCache.ChunkAndHolder chunkproviderserver_a = (ServerChunkCache.ChunkAndHolder) iterator1.next(); + LevelChunk chunk1 = chunkproviderserver_a.chunk; +@@ -579,6 +580,7 @@ public class ServerChunkCache extends ChunkSource { + + if (this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { + this.level.tickChunk(chunk1, l); ++ if ((chunksTicked++ & 1) == 0) net.minecraft.server.MinecraftServer.getServer().executeMidTickTasks(); // Paper + } + } + } +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 4b7cad5f551d620dbd091111df94fb39efcb79ac..9cd54bc390f0629bf6b4494bd9c3a6e0927d1b7b 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -221,6 +221,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + private final StructureCheck structureCheck; + private final boolean tickTime; + private final RandomSequences randomSequences; ++ public long lastMidTickExecuteFailure; // Paper - execute chunk tasks mid tick + + // CraftBukkit start + public final LevelStorageSource.LevelStorageAccess convertable; +@@ -1217,6 +1218,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + if (fluid1.is(fluid)) { + fluid1.tick(this, pos); + } ++ MinecraftServer.getServer().executeMidTickTasks(); // Paper - exec chunk tasks during world tick + + } + +@@ -1226,6 +1228,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + if (iblockdata.is(block)) { + iblockdata.tick(this, pos, this.random); + } ++ MinecraftServer.getServer().executeMidTickTasks(); // Paper - exec chunk tasks during world tick + + } + +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index d3d7abb2d31e8ce9f9c53eca66a83a1c28fec792..dab55ab08665f2b5ae0c899a4ab07c18460552ae 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -915,6 +915,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + // Spigot end + } else if (flag && this.shouldTickBlocksAt(tickingblockentity.getPos())) { + tickingblockentity.tick(); ++ // Paper start - execute chunk tasks during tick ++ if ((this.tileTickPosition & 7) == 0) { ++ MinecraftServer.getServer().executeMidTickTasks(); ++ } ++ // Paper end - execute chunk tasks during tick + } + } + this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075 +@@ -929,6 +934,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public void guardEntityTick(Consumer tickConsumer, T entity) { + try { + tickConsumer.accept(entity); ++ MinecraftServer.getServer().executeMidTickTasks(); // Paper - execute chunk tasks mid tick + } catch (Throwable throwable) { + if (throwable instanceof ThreadDeath) throw throwable; // Paper + // Paper start - Prevent block entity and entity crashes diff --git a/patches/server/1014-Execute-chunk-tasks-mid-tick.patch b/patches/server/1014-Execute-chunk-tasks-mid-tick.patch deleted file mode 100644 index bc666c1184..0000000000 --- a/patches/server/1014-Execute-chunk-tasks-mid-tick.patch +++ /dev/null @@ -1,176 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Mon, 6 Apr 2020 04:20:44 -0700 -Subject: [PATCH] Execute chunk tasks mid-tick - -This will help the server load chunks if tick times are high. - -diff --git a/src/main/java/co/aikar/timings/MinecraftTimings.java b/src/main/java/co/aikar/timings/MinecraftTimings.java -index 6b3cde6d4d1e63bec01f502f2027ee9fddac08aa..46449728f69ee7d4f78470f8da23c055acd53a3b 100644 ---- a/src/main/java/co/aikar/timings/MinecraftTimings.java -+++ b/src/main/java/co/aikar/timings/MinecraftTimings.java -@@ -48,6 +48,8 @@ public final class MinecraftTimings { - public static final Timing antiXrayObfuscateTimer = Timings.ofSafe("anti-xray - obfuscate"); - public static final Timing scoreboardScoreSearch = Timings.ofSafe("Scoreboard score search"); // Paper - add timings for scoreboard search - -+ public static final Timing midTickChunkTasks = Timings.ofSafe("Mid Tick Chunk Tasks"); -+ - private static final Map, String> taskNameCache = new MapMaker().weakKeys().makeMap(); - - private MinecraftTimings() {} -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 101c465397176d873f7a7871c84c8fb5a9897b0b..81d0a0c79958a137ac8bc703eb97f30cd10864e3 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1414,8 +1414,79 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop= 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(); -+ } -+ } -+ // Paper end - execute chunk tasks mid tick -+ - private boolean pollTaskInternal() { - if (super.pollTask()) { -+ this.executeMidTickTasks(); // Paper - execute chunk tasks mid tick - return true; - } else { - boolean ret = false; // Paper - force execution of all worlds, do not just bias the first -diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -index 0b7a38b9e92b19345a34c6226413a9b133264077..18640c8681f6a3b2276123d19e3e8f0a8c630b41 100644 ---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java -+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -@@ -566,6 +566,7 @@ public class ServerChunkCache extends ChunkSource { - boolean flag1 = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getLevelData().getGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit - Iterator iterator1 = list.iterator(); - -+ int chunksTicked = 0; // Paper - while (iterator1.hasNext()) { - ServerChunkCache.ChunkAndHolder chunkproviderserver_a = (ServerChunkCache.ChunkAndHolder) iterator1.next(); - LevelChunk chunk1 = chunkproviderserver_a.chunk; -@@ -579,6 +580,7 @@ public class ServerChunkCache extends ChunkSource { - - if (this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { - this.level.tickChunk(chunk1, l); -+ if ((chunksTicked++ & 1) == 0) net.minecraft.server.MinecraftServer.getServer().executeMidTickTasks(); // Paper - } - } - } -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 4b7cad5f551d620dbd091111df94fb39efcb79ac..9cd54bc390f0629bf6b4494bd9c3a6e0927d1b7b 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -221,6 +221,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - private final StructureCheck structureCheck; - private final boolean tickTime; - private final RandomSequences randomSequences; -+ public long lastMidTickExecuteFailure; // Paper - execute chunk tasks mid tick - - // CraftBukkit start - public final LevelStorageSource.LevelStorageAccess convertable; -@@ -1217,6 +1218,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - if (fluid1.is(fluid)) { - fluid1.tick(this, pos); - } -+ MinecraftServer.getServer().executeMidTickTasks(); // Paper - exec chunk tasks during world tick - - } - -@@ -1226,6 +1228,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - if (iblockdata.is(block)) { - iblockdata.tick(this, pos, this.random); - } -+ MinecraftServer.getServer().executeMidTickTasks(); // Paper - exec chunk tasks during world tick - - } - -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index d3d7abb2d31e8ce9f9c53eca66a83a1c28fec792..dab55ab08665f2b5ae0c899a4ab07c18460552ae 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -915,6 +915,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - // Spigot end - } else if (flag && this.shouldTickBlocksAt(tickingblockentity.getPos())) { - tickingblockentity.tick(); -+ // Paper start - execute chunk tasks during tick -+ if ((this.tileTickPosition & 7) == 0) { -+ MinecraftServer.getServer().executeMidTickTasks(); -+ } -+ // Paper end - execute chunk tasks during tick - } - } - this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075 -@@ -929,6 +934,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - public void guardEntityTick(Consumer tickConsumer, T entity) { - try { - tickConsumer.accept(entity); -+ MinecraftServer.getServer().executeMidTickTasks(); // Paper - execute chunk tasks mid tick - } catch (Throwable throwable) { - if (throwable instanceof ThreadDeath) throw throwable; // Paper - // Paper start - Prevent block entity and entity crashes diff --git a/patches/server/1014-Optimise-random-block-ticking.patch b/patches/server/1014-Optimise-random-block-ticking.patch new file mode 100644 index 0000000000..07b98dffde --- /dev/null +++ b/patches/server/1014-Optimise-random-block-ticking.patch @@ -0,0 +1,456 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sun, 20 Jun 2021 16:19:26 -0700 +Subject: [PATCH] Optimise random block ticking + +Massive performance improvement for random block ticking. +The performance increase comes from the fact that the vast +majority of attempted block ticks (~95% in my testing) fail +because the randomly selected block is not tickable. + +Now only tickable blocks are targeted, however this means that +the maximum number of block ticks occurs per chunk. However, +not all chunks are going to be targeted. The percent chance +of a chunk being targeted is based on how many tickable blocks +are in the chunk. +This means that while block ticks are spread out less, the +total number of blocks ticked per world tick remains the same. +Therefore, the chance of a random tickable block being ticked +remains the same. + +diff --git a/src/main/java/io/papermc/paper/util/math/ThreadUnsafeRandom.java b/src/main/java/io/papermc/paper/util/math/ThreadUnsafeRandom.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7d93652c1abbb6aee6eb7c26cf35d4d032ef7b69 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/math/ThreadUnsafeRandom.java +@@ -0,0 +1,65 @@ ++package io.papermc.paper.util.math; ++ ++import net.minecraft.util.RandomSource; ++import net.minecraft.world.level.levelgen.LegacyRandomSource; ++import net.minecraft.world.level.levelgen.PositionalRandomFactory; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++ ++@DefaultQualifier(NonNull.class) ++public final class ThreadUnsafeRandom extends LegacyRandomSource { ++ ++ // See javadoc and internal comments for java.util.Random where these values come from, how they are used, and the author for them. ++ private static final long multiplier = 0x5DEECE66DL; ++ private static final long addend = 0xBL; ++ private static final long mask = (1L << 48) - 1; ++ ++ private static long initialScramble(long seed) { ++ return (seed ^ multiplier) & mask; ++ } ++ ++ private long seed; ++ ++ public ThreadUnsafeRandom(long seed) { ++ super(seed); ++ } ++ ++ @Override ++ public RandomSource fork() { ++ return new ThreadUnsafeRandom(this.nextLong()); ++ } ++ ++ @Override ++ public PositionalRandomFactory forkPositional() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public void setSeed(long seed) { ++ // note: called by Random constructor ++ this.seed = initialScramble(seed); ++ } ++ ++ @Override ++ public int next(int bits) { ++ // avoid the expensive CAS logic used by superclass ++ return (int) (((this.seed = this.seed * multiplier + addend) & mask) >>> (48 - bits)); ++ } ++ ++ // Taken from ++ // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ ++ // https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog/blob/master/2016/06/25/fastrange.c ++ // Original license is public domain ++ public static int fastRandomBounded(final long randomInteger, final long limit) { ++ // randomInteger must be [0, pow(2, 32)) ++ // limit must be [0, pow(2, 32)) ++ return (int)((randomInteger * limit) >>> 32); ++ } ++ ++ @Override ++ public int nextInt(int bound) { ++ // yes this breaks random's spec ++ // however there's nothing that uses this class that relies on it ++ return fastRandomBounded(this.next(32) & 0xFFFFFFFFL, bound); ++ } ++} +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 9cd54bc390f0629bf6b4494bd9c3a6e0927d1b7b..215f03dfe0e6441588679efaedbcf04c352b404c 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -867,6 +867,10 @@ public class ServerLevel extends Level implements WorldGenLevel { + entityplayer.stopSleepInBed(false, false); + }); + } ++ // Paper start - optimise random block ticking ++ private final BlockPos.MutableBlockPos chunkTickMutablePosition = new BlockPos.MutableBlockPos(); ++ private final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(this.random.nextLong()); ++ // Paper end + + public void tickChunk(LevelChunk chunk, int randomTickSpeed) { + ChunkPos chunkcoordintpair = chunk.getPos(); +@@ -876,8 +880,10 @@ public class ServerLevel extends Level implements WorldGenLevel { + ProfilerFiller gameprofilerfiller = this.getProfiler(); + + gameprofilerfiller.push("thunder"); ++ final BlockPos.MutableBlockPos blockposition = this.chunkTickMutablePosition; // Paper - use mutable to reduce allocation rate, final to force compile fail on change ++ + if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder +- BlockPos blockposition = this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15)); ++ blockposition.set(this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15))); // Paper + + if (this.isRainingAt(blockposition)) { + DifficultyInstance difficultydamagescaler = this.getCurrentDifficultyAt(blockposition); +@@ -909,7 +915,10 @@ public class ServerLevel extends Level implements WorldGenLevel { + if (!this.paperConfig().environment.disableIceAndSnow) { // Paper - Option to disable ice and snow + for (int l = 0; l < randomTickSpeed; ++l) { + if (this.random.nextInt(48) == 0) { +- this.tickPrecipitation(this.getBlockRandomPos(j, 0, k, 15)); ++ // Paper start ++ this.getRandomBlockPosition(j, 0, k, 15, blockposition); ++ this.tickPrecipitation(blockposition, chunk); ++ // Paper end + } + } + } // Paper - Option to disable ice and snow +@@ -917,36 +926,37 @@ public class ServerLevel extends Level implements WorldGenLevel { + gameprofilerfiller.popPush("tickBlocks"); + timings.chunkTicksBlocks.startTiming(); // Paper + if (randomTickSpeed > 0) { +- LevelChunkSection[] achunksection = chunk.getSections(); +- +- for (int i1 = 0; i1 < achunksection.length; ++i1) { +- LevelChunkSection chunksection = achunksection[i1]; +- +- if (chunksection.isRandomlyTicking()) { +- int j1 = chunk.getSectionYFromSectionIndex(i1); +- int k1 = SectionPos.sectionToBlockCoord(j1); +- +- for (int l1 = 0; l1 < randomTickSpeed; ++l1) { +- BlockPos blockposition1 = this.getBlockRandomPos(j, k1, k, 15); +- +- gameprofilerfiller.push("randomTick"); +- BlockState iblockdata = chunksection.getBlockState(blockposition1.getX() - j, blockposition1.getY() - k1, blockposition1.getZ() - k); +- +- if (iblockdata.isRandomlyTicking()) { +- iblockdata.randomTick(this, blockposition1, this.random); +- } ++ // Paper start - optimize random block ticking ++ LevelChunkSection[] sections = chunk.getSections(); ++ final int minSection = io.papermc.paper.util.WorldUtil.getMinSection(this); ++ for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { ++ LevelChunkSection section = sections[sectionIndex]; ++ if (section == null || section.tickingList.size() == 0) continue; ++ ++ int yPos = (sectionIndex + minSection) << 4; ++ for (int a = 0; a < randomTickSpeed; ++a) { ++ int tickingBlocks = section.tickingList.size(); ++ int index = this.randomTickRandom.nextInt(16 * 16 * 16); ++ if (index >= tickingBlocks) { ++ continue; ++ } + +- FluidState fluid = iblockdata.getFluidState(); ++ long raw = section.tickingList.getRaw(index); ++ int location = com.destroystokyo.paper.util.maplist.IBlockDataList.getLocationFromRaw(raw); ++ int randomX = location & 15; ++ int randomY = ((location >>> (4 + 4)) & 255) | yPos; ++ int randomZ = (location >>> 4) & 15; + +- if (fluid.isRandomlyTicking()) { +- fluid.randomTick(this, blockposition1, this.random); +- } ++ BlockPos blockposition2 = blockposition.set(j + randomX, randomY, k + randomZ); ++ BlockState iblockdata = com.destroystokyo.paper.util.maplist.IBlockDataList.getBlockDataFromRaw(raw); + +- gameprofilerfiller.pop(); +- } ++ iblockdata.randomTick(this, blockposition2, this.randomTickRandom); + } ++ // We drop the fluid tick since LAVA is ALREADY TICKED by the above method (See LiquidBlock). ++ // TODO CHECK ON UPDATE (ping the Canadian) + } + } ++ // Paper end - optimise random block ticking + + timings.chunkTicksBlocks.stopTiming(); // Paper + gameprofilerfiller.pop(); +@@ -954,17 +964,25 @@ public class ServerLevel extends Level implements WorldGenLevel { + + @VisibleForTesting + public void tickPrecipitation(BlockPos pos) { +- BlockPos blockposition1 = this.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, pos); +- BlockPos blockposition2 = blockposition1.below(); ++ // Paper start - optimise chunk ticking ++ tickPrecipitation(pos.mutable(), this.getChunkAt(pos)); ++ } ++ public void tickPrecipitation(BlockPos.MutableBlockPos blockposition1, final LevelChunk chunk) { ++ int normalY = chunk.getHeight(Heightmap.Types.MOTION_BLOCKING, blockposition1.getX() & 15, blockposition1.getZ() & 15) + 1; ++ int downY = normalY - 1; ++ blockposition1.setY(normalY); ++ // Paper end - optimise chunk ticking + Biome biomebase = (Biome) this.getBiome(blockposition1).value(); + +- if (biomebase.shouldFreeze(this, blockposition2)) { +- org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition2, Blocks.ICE.defaultBlockState(), null); // CraftBukkit ++ blockposition1.setY(downY); ++ if (biomebase.shouldFreeze(this, blockposition1)) { ++ org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition1, Blocks.ICE.defaultBlockState(), null); // CraftBukkit + } + + if (this.isRaining()) { + int i = this.getGameRules().getInt(GameRules.RULE_SNOW_ACCUMULATION_HEIGHT); + ++ blockposition1.setY(normalY); // Paper - optimise chunk ticking + if (i > 0 && biomebase.shouldSnow(this, blockposition1)) { + BlockState iblockdata = this.getBlockState(blockposition1); + +@@ -982,12 +1000,13 @@ public class ServerLevel extends Level implements WorldGenLevel { + } + } + +- Biome.Precipitation biomebase_precipitation = biomebase.getPrecipitationAt(blockposition2); ++ blockposition1.setY(downY); // Paper - optimise chunk ticking ++ Biome.Precipitation biomebase_precipitation = biomebase.getPrecipitationAt(blockposition1); // Paper - optimise chunk ticking + + if (biomebase_precipitation != Biome.Precipitation.NONE) { +- BlockState iblockdata2 = this.getBlockState(blockposition2); ++ BlockState iblockdata2 = this.getBlockState(blockposition1); // Paper - optimise chunk ticking + +- iblockdata2.getBlock().handlePrecipitation(iblockdata2, this, blockposition2, biomebase_precipitation); ++ iblockdata2.getBlock().handlePrecipitation(iblockdata2, this, blockposition1, biomebase_precipitation); // Paper - optimise chunk ticking + } + } + +diff --git a/src/main/java/net/minecraft/util/BitStorage.java b/src/main/java/net/minecraft/util/BitStorage.java +index 68648c5a5e3ff079f832092af0f2f801c42d1ede..8bafd5fd7499ba4a04bf706cfd1e156073716e21 100644 +--- a/src/main/java/net/minecraft/util/BitStorage.java ++++ b/src/main/java/net/minecraft/util/BitStorage.java +@@ -20,4 +20,15 @@ public interface BitStorage { + void unpack(int[] out); + + BitStorage copy(); ++ ++ // Paper start ++ void forEach(DataBitConsumer consumer); ++ ++ @FunctionalInterface ++ interface DataBitConsumer { ++ ++ void accept(int location, int data); ++ ++ } ++ // Paper end + } +diff --git a/src/main/java/net/minecraft/util/SimpleBitStorage.java b/src/main/java/net/minecraft/util/SimpleBitStorage.java +index 9f438d9c6eb05e43d24e4af68188a3d4c46a938c..8d7d763bf51cac556057645e6169c9447993189b 100644 +--- a/src/main/java/net/minecraft/util/SimpleBitStorage.java ++++ b/src/main/java/net/minecraft/util/SimpleBitStorage.java +@@ -315,6 +315,28 @@ public class SimpleBitStorage implements BitStorage { + return this.bits; + } + ++ // Paper start ++ @Override ++ public final void forEach(DataBitConsumer consumer) { ++ int i = 0; ++ long[] along = this.data; ++ int j = along.length; ++ ++ for (int k = 0; k < j; ++k) { ++ long l = along[k]; ++ ++ for (int i1 = 0; i1 < this.valuesPerLong; ++i1) { ++ consumer.accept(i, (int) (l & this.mask)); ++ l >>= this.bits; ++ ++i; ++ if (i >= this.size) { ++ return; ++ } ++ } ++ } ++ } ++ // Paper end ++ + @Override + public void getAll(IntConsumer action) { + int i = 0; +diff --git a/src/main/java/net/minecraft/util/ZeroBitStorage.java b/src/main/java/net/minecraft/util/ZeroBitStorage.java +index 50040c497a819cd1229042ab3cb057d34a32cacc..01f5b946fabbe34f31110e75973dab9f39897346 100644 +--- a/src/main/java/net/minecraft/util/ZeroBitStorage.java ++++ b/src/main/java/net/minecraft/util/ZeroBitStorage.java +@@ -46,6 +46,15 @@ public class ZeroBitStorage implements BitStorage { + return 0; + } + ++ // Paper start ++ @Override ++ public void forEach(DataBitConsumer consumer) { ++ for(int i = 0; i < this.size; ++i) { ++ consumer.accept(i, 0); ++ } ++ } ++ // Paper end ++ + @Override + public void getAll(IntConsumer action) { + for (int i = 0; i < this.size; i++) { +diff --git a/src/main/java/net/minecraft/world/entity/animal/Turtle.java b/src/main/java/net/minecraft/world/entity/animal/Turtle.java +index ffffa53fcaa6ec8681b283fa20bb5a3320ad9c11..30b87b5cb18c25cdd04eab64cfbe5acd6c1b6d84 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java +@@ -88,7 +88,7 @@ public class Turtle extends Animal { + } + + public void setHomePos(BlockPos pos) { +- this.entityData.set(Turtle.HOME_POS, pos); ++ this.entityData.set(Turtle.HOME_POS, pos.immutable()); // Paper - called with mutablepos... + } + + public BlockPos getHomePos() { +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index dab55ab08665f2b5ae0c899a4ab07c18460552ae..4001bec504b8fcda1eec03f49abd4e4d82cf6336 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -1387,10 +1387,18 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public abstract RecipeManager getRecipeManager(); + + public BlockPos getBlockRandomPos(int x, int y, int z, int l) { ++ // Paper start - allow use of mutable pos ++ BlockPos.MutableBlockPos ret = new BlockPos.MutableBlockPos(); ++ this.getRandomBlockPosition(x, y, z, l, ret); ++ return ret.immutable(); ++ } ++ public final BlockPos.MutableBlockPos getRandomBlockPosition(int x, int y, int z, int l, BlockPos.MutableBlockPos out) { ++ // Paper end + this.randValue = this.randValue * 3 + 1013904223; + int i1 = this.randValue >> 2; + +- return new BlockPos(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15)); ++ out.set(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15)); // Paper - change to setValues call ++ return out; // Paper + } + + public boolean noSave() { +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +index f2e11bff414b521295bde721e01ae2166a6a3fd6..8cd6c1d838e0332125fde3fc36475034aa4effa0 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +@@ -25,6 +25,7 @@ public class LevelChunkSection { + public final PalettedContainer states; + // CraftBukkit start - read/write + private PalettedContainer> biomes; ++ public final com.destroystokyo.paper.util.maplist.IBlockDataList tickingList = new com.destroystokyo.paper.util.maplist.IBlockDataList(); // Paper + + public LevelChunkSection(PalettedContainer datapaletteblock, PalettedContainer> palettedcontainerro) { + // CraftBukkit end +@@ -77,6 +78,9 @@ public class LevelChunkSection { + --this.nonEmptyBlockCount; + if (iblockdata1.isRandomlyTicking()) { + --this.tickingBlockCount; ++ // Paper start ++ this.tickingList.remove(x, y, z); ++ // Paper end + } + } + +@@ -88,6 +92,9 @@ public class LevelChunkSection { + ++this.nonEmptyBlockCount; + if (state.isRandomlyTicking()) { + ++this.tickingBlockCount; ++ // Paper start ++ this.tickingList.add(x, y, z, state); ++ // Paper end + } + } + +@@ -115,40 +122,34 @@ public class LevelChunkSection { + } + + public void recalcBlockCounts() { +- class a implements PalettedContainer.CountConsumer { +- +- public int nonEmptyBlockCount; +- public int tickingBlockCount; +- public int tickingFluidCount; +- +- a(final LevelChunkSection chunksection) {} +- +- public void accept(BlockState iblockdata, int i) { ++ // Paper start - unfuck this ++ this.tickingList.clear(); ++ this.nonEmptyBlockCount = 0; ++ this.tickingBlockCount = 0; ++ this.tickingFluidCount = 0; ++ // Don't run this on clearly empty sections ++ if (this.maybeHas((BlockState state) -> !state.isAir() || !state.getFluidState().isEmpty())) { ++ this.states.forEachLocation((BlockState iblockdata, int i) -> { + FluidState fluid = iblockdata.getFluidState(); + + if (!iblockdata.isAir()) { +- this.nonEmptyBlockCount += i; ++ this.nonEmptyBlockCount = (short) (this.nonEmptyBlockCount + 1); + if (iblockdata.isRandomlyTicking()) { +- this.tickingBlockCount += i; ++ this.tickingBlockCount = (short)(this.tickingBlockCount + 1); ++ this.tickingList.add(i, iblockdata); + } + } + + if (!fluid.isEmpty()) { +- this.nonEmptyBlockCount += i; ++ this.nonEmptyBlockCount = (short) (this.nonEmptyBlockCount + 1); + if (fluid.isRandomlyTicking()) { +- this.tickingFluidCount += i; ++ this.tickingFluidCount = (short) (this.tickingFluidCount + 1); + } + } + +- } ++ }); + } +- +- a a0 = new a(this); +- +- this.states.count(a0); +- this.nonEmptyBlockCount = (short) a0.nonEmptyBlockCount; +- this.tickingBlockCount = (short) a0.tickingBlockCount; +- this.tickingFluidCount = (short) a0.tickingFluidCount; ++ // Paper end - unfuck this + } + + public PalettedContainer getStates() { +diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java +index 926c81a25180d508d662eb3fa35f771636164694..81368bf186365878db2e1ed305bb7bf36c26f61f 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java ++++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java +@@ -381,6 +381,14 @@ public class PalettedContainer implements PaletteResize, PalettedContainer + } + } + ++ // Paper start ++ public void forEachLocation(PalettedContainer.CountConsumer consumer) { ++ this.data.storage.forEach((int location, int data) -> { ++ consumer.accept(this.data.palette.valueFor(data), location); ++ }); ++ } ++ // Paper end ++ + @FunctionalInterface + public interface CountConsumer { + void accept(T object, int count); diff --git a/patches/server/1015-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch b/patches/server/1015-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch new file mode 100644 index 0000000000..0bbbb36e0b --- /dev/null +++ b/patches/server/1015-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch @@ -0,0 +1,777 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sun, 2 Feb 2020 02:25:10 -0800 +Subject: [PATCH] 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. + +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +index e64fe79b231987e240d7482c0d6fa3c89dc97462..db932f8e9d48e1e47a89526d1d76947e91b55eea 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +@@ -72,6 +72,18 @@ import net.minecraft.world.ticks.ProtoChunkTicks; + import org.slf4j.Logger; + + public class ChunkSerializer { ++ // Paper start - Attempt to recalculate regionfile header if it is corrupt ++ // TODO: Check on update ++ public static long getLastWorldSaveTime(CompoundTag chunkData) { ++ final int dataVersion = ChunkStorage.getVersion(chunkData); ++ if (dataVersion < 2842) { // Level tag is removed after this version ++ final CompoundTag levelData = chunkData.getCompound("Level"); ++ return levelData.getLong("LastUpdate"); ++ } else { ++ return chunkData.getLong("LastUpdate"); ++ } ++ } ++ // Paper end - Attempt to recalculate regionfile header if it is corrupt + + public static final Codec> BLOCK_STATE_CODEC = PalettedContainer.codecRW(Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState(), null); // Paper - Anti-Xray - Add preset block states + private static final Logger LOGGER = LogUtils.getLogger(); +@@ -449,7 +461,7 @@ public class ChunkSerializer { + nbttagcompound.putInt("xPos", chunkcoordintpair.x); + nbttagcompound.putInt("yPos", chunk.getMinSection()); + nbttagcompound.putInt("zPos", chunkcoordintpair.z); +- nbttagcompound.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : world.getGameTime()); // Paper - async chunk unloading ++ nbttagcompound.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : world.getGameTime()); // Paper - async chunk unloading // Paper - diff on change + nbttagcompound.putLong("InhabitedTime", chunk.getInhabitedTime()); + nbttagcompound.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getStatus()).toString()); + BlendingData blendingdata = chunk.getBlendingData(); +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java +index 554dede2ad0e45d3ee4ccc5510b7644f2e9e4250..7801fac96d728f951989fca36f6a4890a0638c36 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java +@@ -41,7 +41,7 @@ public class ChunkStorage implements AutoCloseable { + + public ChunkStorage(RegionStorageInfo storageKey, Path directory, DataFixer dataFixer, boolean dsync) { + this.fixerUpper = dataFixer; +- this.regionFileCache = new RegionFileStorage(storageKey, directory, dsync); // Paper - rewrite chunk system; async chunk IO ++ this.regionFileCache = new RegionFileStorage(storageKey, directory, dsync, true); // Paper - rewrite chunk system; async chunk IO & Attempt to recalculate regionfile header if it is corrupt + } + + public boolean isOldChunkAround(ChunkPos chunkPos, int checkRadius) { +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java +index a23dc2f8f4475de1ee35bf18a7a8a53233ccac12..226af44fd469053451a0403a95ffb446face9530 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java +@@ -9,6 +9,27 @@ import java.util.BitSet; + public class RegionBitmap { + private final BitSet used = new BitSet(); + ++ // Paper start - Attempt to recalculate regionfile header if it is corrupt ++ public final void copyFrom(RegionBitmap other) { ++ BitSet thisBitset = this.used; ++ BitSet otherBitset = other.used; ++ ++ 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.used; ++ int firstSet = bitset.nextSetBit(from); ++ if (firstSet > 0 && firstSet < (from + length)) { ++ return false; ++ } ++ bitset.set(from, from + length); ++ return true; ++ } ++ // Paper end - Attempt to recalculate regionfile header if it is corrupt ++ + public void force(int start, int size) { + this.used.set(start, start + size); + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java +index cf43daa019b239464401784938d01af83f9da47c..1362a47943cf1a51a185a15094b1f74c94bf40ef 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java +@@ -52,6 +52,354 @@ public class RegionFile implements AutoCloseable { + @VisibleForTesting + protected final RegionBitmap usedSectors; + public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(); // Paper ++ // Paper start - Attempt to recalculate regionfile header if it is corrupt ++ 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 CompoundTag OVERSIZED_COMPOUND = new CompoundTag(); ++ ++ private CompoundTag 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.file.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; ++ } ++ ++ RegionFileVersion compression = RegionFileVersion.fromId(compressionType); ++ if (compression == null) { ++ return null; ++ } ++ ++ InputStream input = compression.wrap(new ByteArrayInputStream(chunkData.array(), chunkData.position(), chunkDataLength - chunkData.position())); ++ ++ return NbtIo.read(new DataInputStream(input)); ++ } catch (Exception ex) { ++ return null; ++ } ++ } ++ ++ private int getLength(long sector) throws IOException { ++ ByteBuffer length = ByteBuffer.allocate(4); ++ if (4 != this.file.read(length, sector * 4096L)) { ++ return -1; ++ } ++ ++ return length.getInt(0); ++ } ++ ++ private void backupRegionFile() { ++ Path backup = this.path.getParent().resolve(this.path.getFileName() + "." + new java.util.Random().nextLong() + ".backup"); ++ this.backupRegionFile(backup); ++ } ++ ++ private void backupRegionFile(Path to) { ++ try { ++ this.file.force(true); ++ LOGGER.warn("Backing up regionfile \"" + this.path.toAbsolutePath() + "\" to " + to.toAbsolutePath()); ++ java.nio.file.Files.copy(this.path, to, java.nio.file.StandardCopyOption.COPY_ATTRIBUTES); ++ LOGGER.warn("Backed up the regionfile to " + to.toAbsolutePath()); ++ } catch (IOException ex) { ++ LOGGER.error("Failed to backup to " + to.toAbsolutePath(), ex); ++ } ++ } ++ ++ private static boolean inSameRegionfile(ChunkPos first, ChunkPos second) { ++ return (first.x & ~31) == (second.x & ~31) && (first.z & ~31) == (second.z & ~31); ++ } ++ ++ // note: only call for CHUNK regionfiles ++ boolean recalculateHeader() throws IOException { ++ if (!this.canRecalcHeader) { ++ return false; ++ } ++ ChunkPos ourLowerLeftPosition = RegionFileStorage.getRegionFileCoordinates(this.path); ++ if (ourLowerLeftPosition == null) { ++ LOGGER.error("Unable to get chunk location of regionfile " + this.path.toAbsolutePath() + ", cannot recover header"); ++ return false; ++ } ++ synchronized (this) { ++ LOGGER.warn("Corrupt regionfile header detected! Attempting to re-calculate header offsets for regionfile " + this.path.toAbsolutePath(), new Throwable()); ++ ++ // try to backup file so maybe it could be sent to us for further investigation ++ ++ this.backupRegionFile(); ++ CompoundTag[] compounds = new CompoundTag[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.file.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); ++ CompoundTag compound = this.attemptRead(i, chunkDataLength, fileLength); ++ if (compound == null || compound == OVERSIZED_COMPOUND) { ++ continue; ++ } ++ ++ ChunkPos chunkPos = ChunkSerializer.getChunkCoordinate(compound); ++ if (!inSameRegionfile(ourLowerLeftPosition, chunkPos)) { ++ LOGGER.error("Ignoring absolute chunk " + chunkPos + " in regionfile as it is not contained in the bounds of the regionfile '" + this.path.toAbsolutePath() + "'. It should be in regionfile (" + (chunkPos.x >> 5) + "," + (chunkPos.z >> 5) + ")"); ++ continue; ++ } ++ int location = (chunkPos.x & 31) | ((chunkPos.z & 31) << 5); ++ ++ CompoundTag otherCompound = compounds[location]; ++ ++ if (otherCompound != null && ChunkSerializer.getLastWorldSaveTime(otherCompound) > ChunkSerializer.getLastWorldSaveTime(compound)) { ++ continue; // don't overwrite newer data. ++ } ++ ++ // aikar oversized? ++ Path aikarOversizedFile = this.getOversizedFile(chunkPos.x, chunkPos.z); ++ boolean isAikarOversized = false; ++ if (Files.exists(aikarOversizedFile)) { ++ try { ++ CompoundTag aikarOversizedCompound = this.getOversizedData(chunkPos.x, chunkPos.z); ++ if (ChunkSerializer.getLastWorldSaveTime(compound) == ChunkSerializer.getLastWorldSaveTime(aikarOversizedCompound)) { ++ // best we got for an id. hope it's good enough ++ isAikarOversized = true; ++ } ++ } catch (Exception ex) { ++ LOGGER.error("Failed to read aikar oversized data for absolute chunk (" + chunkPos.x + "," + chunkPos.z + ") in regionfile " + this.path.toAbsolutePath() + ", 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.externalFileDir; ++ Path[] regionFiles = Files.list(containingFolder).toArray(Path[]::new); ++ boolean[] oversized = new boolean[32 * 32]; ++ RegionFileVersion[] oversizedCompressionTypes = new RegionFileVersion[32 * 32]; ++ ++ if (regionFiles != null) { ++ 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 (Path regionFile : regionFiles) { ++ ChunkPos 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); ++ } catch (Exception ex) { ++ LOGGER.error("Failed to read oversized chunk data in file " + regionFile.toAbsolutePath() + ", data will be lost", ex); ++ continue; ++ } ++ ++ CompoundTag compound = null; ++ ++ // We do not know the compression type, as it's stored in the regionfile. So we need to try all of them ++ RegionFileVersion compression = null; ++ for (RegionFileVersion compressionType : RegionFileVersion.VERSIONS.values()) { ++ try { ++ DataInputStream in = new DataInputStream(compressionType.wrap(new ByteArrayInputStream(chunkData))); // typical java ++ compound = NbtIo.read((java.io.DataInput)in); ++ compression = compressionType; ++ break; // reaches here iff readNBT does not throw ++ } catch (Exception ex) { ++ continue; ++ } ++ } ++ ++ if (compound == null) { ++ LOGGER.error("Failed to read oversized chunk data in file " + regionFile.toAbsolutePath() + ", it's corrupt. Its data will be lost"); ++ continue; ++ } ++ ++ if (!ChunkSerializer.getChunkCoordinate(compound).equals(oversizedCoords)) { ++ LOGGER.error("Can't use oversized chunk stored in " + regionFile.toAbsolutePath() + ", got absolute chunkpos: " + ChunkSerializer.getChunkCoordinate(compound) + ", expected " + oversizedCoords); ++ continue; ++ } ++ ++ if (compounds[location] == null || ChunkSerializer.getLastWorldSaveTime(compound) > ChunkSerializer.getLastWorldSaveTime(compounds[location])) { ++ oversized[location] = true; ++ oversizedCompressionTypes[location] = compression; ++ } ++ } ++ } ++ ++ // now we need to calculate a new offset header ++ ++ int[] calculatedOffsets = new int[32 * 32]; ++ RegionBitmap newSectorAllocations = new RegionBitmap(); ++ newSectorAllocations.force(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 { ++ LOGGER.error("Failed to allocate space for local chunk (overlapping data??) at (" + chunkX + "," + chunkZ + ") in regionfile " + this.path.toAbsolutePath() + ", 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.allocate(1); ++ int sectorLength = 1; ++ ++ try { ++ this.file.write(this.createExternalStub(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); ++ LOGGER.error("Failed to write new oversized chunk data holder, local chunk at (" + chunkX + "," + chunkZ + ") in regionfile " + this.path.toAbsolutePath() + " 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) { ++ LOGGER.error("Failed to write aikar oversized chunk meta, all aikar style oversized chunk data will be lost for regionfile " + this.path.toAbsolutePath(), ex); ++ Files.deleteIfExists(this.getOversizedMetaFile()); ++ } ++ } else { ++ Files.deleteIfExists(this.getOversizedMetaFile()); ++ } ++ ++ this.usedSectors.copyFrom(newSectorAllocations); ++ ++ // before we overwrite the old sectors, print a summary of the chunks that got changed. ++ ++ LOGGER.info("Starting summary of changes for regionfile " + this.path.toAbsolutePath()); ++ ++ for (int chunkX = 0; chunkX < 32; ++chunkX) { ++ for (int chunkZ = 0; chunkZ < 32; ++chunkZ) { ++ int location = chunkX | (chunkZ << 5); ++ ++ int oldOffset = this.offsets.get(location); ++ int newOffset = calculatedOffsets[location]; ++ ++ if (oldOffset == newOffset) { ++ continue; ++ } ++ ++ this.offsets.put(location, newOffset); // overwrite incorrect offset ++ ++ if (oldOffset == 0) { ++ // found lost data ++ LOGGER.info("Found missing data for local chunk (" + chunkX + "," + chunkZ + ") in regionfile " + this.path.toAbsolutePath()); ++ } else if (newOffset == 0) { ++ LOGGER.warn("Data for local chunk (" + chunkX + "," + chunkZ + ") could not be recovered in regionfile " + this.path.toAbsolutePath() + ", it will be regenerated"); ++ } else { ++ LOGGER.info("Local chunk (" + chunkX + "," + chunkZ + ") changed to point to newer data or correct chunk in regionfile " + this.path.toAbsolutePath()); ++ } ++ } ++ } ++ ++ LOGGER.info("End of change summary for regionfile " + this.path.toAbsolutePath()); ++ ++ // simply destroy the timestamp header, it's not used ++ ++ for (int i = 0; i < 32 * 32; ++i) { ++ this.timestamps.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.flush(); ++ this.file.force(true); // try to ensure it goes through... ++ LOGGER.info("Successfully wrote new header to disk for regionfile " + this.path.toAbsolutePath()); ++ } catch (IOException ex) { ++ LOGGER.error("Failed to write new header to disk for regionfile " + this.path.toAbsolutePath(), ex); ++ } ++ } ++ ++ return true; ++ } ++ ++ final boolean canRecalcHeader; // final forces compile fail on new constructor ++ // Paper end - Attempt to recalculate regionfile header if it is corrupt + // Paper start - Cache chunk status + private final net.minecraft.world.level.chunk.status.ChunkStatus[] statuses = new net.minecraft.world.level.chunk.status.ChunkStatus[32 * 32]; + +@@ -79,8 +427,18 @@ public class RegionFile implements AutoCloseable { + public RegionFile(RegionStorageInfo storageKey, Path directory, Path path, boolean dsync) throws IOException { + this(storageKey, directory, path, RegionFileVersion.getCompressionFormat(), dsync); // Paper - Configurable region compression format + } ++ // Paper start - add can recalc flag ++ public RegionFile(RegionStorageInfo storageKey, Path directory, Path path, boolean dsync, boolean canRecalcHeader) throws IOException { ++ this(storageKey, directory, path, RegionFileVersion.getCompressionFormat(), dsync, canRecalcHeader); ++ } + + public RegionFile(RegionStorageInfo storageKey, Path path, Path directory, RegionFileVersion compressionFormat, boolean dsync) throws IOException { ++ this(storageKey, path, directory, compressionFormat, dsync, true); ++ } ++ ++ public RegionFile(RegionStorageInfo storageKey, Path path, Path directory, RegionFileVersion compressionFormat, boolean dsync, boolean canRecalcHeader) throws IOException { ++ this.canRecalcHeader = canRecalcHeader; ++ // Paper end - add can recalc flag + this.header = ByteBuffer.allocateDirect(8192); + this.usedSectors = new RegionBitmap(); + this.info = storageKey; +@@ -110,14 +468,16 @@ public class RegionFile implements AutoCloseable { + RegionFile.LOGGER.warn("Region file {} has truncated header: {}", path, i); + } + +- long j = Files.size(path); ++ final long j = Files.size(path); final long regionFileSize = j; // Paper - recalculate header on header corruption + +- for (int k = 0; k < 1024; ++k) { +- int l = this.offsets.get(k); ++ boolean needsHeaderRecalc = false; // Paper - recalculate header on header corruption ++ boolean hasBackedUp = false; // Paper - recalculate header on header corruption ++ for (int k = 0; k < 1024; ++k) { final int headerLocation = k; // Paper - we expect this to be the header location ++ final int l = this.offsets.get(k); + + if (l != 0) { +- int i1 = RegionFile.getSectorNumber(l); +- int j1 = RegionFile.getNumSectors(l); ++ final int i1 = RegionFile.getSectorNumber(l); final int offset = i1; // Paper - we expect this to be offset in file in sectors ++ int j1 = RegionFile.getNumSectors(l); final int sectorLength; // Paper - 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 +@@ -126,21 +486,66 @@ public class RegionFile implements AutoCloseable { + j1 = (realLen.getInt(0) + 4) / 4096 + 1; + } + // Spigot end ++ sectorLength = j1; // Paper - diff on change, we expect this to be sector length of region + + if (i1 < 2) { + RegionFile.LOGGER.warn("Region file {} has invalid sector at index: {}; sector {} overlaps with header", new Object[]{path, k, i1}); +- this.offsets.put(k, 0); ++ //this.offsets.put(k, 0); // Paper - we catch this, but need it in the header for the summary change + } else if (j1 == 0) { + RegionFile.LOGGER.warn("Region file {} has an invalid sector at index: {}; size has to be > 0", path, k); +- this.offsets.put(k, 0); ++ //this.offsets.put(k, 0); // Paper - 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", new Object[]{path, k, i1}); +- this.offsets.put(k, 0); ++ //this.offsets.put(k, 0); // Paper - we catch this, but need it in the header for the summary change + } else { +- this.usedSectors.force(i1, j1); ++ //this.usedSectors.force(i1, j1); // Paper - move this down so we can check if it fails to allocate ++ } ++ // Paper start - recalculate header on header corruption ++ if (offset < 2 || sectorLength <= 0 || ((long)offset * 4096L) > regionFileSize) { ++ if (canRecalcHeader) { ++ LOGGER.error("Detected invalid header for regionfile " + this.path.toAbsolutePath() + "! Recalculating header..."); ++ needsHeaderRecalc = true; ++ break; ++ } else { ++ // location = chunkX | (chunkZ << 5); ++ LOGGER.error("Detected invalid header for regionfile " + this.path.toAbsolutePath() + ++ "! Cannot recalculate, removing local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") from header"); ++ if (!hasBackedUp) { ++ hasBackedUp = true; ++ this.backupRegionFile(); ++ } ++ this.timestamps.put(headerLocation, 0); // be consistent, delete the timestamp too ++ this.offsets.put(headerLocation, 0); // delete the entry from header ++ continue; ++ } ++ } ++ boolean failedToAllocate = !this.usedSectors.tryAllocate(offset, sectorLength); ++ if (failedToAllocate) { ++ LOGGER.error("Overlapping allocation by local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") in regionfile " + this.path.toAbsolutePath()); + } ++ if (failedToAllocate & !canRecalcHeader) { ++ // location = chunkX | (chunkZ << 5); ++ LOGGER.error("Detected invalid header for regionfile " + this.path.toAbsolutePath() + ++ "! Cannot recalculate, removing local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") from header"); ++ if (!hasBackedUp) { ++ hasBackedUp = true; ++ this.backupRegionFile(); ++ } ++ this.timestamps.put(headerLocation, 0); // be consistent, delete the timestamp too ++ this.offsets.put(headerLocation, 0); // delete the entry from header ++ continue; ++ } ++ needsHeaderRecalc |= failedToAllocate; ++ // Paper end - recalculate header on header corruption + } + } ++ // Paper 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 ++ LOGGER.error("Recalculating regionfile " + this.path.toAbsolutePath() + ", header gave erroneous offsets & locations"); ++ this.recalculateHeader(); ++ } ++ // Paper end + } + + } +@@ -151,11 +556,36 @@ public class RegionFile implements AutoCloseable { + } + + private Path getExternalChunkPath(ChunkPos chunkPos) { +- String s = "c." + chunkPos.x + "." + chunkPos.z + ".mcc"; ++ String s = "c." + chunkPos.x + "." + chunkPos.z + ".mcc"; // Paper - diff on change + + return this.externalFileDir.resolve(s); + } + ++ // Paper start ++ private static ChunkPos getOversizedChunkPair(Path file) { ++ String fileName = file.getFileName().toString(); ++ ++ 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 ChunkPos(x, z); ++ } catch (NumberFormatException ex) { ++ return null; ++ } ++ } ++ // Paper end ++ + @Nullable + public synchronized DataInputStream getChunkDataInputStream(ChunkPos pos) throws IOException { + int i = this.getOffset(pos); +@@ -179,6 +609,11 @@ public class RegionFile implements AutoCloseable { + ((java.nio.Buffer) bytebuffer).flip(); // CraftBukkit - decompile error + if (bytebuffer.remaining() < 5) { + RegionFile.LOGGER.error("Chunk {} header is truncated: expected {} but read {}", new Object[]{pos, l, bytebuffer.remaining()}); ++ // Paper start - recalculate header on regionfile corruption ++ if (this.canRecalcHeader && this.recalculateHeader()) { ++ return this.getChunkDataInputStream(pos); ++ } ++ // Paper end - recalculate header on regionfile corruption + return null; + } else { + int i1 = bytebuffer.getInt(); +@@ -186,6 +621,11 @@ public class RegionFile implements AutoCloseable { + + if (i1 == 0) { + RegionFile.LOGGER.warn("Chunk {} is allocated, but stream is missing", pos); ++ // Paper start - recalculate header on regionfile corruption ++ if (this.canRecalcHeader && this.recalculateHeader()) { ++ return this.getChunkDataInputStream(pos); ++ } ++ // Paper end - recalculate header on regionfile corruption + return null; + } else { + int j1 = i1 - 1; +@@ -193,18 +633,45 @@ public class RegionFile implements AutoCloseable { + if (RegionFile.isExternalStreamChunk(b0)) { + if (j1 != 0) { + RegionFile.LOGGER.warn("Chunk has both internal and external streams"); ++ // Paper start - recalculate header on regionfile corruption ++ if (this.canRecalcHeader && this.recalculateHeader()) { ++ return this.getChunkDataInputStream(pos); ++ } ++ // Paper end - recalculate header on regionfile corruption + } + +- return this.createExternalChunkInputStream(pos, RegionFile.getExternalChunkVersion(b0)); ++ // Paper start - recalculate header on regionfile corruption ++ final DataInputStream ret = this.createExternalChunkInputStream(pos, RegionFile.getExternalChunkVersion(b0)); ++ if (ret == null && this.canRecalcHeader && this.recalculateHeader()) { ++ return this.getChunkDataInputStream(pos); ++ } ++ return ret; ++ // Paper end - recalculate header on regionfile corruption + } else if (j1 > bytebuffer.remaining()) { + RegionFile.LOGGER.error("Chunk {} stream is truncated: expected {} but read {}", new Object[]{pos, j1, bytebuffer.remaining()}); ++ // Paper start - recalculate header on regionfile corruption ++ if (this.canRecalcHeader && this.recalculateHeader()) { ++ return this.getChunkDataInputStream(pos); ++ } ++ // Paper end - recalculate header on regionfile corruption + return null; + } else if (j1 < 0) { + RegionFile.LOGGER.error("Declared size {} of chunk {} is negative", i1, pos); ++ // Paper start - recalculate header on regionfile corruption ++ if (this.canRecalcHeader && this.recalculateHeader()) { ++ return this.getChunkDataInputStream(pos); ++ } ++ // Paper end - recalculate header on regionfile corruption + return null; + } else { + JvmProfiler.INSTANCE.onRegionFileRead(this.info, pos, this.version, j1); +- return this.createChunkInputStream(pos, b0, RegionFile.createStream(bytebuffer, j1)); ++ // Paper start - recalculate header on regionfile corruption ++ final DataInputStream ret = this.createChunkInputStream(pos, b0, RegionFile.createStream(bytebuffer, j1)); ++ if (ret == null && this.canRecalcHeader && this.recalculateHeader()) { ++ return this.getChunkDataInputStream(pos); ++ } ++ return ret; ++ // Paper end - recalculate header on regionfile corruption + } + } + } +@@ -390,10 +857,15 @@ public class RegionFile implements AutoCloseable { + } + + private ByteBuffer createExternalStub() { ++ // Paper start - add version param ++ return this.createExternalStub(this.version); ++ } ++ private ByteBuffer createExternalStub(RegionFileVersion version) { ++ // Paper end - add version param + ByteBuffer bytebuffer = ByteBuffer.allocate(5); + + bytebuffer.putInt(1); +- bytebuffer.put((byte) (this.version.getId() | 128)); ++ bytebuffer.put((byte) (version.getId() | 128)); // Paper - replace with version param + ((java.nio.Buffer) bytebuffer).flip(); // CraftBukkit - decompile error + return bytebuffer; + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +index c33640859aab837c85f3e860fe2241a0e78bb09a..249705ec1b8b692ef1d7fec34a04918afe6486bc 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +@@ -25,6 +25,7 @@ public class RegionFileStorage implements AutoCloseable { + private final RegionStorageInfo info; + private final Path folder; + private final boolean sync; ++ private final boolean isChunkData; // Paper + + // Paper start - cache regionfile does not exist state + static final int MAX_NON_EXISTING_CACHE = 1024 * 64; +@@ -56,6 +57,12 @@ public class RegionFileStorage implements AutoCloseable { + // Paper end - cache regionfile does not exist state + + protected RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) { // Paper - protected constructor ++ // Paper start - add isChunkData param ++ this(storageKey, directory, dsync, false); ++ } ++ RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync, boolean isChunkData) { ++ this.isChunkData = isChunkData; ++ // Paper end - add isChunkData param + this.folder = directory; + this.sync = dsync; + this.info = storageKey; +@@ -101,7 +108,7 @@ public class RegionFileStorage implements AutoCloseable { + // Paper - only create directory if not existing only - moved down + Path path = this.folder; + int j = chunkcoordintpair.getRegionX(); +- Path path1 = path.resolve("r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca"); ++ Path path1 = path.resolve("r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca"); // Paper - diff on change + if (existingOnly && !java.nio.file.Files.exists(path1)) { // Paper start - cache regionfile does not exist state + this.markNonExisting(regionPos); + return null; // CraftBukkit +@@ -110,7 +117,7 @@ public class RegionFileStorage implements AutoCloseable { + } + // Paper end - cache regionfile does not exist state + FileUtil.createDirectoriesSafe(this.folder); // Paper - only create directory if not existing only - moved from above +- RegionFile regionfile1 = new RegionFile(this.info, path1, this.folder, this.sync); ++ RegionFile regionfile1 = new RegionFile(this.info, path1, this.folder, this.sync, this.isChunkData); // Paper - allow for chunk regionfiles to regen header + + this.regionCache.putAndMoveToFirst(i, regionfile1); + // Paper start +@@ -167,6 +174,13 @@ public class RegionFileStorage implements AutoCloseable { + if (regionfile == null) { + return null; + } ++ // Paper start - Add regionfile parameter ++ return this.read(pos, regionfile); ++ } ++ public CompoundTag read(ChunkPos pos, RegionFile regionfile) 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 ++ // Paper end + // CraftBukkit end + try { // Paper + DataInputStream datainputstream = regionfile.getChunkDataInputStream(pos); +@@ -183,6 +197,20 @@ public class RegionFileStorage implements AutoCloseable { + try { + if (datainputstream != null) { + nbttagcompound = NbtIo.read((DataInput) datainputstream); ++ // Paper start - recover from corrupt regionfile header ++ if (this.isChunkData) { ++ ChunkPos chunkPos = ChunkSerializer.getChunkCoordinate(nbttagcompound); ++ if (!chunkPos.equals(pos)) { ++ net.minecraft.server.MinecraftServer.LOGGER.error("Attempting to read chunk data at " + pos + " but got chunk data for " + chunkPos + " instead! Attempting regionfile recalculation for regionfile " + regionfile.getPath().toAbsolutePath()); ++ if (regionfile.recalculateHeader()) { ++ regionfile.fileLock.lock(); // otherwise we will unlock twice and only lock once. ++ return this.read(pos, regionfile); ++ } ++ net.minecraft.server.MinecraftServer.LOGGER.error("Can't recalculate regionfile header, regenerating chunk " + pos + " for " + regionfile.getPath().toAbsolutePath()); ++ return null; ++ } ++ } ++ // Paper end - recover from corrupt regionfile header + break label43; + } + +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java +index ef68b57ef1d8d7cb317c417569dd23a777fba4ad..f4a39f49b354c560d614483db1cd3dfc154e94b4 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java +@@ -21,7 +21,7 @@ import org.slf4j.Logger; + + public class RegionFileVersion { + private static final Logger LOGGER = LogUtils.getLogger(); +- private static final Int2ObjectMap VERSIONS = new Int2ObjectOpenHashMap<>(); ++ public static final Int2ObjectMap VERSIONS = new Int2ObjectOpenHashMap<>(); // Paper - private -> public + private static final Object2ObjectMap VERSIONS_BY_NAME = new Object2ObjectOpenHashMap<>(); + public static final RegionFileVersion VERSION_GZIP = register( + new RegionFileVersion( diff --git a/patches/server/1015-Optimise-random-block-ticking.patch b/patches/server/1015-Optimise-random-block-ticking.patch deleted file mode 100644 index 07b98dffde..0000000000 --- a/patches/server/1015-Optimise-random-block-ticking.patch +++ /dev/null @@ -1,456 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sun, 20 Jun 2021 16:19:26 -0700 -Subject: [PATCH] Optimise random block ticking - -Massive performance improvement for random block ticking. -The performance increase comes from the fact that the vast -majority of attempted block ticks (~95% in my testing) fail -because the randomly selected block is not tickable. - -Now only tickable blocks are targeted, however this means that -the maximum number of block ticks occurs per chunk. However, -not all chunks are going to be targeted. The percent chance -of a chunk being targeted is based on how many tickable blocks -are in the chunk. -This means that while block ticks are spread out less, the -total number of blocks ticked per world tick remains the same. -Therefore, the chance of a random tickable block being ticked -remains the same. - -diff --git a/src/main/java/io/papermc/paper/util/math/ThreadUnsafeRandom.java b/src/main/java/io/papermc/paper/util/math/ThreadUnsafeRandom.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7d93652c1abbb6aee6eb7c26cf35d4d032ef7b69 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/util/math/ThreadUnsafeRandom.java -@@ -0,0 +1,65 @@ -+package io.papermc.paper.util.math; -+ -+import net.minecraft.util.RandomSource; -+import net.minecraft.world.level.levelgen.LegacyRandomSource; -+import net.minecraft.world.level.levelgen.PositionalRandomFactory; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.framework.qual.DefaultQualifier; -+ -+@DefaultQualifier(NonNull.class) -+public final class ThreadUnsafeRandom extends LegacyRandomSource { -+ -+ // See javadoc and internal comments for java.util.Random where these values come from, how they are used, and the author for them. -+ private static final long multiplier = 0x5DEECE66DL; -+ private static final long addend = 0xBL; -+ private static final long mask = (1L << 48) - 1; -+ -+ private static long initialScramble(long seed) { -+ return (seed ^ multiplier) & mask; -+ } -+ -+ private long seed; -+ -+ public ThreadUnsafeRandom(long seed) { -+ super(seed); -+ } -+ -+ @Override -+ public RandomSource fork() { -+ return new ThreadUnsafeRandom(this.nextLong()); -+ } -+ -+ @Override -+ public PositionalRandomFactory forkPositional() { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public void setSeed(long seed) { -+ // note: called by Random constructor -+ this.seed = initialScramble(seed); -+ } -+ -+ @Override -+ public int next(int bits) { -+ // avoid the expensive CAS logic used by superclass -+ return (int) (((this.seed = this.seed * multiplier + addend) & mask) >>> (48 - bits)); -+ } -+ -+ // Taken from -+ // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ -+ // https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog/blob/master/2016/06/25/fastrange.c -+ // Original license is public domain -+ public static int fastRandomBounded(final long randomInteger, final long limit) { -+ // randomInteger must be [0, pow(2, 32)) -+ // limit must be [0, pow(2, 32)) -+ return (int)((randomInteger * limit) >>> 32); -+ } -+ -+ @Override -+ public int nextInt(int bound) { -+ // yes this breaks random's spec -+ // however there's nothing that uses this class that relies on it -+ return fastRandomBounded(this.next(32) & 0xFFFFFFFFL, bound); -+ } -+} -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 9cd54bc390f0629bf6b4494bd9c3a6e0927d1b7b..215f03dfe0e6441588679efaedbcf04c352b404c 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -867,6 +867,10 @@ public class ServerLevel extends Level implements WorldGenLevel { - entityplayer.stopSleepInBed(false, false); - }); - } -+ // Paper start - optimise random block ticking -+ private final BlockPos.MutableBlockPos chunkTickMutablePosition = new BlockPos.MutableBlockPos(); -+ private final io.papermc.paper.util.math.ThreadUnsafeRandom randomTickRandom = new io.papermc.paper.util.math.ThreadUnsafeRandom(this.random.nextLong()); -+ // Paper end - - public void tickChunk(LevelChunk chunk, int randomTickSpeed) { - ChunkPos chunkcoordintpair = chunk.getPos(); -@@ -876,8 +880,10 @@ public class ServerLevel extends Level implements WorldGenLevel { - ProfilerFiller gameprofilerfiller = this.getProfiler(); - - gameprofilerfiller.push("thunder"); -+ final BlockPos.MutableBlockPos blockposition = this.chunkTickMutablePosition; // Paper - use mutable to reduce allocation rate, final to force compile fail on change -+ - if (!this.paperConfig().environment.disableThunder && flag && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder -- BlockPos blockposition = this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15)); -+ blockposition.set(this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15))); // Paper - - if (this.isRainingAt(blockposition)) { - DifficultyInstance difficultydamagescaler = this.getCurrentDifficultyAt(blockposition); -@@ -909,7 +915,10 @@ public class ServerLevel extends Level implements WorldGenLevel { - if (!this.paperConfig().environment.disableIceAndSnow) { // Paper - Option to disable ice and snow - for (int l = 0; l < randomTickSpeed; ++l) { - if (this.random.nextInt(48) == 0) { -- this.tickPrecipitation(this.getBlockRandomPos(j, 0, k, 15)); -+ // Paper start -+ this.getRandomBlockPosition(j, 0, k, 15, blockposition); -+ this.tickPrecipitation(blockposition, chunk); -+ // Paper end - } - } - } // Paper - Option to disable ice and snow -@@ -917,36 +926,37 @@ public class ServerLevel extends Level implements WorldGenLevel { - gameprofilerfiller.popPush("tickBlocks"); - timings.chunkTicksBlocks.startTiming(); // Paper - if (randomTickSpeed > 0) { -- LevelChunkSection[] achunksection = chunk.getSections(); -- -- for (int i1 = 0; i1 < achunksection.length; ++i1) { -- LevelChunkSection chunksection = achunksection[i1]; -- -- if (chunksection.isRandomlyTicking()) { -- int j1 = chunk.getSectionYFromSectionIndex(i1); -- int k1 = SectionPos.sectionToBlockCoord(j1); -- -- for (int l1 = 0; l1 < randomTickSpeed; ++l1) { -- BlockPos blockposition1 = this.getBlockRandomPos(j, k1, k, 15); -- -- gameprofilerfiller.push("randomTick"); -- BlockState iblockdata = chunksection.getBlockState(blockposition1.getX() - j, blockposition1.getY() - k1, blockposition1.getZ() - k); -- -- if (iblockdata.isRandomlyTicking()) { -- iblockdata.randomTick(this, blockposition1, this.random); -- } -+ // Paper start - optimize random block ticking -+ LevelChunkSection[] sections = chunk.getSections(); -+ final int minSection = io.papermc.paper.util.WorldUtil.getMinSection(this); -+ for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { -+ LevelChunkSection section = sections[sectionIndex]; -+ if (section == null || section.tickingList.size() == 0) continue; -+ -+ int yPos = (sectionIndex + minSection) << 4; -+ for (int a = 0; a < randomTickSpeed; ++a) { -+ int tickingBlocks = section.tickingList.size(); -+ int index = this.randomTickRandom.nextInt(16 * 16 * 16); -+ if (index >= tickingBlocks) { -+ continue; -+ } - -- FluidState fluid = iblockdata.getFluidState(); -+ long raw = section.tickingList.getRaw(index); -+ int location = com.destroystokyo.paper.util.maplist.IBlockDataList.getLocationFromRaw(raw); -+ int randomX = location & 15; -+ int randomY = ((location >>> (4 + 4)) & 255) | yPos; -+ int randomZ = (location >>> 4) & 15; - -- if (fluid.isRandomlyTicking()) { -- fluid.randomTick(this, blockposition1, this.random); -- } -+ BlockPos blockposition2 = blockposition.set(j + randomX, randomY, k + randomZ); -+ BlockState iblockdata = com.destroystokyo.paper.util.maplist.IBlockDataList.getBlockDataFromRaw(raw); - -- gameprofilerfiller.pop(); -- } -+ iblockdata.randomTick(this, blockposition2, this.randomTickRandom); - } -+ // We drop the fluid tick since LAVA is ALREADY TICKED by the above method (See LiquidBlock). -+ // TODO CHECK ON UPDATE (ping the Canadian) - } - } -+ // Paper end - optimise random block ticking - - timings.chunkTicksBlocks.stopTiming(); // Paper - gameprofilerfiller.pop(); -@@ -954,17 +964,25 @@ public class ServerLevel extends Level implements WorldGenLevel { - - @VisibleForTesting - public void tickPrecipitation(BlockPos pos) { -- BlockPos blockposition1 = this.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, pos); -- BlockPos blockposition2 = blockposition1.below(); -+ // Paper start - optimise chunk ticking -+ tickPrecipitation(pos.mutable(), this.getChunkAt(pos)); -+ } -+ public void tickPrecipitation(BlockPos.MutableBlockPos blockposition1, final LevelChunk chunk) { -+ int normalY = chunk.getHeight(Heightmap.Types.MOTION_BLOCKING, blockposition1.getX() & 15, blockposition1.getZ() & 15) + 1; -+ int downY = normalY - 1; -+ blockposition1.setY(normalY); -+ // Paper end - optimise chunk ticking - Biome biomebase = (Biome) this.getBiome(blockposition1).value(); - -- if (biomebase.shouldFreeze(this, blockposition2)) { -- org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition2, Blocks.ICE.defaultBlockState(), null); // CraftBukkit -+ blockposition1.setY(downY); -+ if (biomebase.shouldFreeze(this, blockposition1)) { -+ org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition1, Blocks.ICE.defaultBlockState(), null); // CraftBukkit - } - - if (this.isRaining()) { - int i = this.getGameRules().getInt(GameRules.RULE_SNOW_ACCUMULATION_HEIGHT); - -+ blockposition1.setY(normalY); // Paper - optimise chunk ticking - if (i > 0 && biomebase.shouldSnow(this, blockposition1)) { - BlockState iblockdata = this.getBlockState(blockposition1); - -@@ -982,12 +1000,13 @@ public class ServerLevel extends Level implements WorldGenLevel { - } - } - -- Biome.Precipitation biomebase_precipitation = biomebase.getPrecipitationAt(blockposition2); -+ blockposition1.setY(downY); // Paper - optimise chunk ticking -+ Biome.Precipitation biomebase_precipitation = biomebase.getPrecipitationAt(blockposition1); // Paper - optimise chunk ticking - - if (biomebase_precipitation != Biome.Precipitation.NONE) { -- BlockState iblockdata2 = this.getBlockState(blockposition2); -+ BlockState iblockdata2 = this.getBlockState(blockposition1); // Paper - optimise chunk ticking - -- iblockdata2.getBlock().handlePrecipitation(iblockdata2, this, blockposition2, biomebase_precipitation); -+ iblockdata2.getBlock().handlePrecipitation(iblockdata2, this, blockposition1, biomebase_precipitation); // Paper - optimise chunk ticking - } - } - -diff --git a/src/main/java/net/minecraft/util/BitStorage.java b/src/main/java/net/minecraft/util/BitStorage.java -index 68648c5a5e3ff079f832092af0f2f801c42d1ede..8bafd5fd7499ba4a04bf706cfd1e156073716e21 100644 ---- a/src/main/java/net/minecraft/util/BitStorage.java -+++ b/src/main/java/net/minecraft/util/BitStorage.java -@@ -20,4 +20,15 @@ public interface BitStorage { - void unpack(int[] out); - - BitStorage copy(); -+ -+ // Paper start -+ void forEach(DataBitConsumer consumer); -+ -+ @FunctionalInterface -+ interface DataBitConsumer { -+ -+ void accept(int location, int data); -+ -+ } -+ // Paper end - } -diff --git a/src/main/java/net/minecraft/util/SimpleBitStorage.java b/src/main/java/net/minecraft/util/SimpleBitStorage.java -index 9f438d9c6eb05e43d24e4af68188a3d4c46a938c..8d7d763bf51cac556057645e6169c9447993189b 100644 ---- a/src/main/java/net/minecraft/util/SimpleBitStorage.java -+++ b/src/main/java/net/minecraft/util/SimpleBitStorage.java -@@ -315,6 +315,28 @@ public class SimpleBitStorage implements BitStorage { - return this.bits; - } - -+ // Paper start -+ @Override -+ public final void forEach(DataBitConsumer consumer) { -+ int i = 0; -+ long[] along = this.data; -+ int j = along.length; -+ -+ for (int k = 0; k < j; ++k) { -+ long l = along[k]; -+ -+ for (int i1 = 0; i1 < this.valuesPerLong; ++i1) { -+ consumer.accept(i, (int) (l & this.mask)); -+ l >>= this.bits; -+ ++i; -+ if (i >= this.size) { -+ return; -+ } -+ } -+ } -+ } -+ // Paper end -+ - @Override - public void getAll(IntConsumer action) { - int i = 0; -diff --git a/src/main/java/net/minecraft/util/ZeroBitStorage.java b/src/main/java/net/minecraft/util/ZeroBitStorage.java -index 50040c497a819cd1229042ab3cb057d34a32cacc..01f5b946fabbe34f31110e75973dab9f39897346 100644 ---- a/src/main/java/net/minecraft/util/ZeroBitStorage.java -+++ b/src/main/java/net/minecraft/util/ZeroBitStorage.java -@@ -46,6 +46,15 @@ public class ZeroBitStorage implements BitStorage { - return 0; - } - -+ // Paper start -+ @Override -+ public void forEach(DataBitConsumer consumer) { -+ for(int i = 0; i < this.size; ++i) { -+ consumer.accept(i, 0); -+ } -+ } -+ // Paper end -+ - @Override - public void getAll(IntConsumer action) { - for (int i = 0; i < this.size; i++) { -diff --git a/src/main/java/net/minecraft/world/entity/animal/Turtle.java b/src/main/java/net/minecraft/world/entity/animal/Turtle.java -index ffffa53fcaa6ec8681b283fa20bb5a3320ad9c11..30b87b5cb18c25cdd04eab64cfbe5acd6c1b6d84 100644 ---- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java -+++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java -@@ -88,7 +88,7 @@ public class Turtle extends Animal { - } - - public void setHomePos(BlockPos pos) { -- this.entityData.set(Turtle.HOME_POS, pos); -+ this.entityData.set(Turtle.HOME_POS, pos.immutable()); // Paper - called with mutablepos... - } - - public BlockPos getHomePos() { -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index dab55ab08665f2b5ae0c899a4ab07c18460552ae..4001bec504b8fcda1eec03f49abd4e4d82cf6336 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -1387,10 +1387,18 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - public abstract RecipeManager getRecipeManager(); - - public BlockPos getBlockRandomPos(int x, int y, int z, int l) { -+ // Paper start - allow use of mutable pos -+ BlockPos.MutableBlockPos ret = new BlockPos.MutableBlockPos(); -+ this.getRandomBlockPosition(x, y, z, l, ret); -+ return ret.immutable(); -+ } -+ public final BlockPos.MutableBlockPos getRandomBlockPosition(int x, int y, int z, int l, BlockPos.MutableBlockPos out) { -+ // Paper end - this.randValue = this.randValue * 3 + 1013904223; - int i1 = this.randValue >> 2; - -- return new BlockPos(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15)); -+ out.set(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15)); // Paper - change to setValues call -+ return out; // Paper - } - - public boolean noSave() { -diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java -index f2e11bff414b521295bde721e01ae2166a6a3fd6..8cd6c1d838e0332125fde3fc36475034aa4effa0 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java -+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java -@@ -25,6 +25,7 @@ public class LevelChunkSection { - public final PalettedContainer states; - // CraftBukkit start - read/write - private PalettedContainer> biomes; -+ public final com.destroystokyo.paper.util.maplist.IBlockDataList tickingList = new com.destroystokyo.paper.util.maplist.IBlockDataList(); // Paper - - public LevelChunkSection(PalettedContainer datapaletteblock, PalettedContainer> palettedcontainerro) { - // CraftBukkit end -@@ -77,6 +78,9 @@ public class LevelChunkSection { - --this.nonEmptyBlockCount; - if (iblockdata1.isRandomlyTicking()) { - --this.tickingBlockCount; -+ // Paper start -+ this.tickingList.remove(x, y, z); -+ // Paper end - } - } - -@@ -88,6 +92,9 @@ public class LevelChunkSection { - ++this.nonEmptyBlockCount; - if (state.isRandomlyTicking()) { - ++this.tickingBlockCount; -+ // Paper start -+ this.tickingList.add(x, y, z, state); -+ // Paper end - } - } - -@@ -115,40 +122,34 @@ public class LevelChunkSection { - } - - public void recalcBlockCounts() { -- class a implements PalettedContainer.CountConsumer { -- -- public int nonEmptyBlockCount; -- public int tickingBlockCount; -- public int tickingFluidCount; -- -- a(final LevelChunkSection chunksection) {} -- -- public void accept(BlockState iblockdata, int i) { -+ // Paper start - unfuck this -+ this.tickingList.clear(); -+ this.nonEmptyBlockCount = 0; -+ this.tickingBlockCount = 0; -+ this.tickingFluidCount = 0; -+ // Don't run this on clearly empty sections -+ if (this.maybeHas((BlockState state) -> !state.isAir() || !state.getFluidState().isEmpty())) { -+ this.states.forEachLocation((BlockState iblockdata, int i) -> { - FluidState fluid = iblockdata.getFluidState(); - - if (!iblockdata.isAir()) { -- this.nonEmptyBlockCount += i; -+ this.nonEmptyBlockCount = (short) (this.nonEmptyBlockCount + 1); - if (iblockdata.isRandomlyTicking()) { -- this.tickingBlockCount += i; -+ this.tickingBlockCount = (short)(this.tickingBlockCount + 1); -+ this.tickingList.add(i, iblockdata); - } - } - - if (!fluid.isEmpty()) { -- this.nonEmptyBlockCount += i; -+ this.nonEmptyBlockCount = (short) (this.nonEmptyBlockCount + 1); - if (fluid.isRandomlyTicking()) { -- this.tickingFluidCount += i; -+ this.tickingFluidCount = (short) (this.tickingFluidCount + 1); - } - } - -- } -+ }); - } -- -- a a0 = new a(this); -- -- this.states.count(a0); -- this.nonEmptyBlockCount = (short) a0.nonEmptyBlockCount; -- this.tickingBlockCount = (short) a0.tickingBlockCount; -- this.tickingFluidCount = (short) a0.tickingFluidCount; -+ // Paper end - unfuck this - } - - public PalettedContainer getStates() { -diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java -index 926c81a25180d508d662eb3fa35f771636164694..81368bf186365878db2e1ed305bb7bf36c26f61f 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java -+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java -@@ -381,6 +381,14 @@ public class PalettedContainer implements PaletteResize, PalettedContainer - } - } - -+ // Paper start -+ public void forEachLocation(PalettedContainer.CountConsumer consumer) { -+ this.data.storage.forEach((int location, int data) -> { -+ consumer.accept(this.data.palette.valueFor(data), location); -+ }); -+ } -+ // Paper end -+ - @FunctionalInterface - public interface CountConsumer { - void accept(T object, int count); diff --git a/patches/server/1016-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch b/patches/server/1016-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch deleted file mode 100644 index 0bbbb36e0b..0000000000 --- a/patches/server/1016-Attempt-to-recalculate-regionfile-header-if-it-is-co.patch +++ /dev/null @@ -1,777 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sun, 2 Feb 2020 02:25:10 -0800 -Subject: [PATCH] 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. - -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java -index e64fe79b231987e240d7482c0d6fa3c89dc97462..db932f8e9d48e1e47a89526d1d76947e91b55eea 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java -@@ -72,6 +72,18 @@ import net.minecraft.world.ticks.ProtoChunkTicks; - import org.slf4j.Logger; - - public class ChunkSerializer { -+ // Paper start - Attempt to recalculate regionfile header if it is corrupt -+ // TODO: Check on update -+ public static long getLastWorldSaveTime(CompoundTag chunkData) { -+ final int dataVersion = ChunkStorage.getVersion(chunkData); -+ if (dataVersion < 2842) { // Level tag is removed after this version -+ final CompoundTag levelData = chunkData.getCompound("Level"); -+ return levelData.getLong("LastUpdate"); -+ } else { -+ return chunkData.getLong("LastUpdate"); -+ } -+ } -+ // Paper end - Attempt to recalculate regionfile header if it is corrupt - - public static final Codec> BLOCK_STATE_CODEC = PalettedContainer.codecRW(Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState(), null); // Paper - Anti-Xray - Add preset block states - private static final Logger LOGGER = LogUtils.getLogger(); -@@ -449,7 +461,7 @@ public class ChunkSerializer { - nbttagcompound.putInt("xPos", chunkcoordintpair.x); - nbttagcompound.putInt("yPos", chunk.getMinSection()); - nbttagcompound.putInt("zPos", chunkcoordintpair.z); -- nbttagcompound.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : world.getGameTime()); // Paper - async chunk unloading -+ nbttagcompound.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : world.getGameTime()); // Paper - async chunk unloading // Paper - diff on change - nbttagcompound.putLong("InhabitedTime", chunk.getInhabitedTime()); - nbttagcompound.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getStatus()).toString()); - BlendingData blendingdata = chunk.getBlendingData(); -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java -index 554dede2ad0e45d3ee4ccc5510b7644f2e9e4250..7801fac96d728f951989fca36f6a4890a0638c36 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java -@@ -41,7 +41,7 @@ public class ChunkStorage implements AutoCloseable { - - public ChunkStorage(RegionStorageInfo storageKey, Path directory, DataFixer dataFixer, boolean dsync) { - this.fixerUpper = dataFixer; -- this.regionFileCache = new RegionFileStorage(storageKey, directory, dsync); // Paper - rewrite chunk system; async chunk IO -+ this.regionFileCache = new RegionFileStorage(storageKey, directory, dsync, true); // Paper - rewrite chunk system; async chunk IO & Attempt to recalculate regionfile header if it is corrupt - } - - public boolean isOldChunkAround(ChunkPos chunkPos, int checkRadius) { -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java -index a23dc2f8f4475de1ee35bf18a7a8a53233ccac12..226af44fd469053451a0403a95ffb446face9530 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java -@@ -9,6 +9,27 @@ import java.util.BitSet; - public class RegionBitmap { - private final BitSet used = new BitSet(); - -+ // Paper start - Attempt to recalculate regionfile header if it is corrupt -+ public final void copyFrom(RegionBitmap other) { -+ BitSet thisBitset = this.used; -+ BitSet otherBitset = other.used; -+ -+ 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.used; -+ int firstSet = bitset.nextSetBit(from); -+ if (firstSet > 0 && firstSet < (from + length)) { -+ return false; -+ } -+ bitset.set(from, from + length); -+ return true; -+ } -+ // Paper end - Attempt to recalculate regionfile header if it is corrupt -+ - public void force(int start, int size) { - this.used.set(start, start + size); - } -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -index cf43daa019b239464401784938d01af83f9da47c..1362a47943cf1a51a185a15094b1f74c94bf40ef 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -@@ -52,6 +52,354 @@ public class RegionFile implements AutoCloseable { - @VisibleForTesting - protected final RegionBitmap usedSectors; - public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(); // Paper -+ // Paper start - Attempt to recalculate regionfile header if it is corrupt -+ 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 CompoundTag OVERSIZED_COMPOUND = new CompoundTag(); -+ -+ private CompoundTag 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.file.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; -+ } -+ -+ RegionFileVersion compression = RegionFileVersion.fromId(compressionType); -+ if (compression == null) { -+ return null; -+ } -+ -+ InputStream input = compression.wrap(new ByteArrayInputStream(chunkData.array(), chunkData.position(), chunkDataLength - chunkData.position())); -+ -+ return NbtIo.read(new DataInputStream(input)); -+ } catch (Exception ex) { -+ return null; -+ } -+ } -+ -+ private int getLength(long sector) throws IOException { -+ ByteBuffer length = ByteBuffer.allocate(4); -+ if (4 != this.file.read(length, sector * 4096L)) { -+ return -1; -+ } -+ -+ return length.getInt(0); -+ } -+ -+ private void backupRegionFile() { -+ Path backup = this.path.getParent().resolve(this.path.getFileName() + "." + new java.util.Random().nextLong() + ".backup"); -+ this.backupRegionFile(backup); -+ } -+ -+ private void backupRegionFile(Path to) { -+ try { -+ this.file.force(true); -+ LOGGER.warn("Backing up regionfile \"" + this.path.toAbsolutePath() + "\" to " + to.toAbsolutePath()); -+ java.nio.file.Files.copy(this.path, to, java.nio.file.StandardCopyOption.COPY_ATTRIBUTES); -+ LOGGER.warn("Backed up the regionfile to " + to.toAbsolutePath()); -+ } catch (IOException ex) { -+ LOGGER.error("Failed to backup to " + to.toAbsolutePath(), ex); -+ } -+ } -+ -+ private static boolean inSameRegionfile(ChunkPos first, ChunkPos second) { -+ return (first.x & ~31) == (second.x & ~31) && (first.z & ~31) == (second.z & ~31); -+ } -+ -+ // note: only call for CHUNK regionfiles -+ boolean recalculateHeader() throws IOException { -+ if (!this.canRecalcHeader) { -+ return false; -+ } -+ ChunkPos ourLowerLeftPosition = RegionFileStorage.getRegionFileCoordinates(this.path); -+ if (ourLowerLeftPosition == null) { -+ LOGGER.error("Unable to get chunk location of regionfile " + this.path.toAbsolutePath() + ", cannot recover header"); -+ return false; -+ } -+ synchronized (this) { -+ LOGGER.warn("Corrupt regionfile header detected! Attempting to re-calculate header offsets for regionfile " + this.path.toAbsolutePath(), new Throwable()); -+ -+ // try to backup file so maybe it could be sent to us for further investigation -+ -+ this.backupRegionFile(); -+ CompoundTag[] compounds = new CompoundTag[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.file.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); -+ CompoundTag compound = this.attemptRead(i, chunkDataLength, fileLength); -+ if (compound == null || compound == OVERSIZED_COMPOUND) { -+ continue; -+ } -+ -+ ChunkPos chunkPos = ChunkSerializer.getChunkCoordinate(compound); -+ if (!inSameRegionfile(ourLowerLeftPosition, chunkPos)) { -+ LOGGER.error("Ignoring absolute chunk " + chunkPos + " in regionfile as it is not contained in the bounds of the regionfile '" + this.path.toAbsolutePath() + "'. It should be in regionfile (" + (chunkPos.x >> 5) + "," + (chunkPos.z >> 5) + ")"); -+ continue; -+ } -+ int location = (chunkPos.x & 31) | ((chunkPos.z & 31) << 5); -+ -+ CompoundTag otherCompound = compounds[location]; -+ -+ if (otherCompound != null && ChunkSerializer.getLastWorldSaveTime(otherCompound) > ChunkSerializer.getLastWorldSaveTime(compound)) { -+ continue; // don't overwrite newer data. -+ } -+ -+ // aikar oversized? -+ Path aikarOversizedFile = this.getOversizedFile(chunkPos.x, chunkPos.z); -+ boolean isAikarOversized = false; -+ if (Files.exists(aikarOversizedFile)) { -+ try { -+ CompoundTag aikarOversizedCompound = this.getOversizedData(chunkPos.x, chunkPos.z); -+ if (ChunkSerializer.getLastWorldSaveTime(compound) == ChunkSerializer.getLastWorldSaveTime(aikarOversizedCompound)) { -+ // best we got for an id. hope it's good enough -+ isAikarOversized = true; -+ } -+ } catch (Exception ex) { -+ LOGGER.error("Failed to read aikar oversized data for absolute chunk (" + chunkPos.x + "," + chunkPos.z + ") in regionfile " + this.path.toAbsolutePath() + ", 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.externalFileDir; -+ Path[] regionFiles = Files.list(containingFolder).toArray(Path[]::new); -+ boolean[] oversized = new boolean[32 * 32]; -+ RegionFileVersion[] oversizedCompressionTypes = new RegionFileVersion[32 * 32]; -+ -+ if (regionFiles != null) { -+ 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 (Path regionFile : regionFiles) { -+ ChunkPos 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); -+ } catch (Exception ex) { -+ LOGGER.error("Failed to read oversized chunk data in file " + regionFile.toAbsolutePath() + ", data will be lost", ex); -+ continue; -+ } -+ -+ CompoundTag compound = null; -+ -+ // We do not know the compression type, as it's stored in the regionfile. So we need to try all of them -+ RegionFileVersion compression = null; -+ for (RegionFileVersion compressionType : RegionFileVersion.VERSIONS.values()) { -+ try { -+ DataInputStream in = new DataInputStream(compressionType.wrap(new ByteArrayInputStream(chunkData))); // typical java -+ compound = NbtIo.read((java.io.DataInput)in); -+ compression = compressionType; -+ break; // reaches here iff readNBT does not throw -+ } catch (Exception ex) { -+ continue; -+ } -+ } -+ -+ if (compound == null) { -+ LOGGER.error("Failed to read oversized chunk data in file " + regionFile.toAbsolutePath() + ", it's corrupt. Its data will be lost"); -+ continue; -+ } -+ -+ if (!ChunkSerializer.getChunkCoordinate(compound).equals(oversizedCoords)) { -+ LOGGER.error("Can't use oversized chunk stored in " + regionFile.toAbsolutePath() + ", got absolute chunkpos: " + ChunkSerializer.getChunkCoordinate(compound) + ", expected " + oversizedCoords); -+ continue; -+ } -+ -+ if (compounds[location] == null || ChunkSerializer.getLastWorldSaveTime(compound) > ChunkSerializer.getLastWorldSaveTime(compounds[location])) { -+ oversized[location] = true; -+ oversizedCompressionTypes[location] = compression; -+ } -+ } -+ } -+ -+ // now we need to calculate a new offset header -+ -+ int[] calculatedOffsets = new int[32 * 32]; -+ RegionBitmap newSectorAllocations = new RegionBitmap(); -+ newSectorAllocations.force(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 { -+ LOGGER.error("Failed to allocate space for local chunk (overlapping data??) at (" + chunkX + "," + chunkZ + ") in regionfile " + this.path.toAbsolutePath() + ", 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.allocate(1); -+ int sectorLength = 1; -+ -+ try { -+ this.file.write(this.createExternalStub(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); -+ LOGGER.error("Failed to write new oversized chunk data holder, local chunk at (" + chunkX + "," + chunkZ + ") in regionfile " + this.path.toAbsolutePath() + " 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) { -+ LOGGER.error("Failed to write aikar oversized chunk meta, all aikar style oversized chunk data will be lost for regionfile " + this.path.toAbsolutePath(), ex); -+ Files.deleteIfExists(this.getOversizedMetaFile()); -+ } -+ } else { -+ Files.deleteIfExists(this.getOversizedMetaFile()); -+ } -+ -+ this.usedSectors.copyFrom(newSectorAllocations); -+ -+ // before we overwrite the old sectors, print a summary of the chunks that got changed. -+ -+ LOGGER.info("Starting summary of changes for regionfile " + this.path.toAbsolutePath()); -+ -+ for (int chunkX = 0; chunkX < 32; ++chunkX) { -+ for (int chunkZ = 0; chunkZ < 32; ++chunkZ) { -+ int location = chunkX | (chunkZ << 5); -+ -+ int oldOffset = this.offsets.get(location); -+ int newOffset = calculatedOffsets[location]; -+ -+ if (oldOffset == newOffset) { -+ continue; -+ } -+ -+ this.offsets.put(location, newOffset); // overwrite incorrect offset -+ -+ if (oldOffset == 0) { -+ // found lost data -+ LOGGER.info("Found missing data for local chunk (" + chunkX + "," + chunkZ + ") in regionfile " + this.path.toAbsolutePath()); -+ } else if (newOffset == 0) { -+ LOGGER.warn("Data for local chunk (" + chunkX + "," + chunkZ + ") could not be recovered in regionfile " + this.path.toAbsolutePath() + ", it will be regenerated"); -+ } else { -+ LOGGER.info("Local chunk (" + chunkX + "," + chunkZ + ") changed to point to newer data or correct chunk in regionfile " + this.path.toAbsolutePath()); -+ } -+ } -+ } -+ -+ LOGGER.info("End of change summary for regionfile " + this.path.toAbsolutePath()); -+ -+ // simply destroy the timestamp header, it's not used -+ -+ for (int i = 0; i < 32 * 32; ++i) { -+ this.timestamps.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.flush(); -+ this.file.force(true); // try to ensure it goes through... -+ LOGGER.info("Successfully wrote new header to disk for regionfile " + this.path.toAbsolutePath()); -+ } catch (IOException ex) { -+ LOGGER.error("Failed to write new header to disk for regionfile " + this.path.toAbsolutePath(), ex); -+ } -+ } -+ -+ return true; -+ } -+ -+ final boolean canRecalcHeader; // final forces compile fail on new constructor -+ // Paper end - Attempt to recalculate regionfile header if it is corrupt - // Paper start - Cache chunk status - private final net.minecraft.world.level.chunk.status.ChunkStatus[] statuses = new net.minecraft.world.level.chunk.status.ChunkStatus[32 * 32]; - -@@ -79,8 +427,18 @@ public class RegionFile implements AutoCloseable { - public RegionFile(RegionStorageInfo storageKey, Path directory, Path path, boolean dsync) throws IOException { - this(storageKey, directory, path, RegionFileVersion.getCompressionFormat(), dsync); // Paper - Configurable region compression format - } -+ // Paper start - add can recalc flag -+ public RegionFile(RegionStorageInfo storageKey, Path directory, Path path, boolean dsync, boolean canRecalcHeader) throws IOException { -+ this(storageKey, directory, path, RegionFileVersion.getCompressionFormat(), dsync, canRecalcHeader); -+ } - - public RegionFile(RegionStorageInfo storageKey, Path path, Path directory, RegionFileVersion compressionFormat, boolean dsync) throws IOException { -+ this(storageKey, path, directory, compressionFormat, dsync, true); -+ } -+ -+ public RegionFile(RegionStorageInfo storageKey, Path path, Path directory, RegionFileVersion compressionFormat, boolean dsync, boolean canRecalcHeader) throws IOException { -+ this.canRecalcHeader = canRecalcHeader; -+ // Paper end - add can recalc flag - this.header = ByteBuffer.allocateDirect(8192); - this.usedSectors = new RegionBitmap(); - this.info = storageKey; -@@ -110,14 +468,16 @@ public class RegionFile implements AutoCloseable { - RegionFile.LOGGER.warn("Region file {} has truncated header: {}", path, i); - } - -- long j = Files.size(path); -+ final long j = Files.size(path); final long regionFileSize = j; // Paper - recalculate header on header corruption - -- for (int k = 0; k < 1024; ++k) { -- int l = this.offsets.get(k); -+ boolean needsHeaderRecalc = false; // Paper - recalculate header on header corruption -+ boolean hasBackedUp = false; // Paper - recalculate header on header corruption -+ for (int k = 0; k < 1024; ++k) { final int headerLocation = k; // Paper - we expect this to be the header location -+ final int l = this.offsets.get(k); - - if (l != 0) { -- int i1 = RegionFile.getSectorNumber(l); -- int j1 = RegionFile.getNumSectors(l); -+ final int i1 = RegionFile.getSectorNumber(l); final int offset = i1; // Paper - we expect this to be offset in file in sectors -+ int j1 = RegionFile.getNumSectors(l); final int sectorLength; // Paper - 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 -@@ -126,21 +486,66 @@ public class RegionFile implements AutoCloseable { - j1 = (realLen.getInt(0) + 4) / 4096 + 1; - } - // Spigot end -+ sectorLength = j1; // Paper - diff on change, we expect this to be sector length of region - - if (i1 < 2) { - RegionFile.LOGGER.warn("Region file {} has invalid sector at index: {}; sector {} overlaps with header", new Object[]{path, k, i1}); -- this.offsets.put(k, 0); -+ //this.offsets.put(k, 0); // Paper - we catch this, but need it in the header for the summary change - } else if (j1 == 0) { - RegionFile.LOGGER.warn("Region file {} has an invalid sector at index: {}; size has to be > 0", path, k); -- this.offsets.put(k, 0); -+ //this.offsets.put(k, 0); // Paper - 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", new Object[]{path, k, i1}); -- this.offsets.put(k, 0); -+ //this.offsets.put(k, 0); // Paper - we catch this, but need it in the header for the summary change - } else { -- this.usedSectors.force(i1, j1); -+ //this.usedSectors.force(i1, j1); // Paper - move this down so we can check if it fails to allocate -+ } -+ // Paper start - recalculate header on header corruption -+ if (offset < 2 || sectorLength <= 0 || ((long)offset * 4096L) > regionFileSize) { -+ if (canRecalcHeader) { -+ LOGGER.error("Detected invalid header for regionfile " + this.path.toAbsolutePath() + "! Recalculating header..."); -+ needsHeaderRecalc = true; -+ break; -+ } else { -+ // location = chunkX | (chunkZ << 5); -+ LOGGER.error("Detected invalid header for regionfile " + this.path.toAbsolutePath() + -+ "! Cannot recalculate, removing local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") from header"); -+ if (!hasBackedUp) { -+ hasBackedUp = true; -+ this.backupRegionFile(); -+ } -+ this.timestamps.put(headerLocation, 0); // be consistent, delete the timestamp too -+ this.offsets.put(headerLocation, 0); // delete the entry from header -+ continue; -+ } -+ } -+ boolean failedToAllocate = !this.usedSectors.tryAllocate(offset, sectorLength); -+ if (failedToAllocate) { -+ LOGGER.error("Overlapping allocation by local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") in regionfile " + this.path.toAbsolutePath()); - } -+ if (failedToAllocate & !canRecalcHeader) { -+ // location = chunkX | (chunkZ << 5); -+ LOGGER.error("Detected invalid header for regionfile " + this.path.toAbsolutePath() + -+ "! Cannot recalculate, removing local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") from header"); -+ if (!hasBackedUp) { -+ hasBackedUp = true; -+ this.backupRegionFile(); -+ } -+ this.timestamps.put(headerLocation, 0); // be consistent, delete the timestamp too -+ this.offsets.put(headerLocation, 0); // delete the entry from header -+ continue; -+ } -+ needsHeaderRecalc |= failedToAllocate; -+ // Paper end - recalculate header on header corruption - } - } -+ // Paper 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 -+ LOGGER.error("Recalculating regionfile " + this.path.toAbsolutePath() + ", header gave erroneous offsets & locations"); -+ this.recalculateHeader(); -+ } -+ // Paper end - } - - } -@@ -151,11 +556,36 @@ public class RegionFile implements AutoCloseable { - } - - private Path getExternalChunkPath(ChunkPos chunkPos) { -- String s = "c." + chunkPos.x + "." + chunkPos.z + ".mcc"; -+ String s = "c." + chunkPos.x + "." + chunkPos.z + ".mcc"; // Paper - diff on change - - return this.externalFileDir.resolve(s); - } - -+ // Paper start -+ private static ChunkPos getOversizedChunkPair(Path file) { -+ String fileName = file.getFileName().toString(); -+ -+ 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 ChunkPos(x, z); -+ } catch (NumberFormatException ex) { -+ return null; -+ } -+ } -+ // Paper end -+ - @Nullable - public synchronized DataInputStream getChunkDataInputStream(ChunkPos pos) throws IOException { - int i = this.getOffset(pos); -@@ -179,6 +609,11 @@ public class RegionFile implements AutoCloseable { - ((java.nio.Buffer) bytebuffer).flip(); // CraftBukkit - decompile error - if (bytebuffer.remaining() < 5) { - RegionFile.LOGGER.error("Chunk {} header is truncated: expected {} but read {}", new Object[]{pos, l, bytebuffer.remaining()}); -+ // Paper start - recalculate header on regionfile corruption -+ if (this.canRecalcHeader && this.recalculateHeader()) { -+ return this.getChunkDataInputStream(pos); -+ } -+ // Paper end - recalculate header on regionfile corruption - return null; - } else { - int i1 = bytebuffer.getInt(); -@@ -186,6 +621,11 @@ public class RegionFile implements AutoCloseable { - - if (i1 == 0) { - RegionFile.LOGGER.warn("Chunk {} is allocated, but stream is missing", pos); -+ // Paper start - recalculate header on regionfile corruption -+ if (this.canRecalcHeader && this.recalculateHeader()) { -+ return this.getChunkDataInputStream(pos); -+ } -+ // Paper end - recalculate header on regionfile corruption - return null; - } else { - int j1 = i1 - 1; -@@ -193,18 +633,45 @@ public class RegionFile implements AutoCloseable { - if (RegionFile.isExternalStreamChunk(b0)) { - if (j1 != 0) { - RegionFile.LOGGER.warn("Chunk has both internal and external streams"); -+ // Paper start - recalculate header on regionfile corruption -+ if (this.canRecalcHeader && this.recalculateHeader()) { -+ return this.getChunkDataInputStream(pos); -+ } -+ // Paper end - recalculate header on regionfile corruption - } - -- return this.createExternalChunkInputStream(pos, RegionFile.getExternalChunkVersion(b0)); -+ // Paper start - recalculate header on regionfile corruption -+ final DataInputStream ret = this.createExternalChunkInputStream(pos, RegionFile.getExternalChunkVersion(b0)); -+ if (ret == null && this.canRecalcHeader && this.recalculateHeader()) { -+ return this.getChunkDataInputStream(pos); -+ } -+ return ret; -+ // Paper end - recalculate header on regionfile corruption - } else if (j1 > bytebuffer.remaining()) { - RegionFile.LOGGER.error("Chunk {} stream is truncated: expected {} but read {}", new Object[]{pos, j1, bytebuffer.remaining()}); -+ // Paper start - recalculate header on regionfile corruption -+ if (this.canRecalcHeader && this.recalculateHeader()) { -+ return this.getChunkDataInputStream(pos); -+ } -+ // Paper end - recalculate header on regionfile corruption - return null; - } else if (j1 < 0) { - RegionFile.LOGGER.error("Declared size {} of chunk {} is negative", i1, pos); -+ // Paper start - recalculate header on regionfile corruption -+ if (this.canRecalcHeader && this.recalculateHeader()) { -+ return this.getChunkDataInputStream(pos); -+ } -+ // Paper end - recalculate header on regionfile corruption - return null; - } else { - JvmProfiler.INSTANCE.onRegionFileRead(this.info, pos, this.version, j1); -- return this.createChunkInputStream(pos, b0, RegionFile.createStream(bytebuffer, j1)); -+ // Paper start - recalculate header on regionfile corruption -+ final DataInputStream ret = this.createChunkInputStream(pos, b0, RegionFile.createStream(bytebuffer, j1)); -+ if (ret == null && this.canRecalcHeader && this.recalculateHeader()) { -+ return this.getChunkDataInputStream(pos); -+ } -+ return ret; -+ // Paper end - recalculate header on regionfile corruption - } - } - } -@@ -390,10 +857,15 @@ public class RegionFile implements AutoCloseable { - } - - private ByteBuffer createExternalStub() { -+ // Paper start - add version param -+ return this.createExternalStub(this.version); -+ } -+ private ByteBuffer createExternalStub(RegionFileVersion version) { -+ // Paper end - add version param - ByteBuffer bytebuffer = ByteBuffer.allocate(5); - - bytebuffer.putInt(1); -- bytebuffer.put((byte) (this.version.getId() | 128)); -+ bytebuffer.put((byte) (version.getId() | 128)); // Paper - replace with version param - ((java.nio.Buffer) bytebuffer).flip(); // CraftBukkit - decompile error - return bytebuffer; - } -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -index c33640859aab837c85f3e860fe2241a0e78bb09a..249705ec1b8b692ef1d7fec34a04918afe6486bc 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -@@ -25,6 +25,7 @@ public class RegionFileStorage implements AutoCloseable { - private final RegionStorageInfo info; - private final Path folder; - private final boolean sync; -+ private final boolean isChunkData; // Paper - - // Paper start - cache regionfile does not exist state - static final int MAX_NON_EXISTING_CACHE = 1024 * 64; -@@ -56,6 +57,12 @@ public class RegionFileStorage implements AutoCloseable { - // Paper end - cache regionfile does not exist state - - protected RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync) { // Paper - protected constructor -+ // Paper start - add isChunkData param -+ this(storageKey, directory, dsync, false); -+ } -+ RegionFileStorage(RegionStorageInfo storageKey, Path directory, boolean dsync, boolean isChunkData) { -+ this.isChunkData = isChunkData; -+ // Paper end - add isChunkData param - this.folder = directory; - this.sync = dsync; - this.info = storageKey; -@@ -101,7 +108,7 @@ public class RegionFileStorage implements AutoCloseable { - // Paper - only create directory if not existing only - moved down - Path path = this.folder; - int j = chunkcoordintpair.getRegionX(); -- Path path1 = path.resolve("r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca"); -+ Path path1 = path.resolve("r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca"); // Paper - diff on change - if (existingOnly && !java.nio.file.Files.exists(path1)) { // Paper start - cache regionfile does not exist state - this.markNonExisting(regionPos); - return null; // CraftBukkit -@@ -110,7 +117,7 @@ public class RegionFileStorage implements AutoCloseable { - } - // Paper end - cache regionfile does not exist state - FileUtil.createDirectoriesSafe(this.folder); // Paper - only create directory if not existing only - moved from above -- RegionFile regionfile1 = new RegionFile(this.info, path1, this.folder, this.sync); -+ RegionFile regionfile1 = new RegionFile(this.info, path1, this.folder, this.sync, this.isChunkData); // Paper - allow for chunk regionfiles to regen header - - this.regionCache.putAndMoveToFirst(i, regionfile1); - // Paper start -@@ -167,6 +174,13 @@ public class RegionFileStorage implements AutoCloseable { - if (regionfile == null) { - return null; - } -+ // Paper start - Add regionfile parameter -+ return this.read(pos, regionfile); -+ } -+ public CompoundTag read(ChunkPos pos, RegionFile regionfile) 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 -+ // Paper end - // CraftBukkit end - try { // Paper - DataInputStream datainputstream = regionfile.getChunkDataInputStream(pos); -@@ -183,6 +197,20 @@ public class RegionFileStorage implements AutoCloseable { - try { - if (datainputstream != null) { - nbttagcompound = NbtIo.read((DataInput) datainputstream); -+ // Paper start - recover from corrupt regionfile header -+ if (this.isChunkData) { -+ ChunkPos chunkPos = ChunkSerializer.getChunkCoordinate(nbttagcompound); -+ if (!chunkPos.equals(pos)) { -+ net.minecraft.server.MinecraftServer.LOGGER.error("Attempting to read chunk data at " + pos + " but got chunk data for " + chunkPos + " instead! Attempting regionfile recalculation for regionfile " + regionfile.getPath().toAbsolutePath()); -+ if (regionfile.recalculateHeader()) { -+ regionfile.fileLock.lock(); // otherwise we will unlock twice and only lock once. -+ return this.read(pos, regionfile); -+ } -+ net.minecraft.server.MinecraftServer.LOGGER.error("Can't recalculate regionfile header, regenerating chunk " + pos + " for " + regionfile.getPath().toAbsolutePath()); -+ return null; -+ } -+ } -+ // Paper end - recover from corrupt regionfile header - break label43; - } - -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java -index ef68b57ef1d8d7cb317c417569dd23a777fba4ad..f4a39f49b354c560d614483db1cd3dfc154e94b4 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java -@@ -21,7 +21,7 @@ import org.slf4j.Logger; - - public class RegionFileVersion { - private static final Logger LOGGER = LogUtils.getLogger(); -- private static final Int2ObjectMap VERSIONS = new Int2ObjectOpenHashMap<>(); -+ public static final Int2ObjectMap VERSIONS = new Int2ObjectOpenHashMap<>(); // Paper - private -> public - private static final Object2ObjectMap VERSIONS_BY_NAME = new Object2ObjectOpenHashMap<>(); - public static final RegionFileVersion VERSION_GZIP = register( - new RegionFileVersion( diff --git a/patches/server/1016-Use-Velocity-compression-and-cipher-natives.patch b/patches/server/1016-Use-Velocity-compression-and-cipher-natives.patch new file mode 100644 index 0000000000..c357a07cf3 --- /dev/null +++ b/patches/server/1016-Use-Velocity-compression-and-cipher-natives.patch @@ -0,0 +1,375 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Andrew Steinborn +Date: Mon, 26 Jul 2021 02:15:17 -0400 +Subject: [PATCH] Use Velocity compression and cipher natives + + +diff --git a/build.gradle.kts b/build.gradle.kts +index 41767422b521ff095592d799b14b2f36c3b5c5cb..aab4d1f15745bce838329ba0359e3f9ca70f2e1f 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -38,6 +38,11 @@ dependencies { + implementation("org.spongepowered:configurate-yaml:4.2.0-SNAPSHOT") // Paper - config files + implementation("commons-lang:commons-lang:2.6") + runtimeOnly("com.lmax:disruptor:3.4.4") // Paper ++ // Paper start - Use Velocity cipher ++ implementation("com.velocitypowered:velocity-native:3.1.2-SNAPSHOT") { ++ isTransitive = false ++ } ++ // Paper end - Use Velocity cipher + + runtimeOnly("org.apache.maven:maven-resolver-provider:3.9.6") + runtimeOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.9.18") +diff --git a/src/main/java/net/minecraft/network/CipherDecoder.java b/src/main/java/net/minecraft/network/CipherDecoder.java +index 778beb445eac5769b9e4e07b4d1294c50ae2602b..7b895b6a626da6297f07582e96d98ecfb6c8c951 100644 +--- a/src/main/java/net/minecraft/network/CipherDecoder.java ++++ b/src/main/java/net/minecraft/network/CipherDecoder.java +@@ -7,13 +7,29 @@ import java.util.List; + import javax.crypto.Cipher; + + public class CipherDecoder extends MessageToMessageDecoder { +- private final CipherBase cipher; ++ private final com.velocitypowered.natives.encryption.VelocityCipher cipher; // Paper - Use Velocity cipher + +- public CipherDecoder(Cipher cipher) { +- this.cipher = new CipherBase(cipher); ++ public CipherDecoder(com.velocitypowered.natives.encryption.VelocityCipher cipher) { // Paper - Use Velocity cipher ++ this.cipher = cipher; // Paper - Use Velocity cipher + } + + protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List list) throws Exception { +- list.add(this.cipher.decipher(channelHandlerContext, byteBuf)); ++ // Paper start - Use Velocity cipher ++ ByteBuf compatible = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(channelHandlerContext.alloc(), cipher, byteBuf); ++ try { ++ cipher.process(compatible); ++ list.add(compatible); ++ } catch (Exception e) { ++ compatible.release(); // compatible will never be used if we throw an exception ++ throw e; ++ } ++ // Paper end - Use Velocity cipher + } ++ ++ // Paper start - Use Velocity cipher ++ @Override ++ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { ++ cipher.close(); ++ } ++ // Paper end - Use Velocity cipher + } +diff --git a/src/main/java/net/minecraft/network/CipherEncoder.java b/src/main/java/net/minecraft/network/CipherEncoder.java +index 0f3d502a9680006bcdcd7d272240a2e5c3b46790..ffa1c48585fbbc1d30826d435043527f6183a3ee 100644 +--- a/src/main/java/net/minecraft/network/CipherEncoder.java ++++ b/src/main/java/net/minecraft/network/CipherEncoder.java +@@ -4,15 +4,32 @@ import io.netty.buffer.ByteBuf; + import io.netty.channel.ChannelHandlerContext; + import io.netty.handler.codec.MessageToByteEncoder; + import javax.crypto.Cipher; ++import java.util.List; + +-public class CipherEncoder extends MessageToByteEncoder { +- private final CipherBase cipher; ++public class CipherEncoder extends io.netty.handler.codec.MessageToMessageEncoder { // Paper - Use Velocity cipher; change superclass ++ private final com.velocitypowered.natives.encryption.VelocityCipher cipher; // Paper - Use Velocity cipher + +- public CipherEncoder(Cipher cipher) { +- this.cipher = new CipherBase(cipher); ++ public CipherEncoder(com.velocitypowered.natives.encryption.VelocityCipher cipher) { // Paper - Use Velocity cipher ++ this.cipher = cipher; // Paper - Use Velocity cipher + } + +- protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, ByteBuf byteBuf2) throws Exception { +- this.cipher.encipher(byteBuf, byteBuf2); ++ protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List list) throws Exception { ++ // Paper start - Use Velocity cipher ++ ByteBuf compatible = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(channelHandlerContext.alloc(), cipher, byteBuf); ++ try { ++ cipher.process(compatible); ++ list.add(compatible); ++ } catch (Exception e) { ++ compatible.release(); // compatible will never be used if we throw an exception ++ throw e; ++ } ++ // Paper end - Use Velocity cipher + } ++ ++ // Paper start - Use Velocity cipher ++ @Override ++ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { ++ cipher.close(); ++ } ++ // Paper end - Use Velocity cipher + } +diff --git a/src/main/java/net/minecraft/network/CompressionDecoder.java b/src/main/java/net/minecraft/network/CompressionDecoder.java +index 5927c928b27f64bd3973e95d5867b59f67476d41..ba3619b03ea73e4ea55ae2eb79d3b6cd1f48a191 100644 +--- a/src/main/java/net/minecraft/network/CompressionDecoder.java ++++ b/src/main/java/net/minecraft/network/CompressionDecoder.java +@@ -13,13 +13,20 @@ public class CompressionDecoder extends ByteToMessageDecoder { + public static final int MAXIMUM_COMPRESSED_LENGTH = 2097152; + public static final int MAXIMUM_UNCOMPRESSED_LENGTH = 8388608; + private final Inflater inflater; ++ private com.velocitypowered.natives.compression.VelocityCompressor compressor; // Paper - Use Velocity cipher + private int threshold; + private boolean validateDecompressed; + ++ // Paper start - Use Velocity cipher + public CompressionDecoder(int compressionThreshold, boolean rejectsBadPackets) { ++ this(null, compressionThreshold, rejectsBadPackets); ++ } ++ public CompressionDecoder(com.velocitypowered.natives.compression.VelocityCompressor compressor, int compressionThreshold, boolean rejectsBadPackets) { + this.threshold = compressionThreshold; + this.validateDecompressed = rejectsBadPackets; +- this.inflater = new Inflater(); ++ this.inflater = compressor == null ? new Inflater() : null; ++ this.compressor = compressor; ++ // Paper end - Use Velocity cipher + } + + protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List list) throws Exception { +@@ -38,14 +45,42 @@ public class CompressionDecoder extends ByteToMessageDecoder { + } + } + ++ if (inflater != null) { // Paper - Use Velocity cipher; fallback to vanilla inflater + this.setupInflaterInput(byteBuf); + ByteBuf byteBuf2 = this.inflate(channelHandlerContext, i); + this.inflater.reset(); + list.add(byteBuf2); ++ return; // Paper - Use Velocity cipher ++ } // Paper - use velocity compression ++ ++ // Paper start - Use Velocity cipher ++ int claimedUncompressedSize = i; // OBFHELPER ++ ByteBuf compatibleIn = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(channelHandlerContext.alloc(), this.compressor, byteBuf); ++ ByteBuf uncompressed = com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer(channelHandlerContext.alloc(), this.compressor, claimedUncompressedSize); ++ try { ++ this.compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize); ++ list.add(uncompressed); ++ byteBuf.clear(); ++ } catch (Exception e) { ++ uncompressed.release(); ++ throw e; ++ } finally { ++ compatibleIn.release(); ++ } ++ // Paper end - Use Velocity cipher + } + } + } + ++ // Paper start - Use Velocity cipher ++ @Override ++ public void handlerRemoved0(ChannelHandlerContext ctx) throws Exception { ++ if (this.compressor != null) { ++ this.compressor.close(); ++ } ++ } ++ // Paper end - Use Velocity cipher ++ + private void setupInflaterInput(ByteBuf buf) { + ByteBuffer byteBuffer; + if (buf.nioBufferCount() > 0) { +@@ -82,7 +117,7 @@ public class CompressionDecoder extends ByteToMessageDecoder { + } + } + +- public void setThreshold(int compressionThreshold, boolean rejectsBadPackets) { ++ public void setThreshold(com.velocitypowered.natives.compression.VelocityCompressor compressor, int compressionThreshold, boolean rejectsBadPackets) { // Paper - Use Velocity cipher + this.threshold = compressionThreshold; + this.validateDecompressed = rejectsBadPackets; + } +diff --git a/src/main/java/net/minecraft/network/CompressionEncoder.java b/src/main/java/net/minecraft/network/CompressionEncoder.java +index 34a80d2f34555663ab1b394972957089214cb337..ec30c291188ac3bba7f1c3bc397576d1edb0a57f 100644 +--- a/src/main/java/net/minecraft/network/CompressionEncoder.java ++++ b/src/main/java/net/minecraft/network/CompressionEncoder.java +@@ -6,16 +6,31 @@ import io.netty.handler.codec.MessageToByteEncoder; + import java.util.zip.Deflater; + + public class CompressionEncoder extends MessageToByteEncoder { +- private final byte[] encodeBuf = new byte[8192]; ++ @javax.annotation.Nullable private final byte[] encodeBuf; // Paper - Use Velocity cipher ++ @javax.annotation.Nullable // Paper - Use Velocity cipher + private final Deflater deflater; ++ @javax.annotation.Nullable // Paper - Use Velocity cipher ++ private final com.velocitypowered.natives.compression.VelocityCompressor compressor; // Paper - Use Velocity cipher + private int threshold; + ++ // Paper start - Use Velocity cipher + public CompressionEncoder(int compressionThreshold) { ++ this(null, compressionThreshold); ++ } ++ public CompressionEncoder(@javax.annotation.Nullable com.velocitypowered.natives.compression.VelocityCompressor compressor, int compressionThreshold) { + this.threshold = compressionThreshold; +- this.deflater = new Deflater(); ++ if (compressor == null) { ++ this.encodeBuf = new byte[8192]; ++ this.deflater = new Deflater(); ++ } else { ++ this.encodeBuf = null; ++ this.deflater = null; ++ } ++ this.compressor = compressor; ++ // Paper end - Use Velocity cipher + } + +- protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, ByteBuf byteBuf2) { ++ protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, ByteBuf byteBuf2) throws Exception { // Paper - Use Velocity cipher + int i = byteBuf.readableBytes(); + if (i > 8388608) { + throw new IllegalArgumentException("Packet too big (is " + i + ", should be less than 8388608)"); +@@ -24,6 +39,7 @@ public class CompressionEncoder extends MessageToByteEncoder { + VarInt.write(byteBuf2, 0); + byteBuf2.writeBytes(byteBuf); + } else { ++ if (this.deflater != null) { // Paper - Use Velocity cipher + byte[] bs = new byte[i]; + byteBuf.readBytes(bs); + VarInt.write(byteBuf2, bs.length); +@@ -36,10 +52,47 @@ public class CompressionEncoder extends MessageToByteEncoder { + } + + this.deflater.reset(); ++ // Paper start - Use Velocity cipher ++ return; ++ } ++ ++ VarInt.write(byteBuf2, i); ++ final ByteBuf compatibleIn = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(channelHandlerContext.alloc(), this.compressor, byteBuf); ++ try { ++ this.compressor.deflate(compatibleIn, byteBuf2); ++ } finally { ++ compatibleIn.release(); ++ } + } + } + } + ++ @Override ++ protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect) throws Exception{ ++ if (this.compressor != null) { ++ // We allocate bytes to be compressed plus 1 byte. This covers two cases: ++ // ++ // - Compression ++ // According to https://github.com/ebiggers/libdeflate/blob/master/libdeflate.h#L103, ++ // if the data compresses well (and we do not have some pathological case) then the maximum ++ // size the compressed size will ever be is the input size minus one. ++ // - Uncompressed ++ // This is fairly obvious - we will then have one more than the uncompressed size. ++ final int initialBufferSize = msg.readableBytes() + 1; ++ return com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer(ctx.alloc(), this.compressor, initialBufferSize); ++ } ++ ++ return super.allocateBuffer(ctx, msg, preferDirect); ++ } ++ ++ @Override ++ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { ++ if (this.compressor != null) { ++ this.compressor.close(); ++ // Paper end - Use Velocity cipher ++ } ++ } ++ + public int getThreshold() { + return this.threshold; + } +diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java +index f94458d4270042062b9ae18931cc8179ef1fb420..63066dfde00569833546da01cd580434f8be6593 100644 +--- a/src/main/java/net/minecraft/network/Connection.java ++++ b/src/main/java/net/minecraft/network/Connection.java +@@ -763,11 +763,28 @@ public class Connection extends SimpleChannelInboundHandler> { + return networkmanager; + } + +- public void setEncryptionKey(Cipher decryptionCipher, Cipher encryptionCipher) { +- this.encrypted = true; +- this.channel.pipeline().addBefore("splitter", "decrypt", new CipherDecoder(decryptionCipher)); +- this.channel.pipeline().addBefore("prepender", "encrypt", new CipherEncoder(encryptionCipher)); ++ // Paper start - Use Velocity cipher ++// public void setEncryptionKey(Cipher decryptionCipher, Cipher encryptionCipher) { ++// this.encrypted = true; ++// this.channel.pipeline().addBefore("splitter", "decrypt", new CipherDecoder(decryptionCipher)); ++// this.channel.pipeline().addBefore("prepender", "encrypt", new CipherEncoder(encryptionCipher)); ++// } ++ ++ public void setupEncryption(javax.crypto.SecretKey key) throws net.minecraft.util.CryptException { ++ if (!this.encrypted) { ++ try { ++ com.velocitypowered.natives.encryption.VelocityCipher decryption = com.velocitypowered.natives.util.Natives.cipher.get().forDecryption(key); ++ com.velocitypowered.natives.encryption.VelocityCipher encryption = com.velocitypowered.natives.util.Natives.cipher.get().forEncryption(key); ++ ++ this.encrypted = true; ++ this.channel.pipeline().addBefore("splitter", "decrypt", new CipherDecoder(decryption)); ++ this.channel.pipeline().addBefore("prepender", "encrypt", new CipherEncoder(encryption)); ++ } catch (java.security.GeneralSecurityException e) { ++ throw new net.minecraft.util.CryptException(e); ++ } ++ } + } ++ // Paper end - Use Velocity cipher + + public boolean isEncrypted() { + return this.encrypted; +@@ -800,12 +817,13 @@ public class Connection extends SimpleChannelInboundHandler> { + + public void setupCompression(int compressionThreshold, boolean rejectsBadPackets) { + if (compressionThreshold >= 0) { ++ com.velocitypowered.natives.compression.VelocityCompressor compressor = com.velocitypowered.natives.util.Natives.compress.get().create(io.papermc.paper.configuration.GlobalConfiguration.get().misc.compressionLevel.or(-1)); // Paper - Use Velocity cipher + ChannelHandler channelhandler = this.channel.pipeline().get("decompress"); + + if (channelhandler instanceof CompressionDecoder) { + CompressionDecoder packetdecompressor = (CompressionDecoder) channelhandler; + +- packetdecompressor.setThreshold(compressionThreshold, rejectsBadPackets); ++ packetdecompressor.setThreshold(compressor, compressionThreshold, rejectsBadPackets); // Paper - Use Velocity cipher + } else { + this.channel.pipeline().addAfter("splitter", "decompress", new CompressionDecoder(compressionThreshold, rejectsBadPackets)); + } +@@ -816,7 +834,7 @@ public class Connection extends SimpleChannelInboundHandler> { + + packetcompressor.setThreshold(compressionThreshold); + } else { +- this.channel.pipeline().addAfter("prepender", "compress", new CompressionEncoder(compressionThreshold)); ++ this.channel.pipeline().addAfter("prepender", "compress", new CompressionEncoder(compressor, compressionThreshold)); // Paper - Use Velocity cipher + } + this.channel.pipeline().fireUserEventTriggered(io.papermc.paper.network.ConnectionEvent.COMPRESSION_THRESHOLD_SET); // Paper - Add Channel initialization listeners + } else { +diff --git a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java +index a08d9aa6e420f691795df9b627a9cd5b5c0112c5..52f537b7bfbdeaad9d17c0e88a1ed1c8925a833f 100644 +--- a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java ++++ b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java +@@ -106,6 +106,11 @@ public class ServerConnectionListener { + ServerConnectionListener.LOGGER.info("Using default channel type"); + } + ++ // Paper start - Use Velocity cipher ++ ServerConnectionListener.LOGGER.info("Paper: Using " + com.velocitypowered.natives.util.Natives.compress.getLoadedVariant() + " compression from Velocity."); ++ ServerConnectionListener.LOGGER.info("Paper: Using " + com.velocitypowered.natives.util.Natives.cipher.getLoadedVariant() + " cipher from Velocity."); ++ // Paper end - Use Velocity cipher ++ + this.channels.add(((ServerBootstrap) ((ServerBootstrap) (new ServerBootstrap()).channel(oclass)).childHandler(new ChannelInitializer() { + protected void initChannel(Channel channel) { + try { +diff --git a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +index 3333cb70744dc7ef8b181d332b63766aaaaad11f..765cf12c1558afd9686793b18388fc229c55d2d8 100644 +--- a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +@@ -274,12 +274,14 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + } + + SecretKey secretkey = packet.getSecretKey(privatekey); +- Cipher cipher = Crypt.getCipher(2, secretkey); +- Cipher cipher1 = Crypt.getCipher(1, secretkey); ++ // Paper start - Use Velocity cipher ++// Cipher cipher = Crypt.getCipher(2, secretkey); ++// Cipher cipher1 = Crypt.getCipher(1, secretkey); ++ // Paper end - Use Velocity cipher + + s = (new BigInteger(Crypt.digestData("", this.server.getKeyPair().getPublic(), secretkey))).toString(16); + this.state = ServerLoginPacketListenerImpl.State.AUTHENTICATING; +- this.connection.setEncryptionKey(cipher, cipher1); ++ this.connection.setupEncryption(secretkey); // Paper - Use Velocity cipher + } catch (CryptException cryptographyexception) { + throw new IllegalStateException("Protocol error", cryptographyexception); + } diff --git a/patches/server/1017-Detail-more-information-in-watchdog-dumps.patch b/patches/server/1017-Detail-more-information-in-watchdog-dumps.patch new file mode 100644 index 0000000000..7859dad6a7 --- /dev/null +++ b/patches/server/1017-Detail-more-information-in-watchdog-dumps.patch @@ -0,0 +1,296 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Thu, 26 Mar 2020 21:59:32 -0700 +Subject: [PATCH] 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 + +diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java +index 63066dfde00569833546da01cd580434f8be6593..7fb162fa031fd76aa9a94f5fdaa3e32ceb9b9abc 100644 +--- a/src/main/java/net/minecraft/network/Connection.java ++++ b/src/main/java/net/minecraft/network/Connection.java +@@ -607,7 +607,13 @@ public class Connection extends SimpleChannelInboundHandler> { + if (!(this.packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) + || loginPacketListener.state != net.minecraft.server.network.ServerLoginPacketListenerImpl.State.VERIFYING + || Connection.joinAttemptsThisTick++ < MAX_PER_TICK) { ++ // Paper start - detailed watchdog information ++ net.minecraft.network.protocol.PacketUtils.packetProcessing.push(this.packetListener); ++ try { + tickablepacketlistener.tick(); ++ } finally { ++ net.minecraft.network.protocol.PacketUtils.packetProcessing.pop(); ++ } // Paper end - detailed watchdog information + } // Paper end - Buffer joins to world + } + +diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java +index e161ad0f53a21a68e8c78575ba5d3cdbdb11fca0..57e76b53e5e314c3e6b8856010f7a84188121582 100644 +--- a/src/main/java/net/minecraft/network/protocol/PacketUtils.java ++++ b/src/main/java/net/minecraft/network/protocol/PacketUtils.java +@@ -19,6 +19,24 @@ public class PacketUtils { + + private static final Logger LOGGER = LogUtils.getLogger(); + ++ // Paper start - detailed watchdog information ++ public static final java.util.concurrent.ConcurrentLinkedDeque packetProcessing = new java.util.concurrent.ConcurrentLinkedDeque<>(); ++ 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; ++ } ++ // Paper end - detailed watchdog information ++ + public PacketUtils() {} + + public static void ensureRunningOnSameThread(Packet packet, T listener, ServerLevel world) throws RunningOnDifferentThreadException { +@@ -28,6 +46,8 @@ public class PacketUtils { + public static void ensureRunningOnSameThread(Packet packet, T listener, BlockableEventLoop engine) throws RunningOnDifferentThreadException { + if (!engine.isSameThread()) { + engine.executeIfPossible(() -> { ++ packetProcessing.push(listener); // Paper - detailed watchdog information ++ try { // Paper - detailed watchdog information + if (listener instanceof ServerCommonPacketListenerImpl serverCommonPacketListener && serverCommonPacketListener.processedDisconnect) return; // CraftBukkit - Don't handle sync packets for kicked players + if (listener.shouldHandleMessage(packet)) { + co.aikar.timings.Timing timing = co.aikar.timings.MinecraftTimings.getPacketTiming(packet); // Paper - timings +@@ -47,6 +67,12 @@ public class PacketUtils { + } else { + PacketUtils.LOGGER.debug("Ignoring packet due to disconnection: {}", packet); + } ++ // Paper start - detailed watchdog information ++ } finally { ++ totalMainThreadPacketsProcessed.getAndIncrement(); ++ packetProcessing.pop(); ++ } ++ // Paper end - detailed watchdog information + + }); + throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD; +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 215f03dfe0e6441588679efaedbcf04c352b404c..aabaaf89f5e344791604a7e05122d9865bb0224e 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -1251,7 +1251,26 @@ public class ServerLevel extends Level implements WorldGenLevel { + + } + ++ // Paper start - log detailed entity tick information ++ // TODO replace with varhandle ++ static final java.util.concurrent.atomic.AtomicReference currentlyTickingEntity = new java.util.concurrent.atomic.AtomicReference<>(); ++ ++ public static List getCurrentlyTickingEntities() { ++ Entity ticking = currentlyTickingEntity.get(); ++ List ret = java.util.Arrays.asList(ticking == null ? new Entity[0] : new Entity[] { ticking }); ++ ++ return ret; ++ } ++ // Paper end - log detailed entity tick information ++ + public void tickNonPassenger(Entity entity) { ++ // Paper start - log detailed entity tick information ++ io.papermc.paper.util.TickThread.ensureTickThread("Cannot tick an entity off-main"); ++ try { ++ if (currentlyTickingEntity.get() == null) { ++ currentlyTickingEntity.lazySet(entity); ++ } ++ // Paper end - log detailed entity tick information + ++TimingHistory.entityTicks; // Paper - timings + // Spigot start + co.aikar.timings.Timing timer; // Paper +@@ -1291,7 +1310,13 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.tickPassenger(entity, entity1); + } + // } finally { timer.stopTiming(); } // Paper - timings - move up +- ++ // Paper start - log detailed entity tick information ++ } finally { ++ if (currentlyTickingEntity.get() == entity) { ++ currentlyTickingEntity.lazySet(null); ++ } ++ } ++ // Paper end - log detailed entity tick information + } + + private void tickPassenger(Entity vehicle, Entity passenger) { +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 257943b4c984d6faee29eca17c8f951e7f43168b..0fbcf60a994f67bdd81d40e4a8bf38f0cbb8993d 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -1074,8 +1074,43 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return this.onGround; + } + ++ // Paper start - detailed watchdog information ++ public final Object posLock = new Object(); // Paper - log detailed entity tick information ++ ++ private Vec3 moveVector; ++ private double moveStartX; ++ private double moveStartY; ++ private double moveStartZ; ++ ++ public final Vec3 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; ++ } ++ // Paper end - detailed watchdog information ++ + public void move(MoverType movementType, Vec3 movement) { + final Vec3 originalMovement = movement; // Paper - Expose pre-collision velocity ++ // Paper start - detailed watchdog information ++ io.papermc.paper.util.TickThread.ensureTickThread("Cannot move an entity off-main"); ++ synchronized (this.posLock) { ++ this.moveStartX = this.getX(); ++ this.moveStartY = this.getY(); ++ this.moveStartZ = this.getZ(); ++ this.moveVector = movement; ++ } ++ try { ++ // Paper end - detailed watchdog information + if (this.noPhysics) { + this.setPos(this.getX() + movement.x, this.getY() + movement.y, this.getZ() + movement.z); + } else { +@@ -1245,6 +1280,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.level().getProfiler().pop(); + } + } ++ // Paper start - detailed watchdog information ++ } finally { ++ synchronized (this.posLock) { // Paper ++ this.moveVector = null; ++ } // Paper ++ } ++ // Paper end - detailed watchdog information + } + + private boolean isStateClimbable(BlockState state) { +@@ -4405,7 +4447,9 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + public void setDeltaMovement(Vec3 velocity) { ++ synchronized (this.posLock) { // Paper + this.deltaMovement = velocity; ++ } // Paper + } + + public void addDeltaMovement(Vec3 velocity) { +@@ -4508,7 +4552,9 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + // Paper end - Fix MC-4 + if (this.position.x != x || this.position.y != y || this.position.z != z) { ++ synchronized (this.posLock) { // Paper + this.position = new Vec3(x, y, z); ++ } // Paper + int i = Mth.floor(x); + int j = Mth.floor(y); + int k = Mth.floor(z); +diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java +index 577f29519156f33b49ee319a64a52249630e28d8..6db566e3111ec08a99aa429624979cb83a85e272 100644 +--- a/src/main/java/org/spigotmc/WatchdogThread.java ++++ b/src/main/java/org/spigotmc/WatchdogThread.java +@@ -22,6 +22,78 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa + private volatile long lastTick; + private volatile boolean stopping; + ++ // Paper start - log detailed tick information ++ private void dumpEntity(net.minecraft.world.entity.Entity entity) { ++ Logger log = Bukkit.getServer().getLogger(); ++ double posX, posY, posZ; ++ net.minecraft.world.phys.Vec3 mot; ++ double moveStartX, moveStartY, moveStartZ; ++ net.minecraft.world.phys.Vec3 moveVec; ++ synchronized (entity.posLock) { ++ posX = entity.getX(); ++ posY = entity.getY(); ++ posZ = entity.getZ(); ++ mot = entity.getDeltaMovement(); ++ moveStartX = entity.getMoveStartX(); ++ moveStartY = entity.getMoveStartY(); ++ moveStartZ = entity.getMoveStartZ(); ++ moveVec = entity.getMoveVector(); ++ } ++ ++ String entityType = net.minecraft.world.entity.EntityType.getKey(entity.getType()).toString(); ++ java.util.UUID entityUUID = entity.getUUID(); ++ net.minecraft.world.level.Level world = entity.level(); ++ ++ log.log(Level.SEVERE, "Ticking entity: " + entityType + ", entity class: " + entity.getClass().getName()); ++ log.log(Level.SEVERE, "Entity status: removed: " + entity.isRemoved() + ", valid: " + entity.valid + ", alive: " + entity.isAlive() + ", is passenger: " + entity.isPassenger()); ++ log.log(Level.SEVERE, "Entity UUID: " + entityUUID); ++ 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)"); ++ log.log(Level.SEVERE, "Entity AABB: " + entity.getBoundingBox()); ++ 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()); ++ } ++ } ++ ++ private void dumpTickingInfo() { ++ Logger log = Bukkit.getServer().getLogger(); ++ ++ // ticking entities ++ for (net.minecraft.world.entity.Entity entity : net.minecraft.server.level.ServerLevel.getCurrentlyTickingEntities()) { ++ this.dumpEntity(entity); ++ net.minecraft.world.entity.Entity vehicle = entity.getVehicle(); ++ if (vehicle != null) { ++ log.log(Level.SEVERE, "Detailing vehicle for above entity:"); ++ this.dumpEntity(vehicle); ++ } ++ } ++ ++ // packet processors ++ for (net.minecraft.network.PacketListener packetListener : net.minecraft.network.protocol.PacketUtils.getCurrentPacketProcessors()) { ++ if (packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl) { ++ net.minecraft.server.level.ServerPlayer player = ((net.minecraft.server.network.ServerGamePacketListenerImpl)packetListener).player; ++ long totalPackets = net.minecraft.network.protocol.PacketUtils.getTotalProcessedPackets(); ++ if (player == null) { ++ log.log(Level.SEVERE, "Handling packet for player connection or ticking player connection (null player): " + packetListener); ++ log.log(Level.SEVERE, "Total packets processed on the main thread for all players: " + totalPackets); ++ } else { ++ this.dumpEntity(player); ++ net.minecraft.world.entity.Entity vehicle = player.getVehicle(); ++ if (vehicle != null) { ++ log.log(Level.SEVERE, "Detailing vehicle for above entity:"); ++ this.dumpEntity(vehicle); ++ } ++ 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); ++ } ++ } ++ } ++ // Paper end - log detailed tick information ++ + private WatchdogThread(long timeoutTime, boolean restart) + { + super( "Paper Watchdog Thread" ); +@@ -119,6 +191,7 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa + log.log( Level.SEVERE, "------------------------------" ); + log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper + io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(isLongTimeout); // Paper - rewrite chunk system ++ this.dumpTickingInfo(); // Paper - log detailed tick information + WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log ); + log.log( Level.SEVERE, "------------------------------" ); + // diff --git a/patches/server/1017-Use-Velocity-compression-and-cipher-natives.patch b/patches/server/1017-Use-Velocity-compression-and-cipher-natives.patch deleted file mode 100644 index c357a07cf3..0000000000 --- a/patches/server/1017-Use-Velocity-compression-and-cipher-natives.patch +++ /dev/null @@ -1,375 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Andrew Steinborn -Date: Mon, 26 Jul 2021 02:15:17 -0400 -Subject: [PATCH] Use Velocity compression and cipher natives - - -diff --git a/build.gradle.kts b/build.gradle.kts -index 41767422b521ff095592d799b14b2f36c3b5c5cb..aab4d1f15745bce838329ba0359e3f9ca70f2e1f 100644 ---- a/build.gradle.kts -+++ b/build.gradle.kts -@@ -38,6 +38,11 @@ dependencies { - implementation("org.spongepowered:configurate-yaml:4.2.0-SNAPSHOT") // Paper - config files - implementation("commons-lang:commons-lang:2.6") - runtimeOnly("com.lmax:disruptor:3.4.4") // Paper -+ // Paper start - Use Velocity cipher -+ implementation("com.velocitypowered:velocity-native:3.1.2-SNAPSHOT") { -+ isTransitive = false -+ } -+ // Paper end - Use Velocity cipher - - runtimeOnly("org.apache.maven:maven-resolver-provider:3.9.6") - runtimeOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.9.18") -diff --git a/src/main/java/net/minecraft/network/CipherDecoder.java b/src/main/java/net/minecraft/network/CipherDecoder.java -index 778beb445eac5769b9e4e07b4d1294c50ae2602b..7b895b6a626da6297f07582e96d98ecfb6c8c951 100644 ---- a/src/main/java/net/minecraft/network/CipherDecoder.java -+++ b/src/main/java/net/minecraft/network/CipherDecoder.java -@@ -7,13 +7,29 @@ import java.util.List; - import javax.crypto.Cipher; - - public class CipherDecoder extends MessageToMessageDecoder { -- private final CipherBase cipher; -+ private final com.velocitypowered.natives.encryption.VelocityCipher cipher; // Paper - Use Velocity cipher - -- public CipherDecoder(Cipher cipher) { -- this.cipher = new CipherBase(cipher); -+ public CipherDecoder(com.velocitypowered.natives.encryption.VelocityCipher cipher) { // Paper - Use Velocity cipher -+ this.cipher = cipher; // Paper - Use Velocity cipher - } - - protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List list) throws Exception { -- list.add(this.cipher.decipher(channelHandlerContext, byteBuf)); -+ // Paper start - Use Velocity cipher -+ ByteBuf compatible = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(channelHandlerContext.alloc(), cipher, byteBuf); -+ try { -+ cipher.process(compatible); -+ list.add(compatible); -+ } catch (Exception e) { -+ compatible.release(); // compatible will never be used if we throw an exception -+ throw e; -+ } -+ // Paper end - Use Velocity cipher - } -+ -+ // Paper start - Use Velocity cipher -+ @Override -+ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { -+ cipher.close(); -+ } -+ // Paper end - Use Velocity cipher - } -diff --git a/src/main/java/net/minecraft/network/CipherEncoder.java b/src/main/java/net/minecraft/network/CipherEncoder.java -index 0f3d502a9680006bcdcd7d272240a2e5c3b46790..ffa1c48585fbbc1d30826d435043527f6183a3ee 100644 ---- a/src/main/java/net/minecraft/network/CipherEncoder.java -+++ b/src/main/java/net/minecraft/network/CipherEncoder.java -@@ -4,15 +4,32 @@ import io.netty.buffer.ByteBuf; - import io.netty.channel.ChannelHandlerContext; - import io.netty.handler.codec.MessageToByteEncoder; - import javax.crypto.Cipher; -+import java.util.List; - --public class CipherEncoder extends MessageToByteEncoder { -- private final CipherBase cipher; -+public class CipherEncoder extends io.netty.handler.codec.MessageToMessageEncoder { // Paper - Use Velocity cipher; change superclass -+ private final com.velocitypowered.natives.encryption.VelocityCipher cipher; // Paper - Use Velocity cipher - -- public CipherEncoder(Cipher cipher) { -- this.cipher = new CipherBase(cipher); -+ public CipherEncoder(com.velocitypowered.natives.encryption.VelocityCipher cipher) { // Paper - Use Velocity cipher -+ this.cipher = cipher; // Paper - Use Velocity cipher - } - -- protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, ByteBuf byteBuf2) throws Exception { -- this.cipher.encipher(byteBuf, byteBuf2); -+ protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List list) throws Exception { -+ // Paper start - Use Velocity cipher -+ ByteBuf compatible = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(channelHandlerContext.alloc(), cipher, byteBuf); -+ try { -+ cipher.process(compatible); -+ list.add(compatible); -+ } catch (Exception e) { -+ compatible.release(); // compatible will never be used if we throw an exception -+ throw e; -+ } -+ // Paper end - Use Velocity cipher - } -+ -+ // Paper start - Use Velocity cipher -+ @Override -+ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { -+ cipher.close(); -+ } -+ // Paper end - Use Velocity cipher - } -diff --git a/src/main/java/net/minecraft/network/CompressionDecoder.java b/src/main/java/net/minecraft/network/CompressionDecoder.java -index 5927c928b27f64bd3973e95d5867b59f67476d41..ba3619b03ea73e4ea55ae2eb79d3b6cd1f48a191 100644 ---- a/src/main/java/net/minecraft/network/CompressionDecoder.java -+++ b/src/main/java/net/minecraft/network/CompressionDecoder.java -@@ -13,13 +13,20 @@ public class CompressionDecoder extends ByteToMessageDecoder { - public static final int MAXIMUM_COMPRESSED_LENGTH = 2097152; - public static final int MAXIMUM_UNCOMPRESSED_LENGTH = 8388608; - private final Inflater inflater; -+ private com.velocitypowered.natives.compression.VelocityCompressor compressor; // Paper - Use Velocity cipher - private int threshold; - private boolean validateDecompressed; - -+ // Paper start - Use Velocity cipher - public CompressionDecoder(int compressionThreshold, boolean rejectsBadPackets) { -+ this(null, compressionThreshold, rejectsBadPackets); -+ } -+ public CompressionDecoder(com.velocitypowered.natives.compression.VelocityCompressor compressor, int compressionThreshold, boolean rejectsBadPackets) { - this.threshold = compressionThreshold; - this.validateDecompressed = rejectsBadPackets; -- this.inflater = new Inflater(); -+ this.inflater = compressor == null ? new Inflater() : null; -+ this.compressor = compressor; -+ // Paper end - Use Velocity cipher - } - - protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List list) throws Exception { -@@ -38,14 +45,42 @@ public class CompressionDecoder extends ByteToMessageDecoder { - } - } - -+ if (inflater != null) { // Paper - Use Velocity cipher; fallback to vanilla inflater - this.setupInflaterInput(byteBuf); - ByteBuf byteBuf2 = this.inflate(channelHandlerContext, i); - this.inflater.reset(); - list.add(byteBuf2); -+ return; // Paper - Use Velocity cipher -+ } // Paper - use velocity compression -+ -+ // Paper start - Use Velocity cipher -+ int claimedUncompressedSize = i; // OBFHELPER -+ ByteBuf compatibleIn = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(channelHandlerContext.alloc(), this.compressor, byteBuf); -+ ByteBuf uncompressed = com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer(channelHandlerContext.alloc(), this.compressor, claimedUncompressedSize); -+ try { -+ this.compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize); -+ list.add(uncompressed); -+ byteBuf.clear(); -+ } catch (Exception e) { -+ uncompressed.release(); -+ throw e; -+ } finally { -+ compatibleIn.release(); -+ } -+ // Paper end - Use Velocity cipher - } - } - } - -+ // Paper start - Use Velocity cipher -+ @Override -+ public void handlerRemoved0(ChannelHandlerContext ctx) throws Exception { -+ if (this.compressor != null) { -+ this.compressor.close(); -+ } -+ } -+ // Paper end - Use Velocity cipher -+ - private void setupInflaterInput(ByteBuf buf) { - ByteBuffer byteBuffer; - if (buf.nioBufferCount() > 0) { -@@ -82,7 +117,7 @@ public class CompressionDecoder extends ByteToMessageDecoder { - } - } - -- public void setThreshold(int compressionThreshold, boolean rejectsBadPackets) { -+ public void setThreshold(com.velocitypowered.natives.compression.VelocityCompressor compressor, int compressionThreshold, boolean rejectsBadPackets) { // Paper - Use Velocity cipher - this.threshold = compressionThreshold; - this.validateDecompressed = rejectsBadPackets; - } -diff --git a/src/main/java/net/minecraft/network/CompressionEncoder.java b/src/main/java/net/minecraft/network/CompressionEncoder.java -index 34a80d2f34555663ab1b394972957089214cb337..ec30c291188ac3bba7f1c3bc397576d1edb0a57f 100644 ---- a/src/main/java/net/minecraft/network/CompressionEncoder.java -+++ b/src/main/java/net/minecraft/network/CompressionEncoder.java -@@ -6,16 +6,31 @@ import io.netty.handler.codec.MessageToByteEncoder; - import java.util.zip.Deflater; - - public class CompressionEncoder extends MessageToByteEncoder { -- private final byte[] encodeBuf = new byte[8192]; -+ @javax.annotation.Nullable private final byte[] encodeBuf; // Paper - Use Velocity cipher -+ @javax.annotation.Nullable // Paper - Use Velocity cipher - private final Deflater deflater; -+ @javax.annotation.Nullable // Paper - Use Velocity cipher -+ private final com.velocitypowered.natives.compression.VelocityCompressor compressor; // Paper - Use Velocity cipher - private int threshold; - -+ // Paper start - Use Velocity cipher - public CompressionEncoder(int compressionThreshold) { -+ this(null, compressionThreshold); -+ } -+ public CompressionEncoder(@javax.annotation.Nullable com.velocitypowered.natives.compression.VelocityCompressor compressor, int compressionThreshold) { - this.threshold = compressionThreshold; -- this.deflater = new Deflater(); -+ if (compressor == null) { -+ this.encodeBuf = new byte[8192]; -+ this.deflater = new Deflater(); -+ } else { -+ this.encodeBuf = null; -+ this.deflater = null; -+ } -+ this.compressor = compressor; -+ // Paper end - Use Velocity cipher - } - -- protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, ByteBuf byteBuf2) { -+ protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, ByteBuf byteBuf2) throws Exception { // Paper - Use Velocity cipher - int i = byteBuf.readableBytes(); - if (i > 8388608) { - throw new IllegalArgumentException("Packet too big (is " + i + ", should be less than 8388608)"); -@@ -24,6 +39,7 @@ public class CompressionEncoder extends MessageToByteEncoder { - VarInt.write(byteBuf2, 0); - byteBuf2.writeBytes(byteBuf); - } else { -+ if (this.deflater != null) { // Paper - Use Velocity cipher - byte[] bs = new byte[i]; - byteBuf.readBytes(bs); - VarInt.write(byteBuf2, bs.length); -@@ -36,10 +52,47 @@ public class CompressionEncoder extends MessageToByteEncoder { - } - - this.deflater.reset(); -+ // Paper start - Use Velocity cipher -+ return; -+ } -+ -+ VarInt.write(byteBuf2, i); -+ final ByteBuf compatibleIn = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(channelHandlerContext.alloc(), this.compressor, byteBuf); -+ try { -+ this.compressor.deflate(compatibleIn, byteBuf2); -+ } finally { -+ compatibleIn.release(); -+ } - } - } - } - -+ @Override -+ protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect) throws Exception{ -+ if (this.compressor != null) { -+ // We allocate bytes to be compressed plus 1 byte. This covers two cases: -+ // -+ // - Compression -+ // According to https://github.com/ebiggers/libdeflate/blob/master/libdeflate.h#L103, -+ // if the data compresses well (and we do not have some pathological case) then the maximum -+ // size the compressed size will ever be is the input size minus one. -+ // - Uncompressed -+ // This is fairly obvious - we will then have one more than the uncompressed size. -+ final int initialBufferSize = msg.readableBytes() + 1; -+ return com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer(ctx.alloc(), this.compressor, initialBufferSize); -+ } -+ -+ return super.allocateBuffer(ctx, msg, preferDirect); -+ } -+ -+ @Override -+ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { -+ if (this.compressor != null) { -+ this.compressor.close(); -+ // Paper end - Use Velocity cipher -+ } -+ } -+ - public int getThreshold() { - return this.threshold; - } -diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java -index f94458d4270042062b9ae18931cc8179ef1fb420..63066dfde00569833546da01cd580434f8be6593 100644 ---- a/src/main/java/net/minecraft/network/Connection.java -+++ b/src/main/java/net/minecraft/network/Connection.java -@@ -763,11 +763,28 @@ public class Connection extends SimpleChannelInboundHandler> { - return networkmanager; - } - -- public void setEncryptionKey(Cipher decryptionCipher, Cipher encryptionCipher) { -- this.encrypted = true; -- this.channel.pipeline().addBefore("splitter", "decrypt", new CipherDecoder(decryptionCipher)); -- this.channel.pipeline().addBefore("prepender", "encrypt", new CipherEncoder(encryptionCipher)); -+ // Paper start - Use Velocity cipher -+// public void setEncryptionKey(Cipher decryptionCipher, Cipher encryptionCipher) { -+// this.encrypted = true; -+// this.channel.pipeline().addBefore("splitter", "decrypt", new CipherDecoder(decryptionCipher)); -+// this.channel.pipeline().addBefore("prepender", "encrypt", new CipherEncoder(encryptionCipher)); -+// } -+ -+ public void setupEncryption(javax.crypto.SecretKey key) throws net.minecraft.util.CryptException { -+ if (!this.encrypted) { -+ try { -+ com.velocitypowered.natives.encryption.VelocityCipher decryption = com.velocitypowered.natives.util.Natives.cipher.get().forDecryption(key); -+ com.velocitypowered.natives.encryption.VelocityCipher encryption = com.velocitypowered.natives.util.Natives.cipher.get().forEncryption(key); -+ -+ this.encrypted = true; -+ this.channel.pipeline().addBefore("splitter", "decrypt", new CipherDecoder(decryption)); -+ this.channel.pipeline().addBefore("prepender", "encrypt", new CipherEncoder(encryption)); -+ } catch (java.security.GeneralSecurityException e) { -+ throw new net.minecraft.util.CryptException(e); -+ } -+ } - } -+ // Paper end - Use Velocity cipher - - public boolean isEncrypted() { - return this.encrypted; -@@ -800,12 +817,13 @@ public class Connection extends SimpleChannelInboundHandler> { - - public void setupCompression(int compressionThreshold, boolean rejectsBadPackets) { - if (compressionThreshold >= 0) { -+ com.velocitypowered.natives.compression.VelocityCompressor compressor = com.velocitypowered.natives.util.Natives.compress.get().create(io.papermc.paper.configuration.GlobalConfiguration.get().misc.compressionLevel.or(-1)); // Paper - Use Velocity cipher - ChannelHandler channelhandler = this.channel.pipeline().get("decompress"); - - if (channelhandler instanceof CompressionDecoder) { - CompressionDecoder packetdecompressor = (CompressionDecoder) channelhandler; - -- packetdecompressor.setThreshold(compressionThreshold, rejectsBadPackets); -+ packetdecompressor.setThreshold(compressor, compressionThreshold, rejectsBadPackets); // Paper - Use Velocity cipher - } else { - this.channel.pipeline().addAfter("splitter", "decompress", new CompressionDecoder(compressionThreshold, rejectsBadPackets)); - } -@@ -816,7 +834,7 @@ public class Connection extends SimpleChannelInboundHandler> { - - packetcompressor.setThreshold(compressionThreshold); - } else { -- this.channel.pipeline().addAfter("prepender", "compress", new CompressionEncoder(compressionThreshold)); -+ this.channel.pipeline().addAfter("prepender", "compress", new CompressionEncoder(compressor, compressionThreshold)); // Paper - Use Velocity cipher - } - this.channel.pipeline().fireUserEventTriggered(io.papermc.paper.network.ConnectionEvent.COMPRESSION_THRESHOLD_SET); // Paper - Add Channel initialization listeners - } else { -diff --git a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java -index a08d9aa6e420f691795df9b627a9cd5b5c0112c5..52f537b7bfbdeaad9d17c0e88a1ed1c8925a833f 100644 ---- a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java -+++ b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java -@@ -106,6 +106,11 @@ public class ServerConnectionListener { - ServerConnectionListener.LOGGER.info("Using default channel type"); - } - -+ // Paper start - Use Velocity cipher -+ ServerConnectionListener.LOGGER.info("Paper: Using " + com.velocitypowered.natives.util.Natives.compress.getLoadedVariant() + " compression from Velocity."); -+ ServerConnectionListener.LOGGER.info("Paper: Using " + com.velocitypowered.natives.util.Natives.cipher.getLoadedVariant() + " cipher from Velocity."); -+ // Paper end - Use Velocity cipher -+ - this.channels.add(((ServerBootstrap) ((ServerBootstrap) (new ServerBootstrap()).channel(oclass)).childHandler(new ChannelInitializer() { - protected void initChannel(Channel channel) { - try { -diff --git a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java -index 3333cb70744dc7ef8b181d332b63766aaaaad11f..765cf12c1558afd9686793b18388fc229c55d2d8 100644 ---- a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java -+++ b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java -@@ -274,12 +274,14 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, - } - - SecretKey secretkey = packet.getSecretKey(privatekey); -- Cipher cipher = Crypt.getCipher(2, secretkey); -- Cipher cipher1 = Crypt.getCipher(1, secretkey); -+ // Paper start - Use Velocity cipher -+// Cipher cipher = Crypt.getCipher(2, secretkey); -+// Cipher cipher1 = Crypt.getCipher(1, secretkey); -+ // Paper end - Use Velocity cipher - - s = (new BigInteger(Crypt.digestData("", this.server.getKeyPair().getPublic(), secretkey))).toString(16); - this.state = ServerLoginPacketListenerImpl.State.AUTHENTICATING; -- this.connection.setEncryptionKey(cipher, cipher1); -+ this.connection.setupEncryption(secretkey); // Paper - Use Velocity cipher - } catch (CryptException cryptographyexception) { - throw new IllegalStateException("Protocol error", cryptographyexception); - } diff --git a/patches/server/1018-Collision-optimisations.patch b/patches/server/1018-Collision-optimisations.patch new file mode 100644 index 0000000000..a6179487ff --- /dev/null +++ b/patches/server/1018-Collision-optimisations.patch @@ -0,0 +1,4726 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Mon, 4 May 2020 10:06:24 -0700 +Subject: [PATCH] Collision optimisations + +The collision patch has been designed with the assumption that +most shapes are either a single AABB or an ArrayVoxelShape +(typical voxel bitset representation). Like previously, +single AABB shapes are treated as AABBs. Unlike previously, the +VoxelShape class has been changed to carry shape data that +ArrayVoxelShape would, except in a discrete manner rather +than abstracted away (not hidden behind DoubleList and +the poorly named DiscreteVoxelShape). + +VoxelShape now carries three important states: + 1. The voxel bitset + its sizes for the X, Y, and Z axis + 2. The voxel coordinates (represented as an array and an offset per axis) + 3. Single AABB representation, if possible + +Note that if the single AABB representation is present, +it is used instead of the voxel bitset representation as +the single AABB representation is a special case of the +voxel bitset representation and can be optimised as such. + +This effectively turns every VoxelShape instance, regardless of +actual class, into a typical voxel bitset representation. +This allows all VoxelShape operations to be optimised +for voxel bitset representations without dealing with the +abstraction and indirection that was imposed on VoxelShape +by Mojang. The patch now effectively optimises all VoxelShape +operations. Below is a list of some of the operations optimised: + - Shape merging/ORing + - Shape optimisation + - Occlusion checking + - Non-single AABB VoxelShape collisions/intersection + - Shape raytracing + - Empty VoxelShape testing + +This patch also includes optimisations for raytracing, +which mostly boil down to removing indirection caused by the +interface BlockGetter which allows chunk caching. + +diff --git a/src/main/java/io/papermc/paper/util/CachedLists.java b/src/main/java/io/papermc/paper/util/CachedLists.java +index be668387f65a633c6ac497fca632a4767a1bf3a2..e08f4e39db4ee3fed62e37364d17dcc5c5683504 100644 +--- a/src/main/java/io/papermc/paper/util/CachedLists.java ++++ b/src/main/java/io/papermc/paper/util/CachedLists.java +@@ -1,8 +1,57 @@ + package io.papermc.paper.util; + ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.phys.AABB; ++import org.bukkit.Bukkit; ++import org.bukkit.craftbukkit.util.UnsafeList; ++import java.util.List; ++ + public final class CachedLists { + +- public static void reset() { ++ // Paper start - optimise collisions ++ 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; ++ } ++ // Paper end - optimise collisions ++ ++ public static void reset() { ++ // Paper start - optimise collisions ++ TEMP_COLLISION_LIST.completeReset(); ++ TEMP_GET_ENTITIES_LIST.completeReset(); ++ // Paper end - optimise collisions + } + } +diff --git a/src/main/java/io/papermc/paper/util/CollisionUtil.java b/src/main/java/io/papermc/paper/util/CollisionUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ee8e9c0e3690e78f3cc621ddfca89ea4256d4803 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/CollisionUtil.java +@@ -0,0 +1,1851 @@ ++package io.papermc.paper.util; ++ ++import io.papermc.paper.util.collisions.CachedShapeData; ++import it.unimi.dsi.fastutil.doubles.DoubleArrayList; ++import it.unimi.dsi.fastutil.doubles.DoubleList; ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.Direction; ++import net.minecraft.server.level.ServerChunkCache; ++import net.minecraft.util.Mth; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.item.Item; ++import net.minecraft.world.level.CollisionGetter; ++import net.minecraft.world.level.EntityGetter; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.border.WorldBorder; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.chunk.LevelChunkSection; ++import net.minecraft.world.level.chunk.PalettedContainer; ++import net.minecraft.world.level.material.FluidState; ++import net.minecraft.world.phys.AABB; ++import net.minecraft.world.phys.Vec3; ++import net.minecraft.world.phys.shapes.ArrayVoxelShape; ++import net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape; ++import net.minecraft.world.phys.shapes.BooleanOp; ++import net.minecraft.world.phys.shapes.CollisionContext; ++import net.minecraft.world.phys.shapes.DiscreteVoxelShape; ++import net.minecraft.world.phys.shapes.EntityCollisionContext; ++import net.minecraft.world.phys.shapes.OffsetDoubleList; ++import net.minecraft.world.phys.shapes.Shapes; ++import net.minecraft.world.phys.shapes.VoxelShape; ++import java.util.Arrays; ++import java.util.List; ++import java.util.function.BiPredicate; ++import java.util.function.Predicate; ++ ++public final class CollisionUtil { ++ ++ public static final double COLLISION_EPSILON = 1.0E-7; ++ public static final DoubleArrayList ZERO_ONE = DoubleArrayList.wrap(new double[] { 0.0, 1.0 }); ++ ++ public static boolean isSpecialCollidingBlock(final net.minecraft.world.level.block.state.BlockBehaviour.BlockStateBase block) { ++ return block.hasLargeCollisionShape() || block.getBlock() == Blocks.MOVING_PISTON; ++ } ++ ++ public static boolean isEmpty(final AABB aabb) { ++ return (aabb.maxX - aabb.minX) < COLLISION_EPSILON || (aabb.maxY - aabb.minY) < COLLISION_EPSILON || (aabb.maxZ - aabb.minZ) < COLLISION_EPSILON; ++ } ++ ++ public static boolean isEmpty(final double minX, final double minY, final double minZ, ++ final double maxX, final double maxY, final double maxZ) { ++ return (maxX - minX) < COLLISION_EPSILON || (maxY - minY) < COLLISION_EPSILON || (maxZ - minZ) < COLLISION_EPSILON; ++ } ++ ++ public static AABB getBoxForChunk(final int chunkX, final 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 AABB(x - 3*COLLISION_EPSILON, Double.NEGATIVE_INFINITY, z - 3*COLLISION_EPSILON, ++ x + (16.0 + 3*COLLISION_EPSILON), Double.POSITIVE_INFINITY, z + (16.0 + 3*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 static boolean voxelShapeIntersect(final double minX1, final double minY1, final double minZ1, final double maxX1, ++ final double maxY1, final double maxZ1, final double minX2, final double minY2, ++ final double minZ2, final double maxX2, final double maxY2, final double maxZ2) { ++ return (minX1 - maxX2) < -COLLISION_EPSILON && (maxX1 - minX2) > COLLISION_EPSILON && ++ (minY1 - maxY2) < -COLLISION_EPSILON && (maxY1 - minY2) > COLLISION_EPSILON && ++ (minZ1 - maxZ2) < -COLLISION_EPSILON && (maxZ1 - minZ2) > COLLISION_EPSILON; ++ } ++ ++ public static boolean voxelShapeIntersect(final AABB box, final double minX, final double minY, final double minZ, ++ final double maxX, final double maxY, final double maxZ) { ++ return (box.minX - maxX) < -COLLISION_EPSILON && (box.maxX - minX) > COLLISION_EPSILON && ++ (box.minY - maxY) < -COLLISION_EPSILON && (box.maxY - minY) > COLLISION_EPSILON && ++ (box.minZ - maxZ) < -COLLISION_EPSILON && (box.maxZ - minZ) > COLLISION_EPSILON; ++ } ++ ++ public static boolean voxelShapeIntersect(final AABB box1, final AABB box2) { ++ return (box1.minX - box2.maxX) < -COLLISION_EPSILON && (box1.maxX - box2.minX) > COLLISION_EPSILON && ++ (box1.minY - box2.maxY) < -COLLISION_EPSILON && (box1.maxY - box2.minY) > COLLISION_EPSILON && ++ (box1.minZ - box2.maxZ) < -COLLISION_EPSILON && (box1.maxZ - box2.minZ) > COLLISION_EPSILON; ++ } ++ ++ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON ++ public static double collideX(final AABB target, final AABB source, final double source_move) { ++ if ((source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON && ++ (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) { ++ if (source_move >= 0.0) { ++ final double max_move = target.minX - source.maxX; // < 0.0 if no strict collision ++ if (max_move < -COLLISION_EPSILON) { ++ return source_move; ++ } ++ return Math.min(max_move, source_move); ++ } else { ++ final double max_move = target.maxX - source.minX; // > 0.0 if no strict collision ++ if (max_move > COLLISION_EPSILON) { ++ return source_move; ++ } ++ return Math.max(max_move, source_move); ++ } ++ } ++ return source_move; ++ } ++ ++ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON ++ public static double collideY(final AABB target, final AABB source, final double source_move) { ++ if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON && ++ (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) { ++ if (source_move >= 0.0) { ++ final double max_move = target.minY - source.maxY; // < 0.0 if no strict collision ++ if (max_move < -COLLISION_EPSILON) { ++ return source_move; ++ } ++ return Math.min(max_move, source_move); ++ } else { ++ final double max_move = target.maxY - source.minY; // > 0.0 if no strict collision ++ if (max_move > COLLISION_EPSILON) { ++ return source_move; ++ } ++ return Math.max(max_move, source_move); ++ } ++ } ++ return source_move; ++ } ++ ++ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON ++ public static double collideZ(final AABB target, final AABB source, final double source_move) { ++ if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON && ++ (source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON) { ++ if (source_move >= 0.0) { ++ final double max_move = target.minZ - source.maxZ; // < 0.0 if no strict collision ++ if (max_move < -COLLISION_EPSILON) { ++ return source_move; ++ } ++ return Math.min(max_move, source_move); ++ } else { ++ final double max_move = target.maxZ - source.minZ; // > 0.0 if no strict collision ++ if (max_move > COLLISION_EPSILON) { ++ return source_move; ++ } ++ return Math.max(max_move, source_move); ++ } ++ } ++ return source_move; ++ } ++ ++ // startIndex and endIndex inclusive ++ // assumes indices are in range of array ++ private static int findFloor(final double[] values, final double value, int startIndex, int endIndex) { ++ do { ++ final int middle = (startIndex + endIndex) >>> 1; ++ final double middleVal = values[middle]; ++ ++ if (value < middleVal) { ++ endIndex = middle - 1; ++ } else { ++ startIndex = middle + 1; ++ } ++ } while (startIndex <= endIndex); ++ ++ return startIndex - 1; ++ } ++ ++ public static boolean voxelShapeIntersectNoEmpty(final VoxelShape voxel, final AABB aabb) { ++ if (voxel.isEmpty()) { ++ return false; ++ } ++ ++ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true ++ ++ // offsets that should be applied to coords ++ final double off_x = voxel.offsetX(); ++ final double off_y = voxel.offsetY(); ++ final double off_z = voxel.offsetZ(); ++ ++ final double[] coords_x = voxel.rootCoordinatesX(); ++ final double[] coords_y = voxel.rootCoordinatesY(); ++ final double[] coords_z = voxel.rootCoordinatesZ(); ++ ++ final CachedShapeData cached_shape_data = voxel.getCachedVoxelData(); ++ ++ // note: size = coords.length - 1 ++ final int size_x = cached_shape_data.sizeX(); ++ final int size_y = cached_shape_data.sizeY(); ++ final int size_z = cached_shape_data.sizeZ(); ++ ++ // note: voxel bitset with set index (x, y, z) indicates that ++ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1]) ++ // is collidable. this is the fundamental principle of operation for the voxel collision operation ++ ++ // note: we should be offsetting coords, but we can also just subtract from source as well - which is ++ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that) ++ // note: for intersection, one we find the floor of the min we can use that as the start index ++ // for the next check as source max >= source min ++ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size, ++ // as this implies that coords[coords.length - 1] < source min ++ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max ++ ++ final int floor_min_x = Math.max( ++ 0, ++ findFloor(coords_x, (aabb.minX - off_x) + COLLISION_EPSILON, 0, size_x) ++ ); ++ if (floor_min_x >= size_x) { ++ // cannot intersect ++ return false; ++ } ++ ++ final int ceil_max_x = Math.min( ++ size_x, ++ findFloor(coords_x, (aabb.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1 ++ ); ++ if (floor_min_x >= ceil_max_x) { ++ // cannot intersect ++ return false; ++ } ++ ++ final int floor_min_y = Math.max( ++ 0, ++ findFloor(coords_y, (aabb.minY - off_y) + COLLISION_EPSILON, 0, size_y) ++ ); ++ if (floor_min_y >= size_y) { ++ // cannot intersect ++ return false; ++ } ++ ++ final int ceil_max_y = Math.min( ++ size_y, ++ findFloor(coords_y, (aabb.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1 ++ ); ++ if (floor_min_y >= ceil_max_y) { ++ // cannot intersect ++ return false; ++ } ++ ++ final int floor_min_z = Math.max( ++ 0, ++ findFloor(coords_z, (aabb.minZ - off_z) + COLLISION_EPSILON, 0, size_z) ++ ); ++ if (floor_min_z >= size_z) { ++ // cannot intersect ++ return false; ++ } ++ ++ final int ceil_max_z = Math.min( ++ size_z, ++ findFloor(coords_z, (aabb.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1 ++ ); ++ if (floor_min_z >= ceil_max_z) { ++ // cannot intersect ++ return false; ++ } ++ ++ final long[] bitset = cached_shape_data.voxelSet(); ++ ++ // check bitset to check if any shapes in range are full ++ ++ final int mul_x = size_y*size_z; ++ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { ++ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { ++ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { ++ final int index = curr_z + curr_y*size_z + curr_x*mul_x; ++ // note: JLS states long shift operators ANDS shift by 63 ++ if ((bitset[index >>> 6] & (1L << index)) != 0L) { ++ return true; ++ } ++ } ++ } ++ } ++ ++ return false; ++ } ++ ++ // assume !target.isEmpty() && abs(source_move) >= COLLISION_EPSILON ++ public static double collideX(final VoxelShape target, final AABB source, final double source_move) { ++ final AABB single_aabb = target.getSingleAABBRepresentation(); ++ if (single_aabb != null) { ++ return collideX(single_aabb, source, source_move); ++ } ++ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true ++ ++ // offsets that should be applied to coords ++ final double off_x = target.offsetX(); ++ final double off_y = target.offsetY(); ++ final double off_z = target.offsetZ(); ++ ++ final double[] coords_x = target.rootCoordinatesX(); ++ final double[] coords_y = target.rootCoordinatesY(); ++ final double[] coords_z = target.rootCoordinatesZ(); ++ ++ final CachedShapeData cached_shape_data = target.getCachedVoxelData(); ++ ++ // note: size = coords.length - 1 ++ final int size_x = cached_shape_data.sizeX(); ++ final int size_y = cached_shape_data.sizeY(); ++ final int size_z = cached_shape_data.sizeZ(); ++ ++ // note: voxel bitset with set index (x, y, z) indicates that ++ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1]) ++ // is collidable. this is the fundamental principle of operation for the voxel collision operation ++ ++ ++ // note: we should be offsetting coords, but we can also just subtract from source as well - which is ++ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that) ++ // note: for intersection, one we find the floor of the min we can use that as the start index ++ // for the next check as source max >= source min ++ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size, ++ // as this implies that coords[coords.length - 1] < source min ++ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max ++ ++ final int floor_min_y = Math.max( ++ 0, ++ findFloor(coords_y, (source.minY - off_y) + COLLISION_EPSILON, 0, size_y) ++ ); ++ if (floor_min_y >= size_y) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ final int ceil_max_y = Math.min( ++ size_y, ++ findFloor(coords_y, (source.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1 ++ ); ++ if (floor_min_y >= ceil_max_y) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ final int floor_min_z = Math.max( ++ 0, ++ findFloor(coords_z, (source.minZ - off_z) + COLLISION_EPSILON, 0, size_z) ++ ); ++ if (floor_min_z >= size_z) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ final int ceil_max_z = Math.min( ++ size_z, ++ findFloor(coords_z, (source.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1 ++ ); ++ if (floor_min_z >= ceil_max_z) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ // index = z + y*size_z + x*(size_z*size_y) ++ ++ final long[] bitset = cached_shape_data.voxelSet(); ++ ++ if (source_move > 0.0) { ++ final double source_max = source.maxX - off_x; ++ final int ceil_max_x = findFloor( ++ coords_x, source_max - COLLISION_EPSILON, 0, size_x ++ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max ++ ++ // note: only the order of the first loop matters ++ ++ // note: we cannot collide with the face at index size on the collision axis for forward movement ++ ++ final int mul_x = size_y*size_z; ++ for (int curr_x = ceil_max_x; curr_x < size_x; ++curr_x) { ++ double max_dist = coords_x[curr_x] - source_max; ++ if (max_dist >= source_move) { ++ // if we reach here, then we will never have a case where ++ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1] ++ // thus, we can return immediately ++ ++ // this optimization is important since this loop is bounded by size, and _not_ by ++ // a calculated max index based off of source_move - so it would be possible to check ++ // the whole intersected shape for collisions when we didn't need to! ++ return source_move; ++ } ++ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON ++ max_dist = Math.min(max_dist, source_move); ++ } ++ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { ++ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { ++ final int index = curr_z + curr_y*size_z + curr_x*mul_x; ++ // note: JLS states long shift operators ANDS shift by 63 ++ if ((bitset[index >>> 6] & (1L << index)) != 0L) { ++ return max_dist; ++ } ++ } ++ } ++ } ++ ++ return source_move; ++ } else { ++ final double source_min = source.minX - off_x; ++ final int floor_min_x = findFloor( ++ coords_x, source_min + COLLISION_EPSILON, 0, size_x ++ ); ++ ++ // note: only the order of the first loop matters ++ ++ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement ++ ++ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the ++ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1] ++ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid ++ final int mul_x = size_y*size_z; ++ for (int curr_x = floor_min_x - 1; curr_x >= 0; --curr_x) { ++ double max_dist = coords_x[curr_x + 1] - source_min; ++ if (max_dist <= source_move) { ++ // if we reach here, then we will never have a case where ++ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1] ++ // thus, we can return immediately ++ ++ // this optimization is important since this loop is possibly bounded by size, and _not_ by ++ // a calculated max index based off of source_move - so it would be possible to check ++ // the whole intersected shape for collisions when we didn't need to! ++ return source_move; ++ } ++ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON ++ max_dist = Math.max(max_dist, source_move); ++ } ++ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { ++ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { ++ final int index = curr_z + curr_y*size_z + curr_x*mul_x; ++ // note: JLS states long shift operators ANDS shift by 63 ++ if ((bitset[index >>> 6] & (1L << index)) != 0L) { ++ return max_dist; ++ } ++ } ++ } ++ } ++ ++ return source_move; ++ } ++ } ++ ++ public static double collideY(final VoxelShape target, final AABB source, final double source_move) { ++ final AABB single_aabb = target.getSingleAABBRepresentation(); ++ if (single_aabb != null) { ++ return collideY(single_aabb, source, source_move); ++ } ++ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true ++ ++ // offsets that should be applied to coords ++ final double off_x = target.offsetX(); ++ final double off_y = target.offsetY(); ++ final double off_z = target.offsetZ(); ++ ++ final double[] coords_x = target.rootCoordinatesX(); ++ final double[] coords_y = target.rootCoordinatesY(); ++ final double[] coords_z = target.rootCoordinatesZ(); ++ ++ final CachedShapeData cached_shape_data = target.getCachedVoxelData(); ++ ++ // note: size = coords.length - 1 ++ final int size_x = cached_shape_data.sizeX(); ++ final int size_y = cached_shape_data.sizeY(); ++ final int size_z = cached_shape_data.sizeZ(); ++ ++ // note: voxel bitset with set index (x, y, z) indicates that ++ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1]) ++ // is collidable. this is the fundamental principle of operation for the voxel collision operation ++ ++ ++ // note: we should be offsetting coords, but we can also just subtract from source as well - which is ++ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that) ++ // note: for intersection, one we find the floor of the min we can use that as the start index ++ // for the next check as source max >= source min ++ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size, ++ // as this implies that coords[coords.length - 1] < source min ++ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max ++ ++ final int floor_min_x = Math.max( ++ 0, ++ findFloor(coords_x, (source.minX - off_x) + COLLISION_EPSILON, 0, size_x) ++ ); ++ if (floor_min_x >= size_x) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ final int ceil_max_x = Math.min( ++ size_x, ++ findFloor(coords_x, (source.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1 ++ ); ++ if (floor_min_x >= ceil_max_x) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ final int floor_min_z = Math.max( ++ 0, ++ findFloor(coords_z, (source.minZ - off_z) + COLLISION_EPSILON, 0, size_z) ++ ); ++ if (floor_min_z >= size_z) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ final int ceil_max_z = Math.min( ++ size_z, ++ findFloor(coords_z, (source.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1 ++ ); ++ if (floor_min_z >= ceil_max_z) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ // index = z + y*size_z + x*(size_z*size_y) ++ ++ final long[] bitset = cached_shape_data.voxelSet(); ++ ++ if (source_move > 0.0) { ++ final double source_max = source.maxY - off_y; ++ final int ceil_max_y = findFloor( ++ coords_y, source_max - COLLISION_EPSILON, 0, size_y ++ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max ++ ++ // note: only the order of the first loop matters ++ ++ // note: we cannot collide with the face at index size on the collision axis for forward movement ++ ++ final int mul_x = size_y*size_z; ++ for (int curr_y = ceil_max_y; curr_y < size_y; ++curr_y) { ++ double max_dist = coords_y[curr_y] - source_max; ++ if (max_dist >= source_move) { ++ // if we reach here, then we will never have a case where ++ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1] ++ // thus, we can return immediately ++ ++ // this optimization is important since this loop is bounded by size, and _not_ by ++ // a calculated max index based off of source_move - so it would be possible to check ++ // the whole intersected shape for collisions when we didn't need to! ++ return source_move; ++ } ++ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON ++ max_dist = Math.min(max_dist, source_move); ++ } ++ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { ++ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { ++ final int index = curr_z + curr_y*size_z + curr_x*mul_x; ++ // note: JLS states long shift operators ANDS shift by 63 ++ if ((bitset[index >>> 6] & (1L << index)) != 0L) { ++ return max_dist; ++ } ++ } ++ } ++ } ++ ++ return source_move; ++ } else { ++ final double source_min = source.minY - off_y; ++ final int floor_min_y = findFloor( ++ coords_y, source_min + COLLISION_EPSILON, 0, size_y ++ ); ++ ++ // note: only the order of the first loop matters ++ ++ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement ++ ++ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the ++ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1] ++ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid ++ final int mul_x = size_y*size_z; ++ for (int curr_y = floor_min_y - 1; curr_y >= 0; --curr_y) { ++ double max_dist = coords_y[curr_y + 1] - source_min; ++ if (max_dist <= source_move) { ++ // if we reach here, then we will never have a case where ++ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1] ++ // thus, we can return immediately ++ ++ // this optimization is important since this loop is possibly bounded by size, and _not_ by ++ // a calculated max index based off of source_move - so it would be possible to check ++ // the whole intersected shape for collisions when we didn't need to! ++ return source_move; ++ } ++ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON ++ max_dist = Math.max(max_dist, source_move); ++ } ++ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { ++ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { ++ final int index = curr_z + curr_y*size_z + curr_x*mul_x; ++ // note: JLS states long shift operators ANDS shift by 63 ++ if ((bitset[index >>> 6] & (1L << index)) != 0L) { ++ return max_dist; ++ } ++ } ++ } ++ } ++ ++ return source_move; ++ } ++ } ++ ++ public static double collideZ(final VoxelShape target, final AABB source, final double source_move) { ++ final AABB single_aabb = target.getSingleAABBRepresentation(); ++ if (single_aabb != null) { ++ return collideZ(single_aabb, source, source_move); ++ } ++ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true ++ ++ // offsets that should be applied to coords ++ final double off_x = target.offsetX(); ++ final double off_y = target.offsetY(); ++ final double off_z = target.offsetZ(); ++ ++ final double[] coords_x = target.rootCoordinatesX(); ++ final double[] coords_y = target.rootCoordinatesY(); ++ final double[] coords_z = target.rootCoordinatesZ(); ++ ++ final CachedShapeData cached_shape_data = target.getCachedVoxelData(); ++ ++ // note: size = coords.length - 1 ++ final int size_x = cached_shape_data.sizeX(); ++ final int size_y = cached_shape_data.sizeY(); ++ final int size_z = cached_shape_data.sizeZ(); ++ ++ // note: voxel bitset with set index (x, y, z) indicates that ++ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1]) ++ // is collidable. this is the fundamental principle of operation for the voxel collision operation ++ ++ ++ // note: we should be offsetting coords, but we can also just subtract from source as well - which is ++ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that) ++ // note: for intersection, one we find the floor of the min we can use that as the start index ++ // for the next check as source max >= source min ++ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size, ++ // as this implies that coords[coords.length - 1] < source min ++ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max ++ ++ final int floor_min_x = Math.max( ++ 0, ++ findFloor(coords_x, (source.minX - off_x) + COLLISION_EPSILON, 0, size_x) ++ ); ++ if (floor_min_x >= size_x) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ final int ceil_max_x = Math.min( ++ size_x, ++ findFloor(coords_x, (source.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1 ++ ); ++ if (floor_min_x >= ceil_max_x) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ final int floor_min_y = Math.max( ++ 0, ++ findFloor(coords_y, (source.minY - off_y) + COLLISION_EPSILON, 0, size_y) ++ ); ++ if (floor_min_y >= size_y) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ final int ceil_max_y = Math.min( ++ size_y, ++ findFloor(coords_y, (source.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1 ++ ); ++ if (floor_min_y >= ceil_max_y) { ++ // cannot intersect ++ return source_move; ++ } ++ ++ // index = z + y*size_z + x*(size_z*size_y) ++ ++ final long[] bitset = cached_shape_data.voxelSet(); ++ ++ if (source_move > 0.0) { ++ final double source_max = source.maxZ - off_z; ++ final int ceil_max_z = findFloor( ++ coords_z, source_max - COLLISION_EPSILON, 0, size_z ++ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max ++ ++ // note: only the order of the first loop matters ++ ++ // note: we cannot collide with the face at index size on the collision axis for forward movement ++ ++ final int mul_x = size_y*size_z; ++ for (int curr_z = ceil_max_z; curr_z < size_z; ++curr_z) { ++ double max_dist = coords_z[curr_z] - source_max; ++ if (max_dist >= source_move) { ++ // if we reach here, then we will never have a case where ++ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1] ++ // thus, we can return immediately ++ ++ // this optimization is important since this loop is bounded by size, and _not_ by ++ // a calculated max index based off of source_move - so it would be possible to check ++ // the whole intersected shape for collisions when we didn't need to! ++ return source_move; ++ } ++ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON ++ max_dist = Math.min(max_dist, source_move); ++ } ++ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { ++ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { ++ final int index = curr_z + curr_y*size_z + curr_x*mul_x; ++ // note: JLS states long shift operators ANDS shift by 63 ++ if ((bitset[index >>> 6] & (1L << index)) != 0L) { ++ return max_dist; ++ } ++ } ++ } ++ } ++ ++ return source_move; ++ } else { ++ final double source_min = source.minZ - off_z; ++ final int floor_min_z = findFloor( ++ coords_z, source_min + COLLISION_EPSILON, 0, size_z ++ ); ++ ++ // note: only the order of the first loop matters ++ ++ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement ++ ++ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the ++ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1] ++ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid ++ final int mul_x = size_y*size_z; ++ for (int curr_z = floor_min_z - 1; curr_z >= 0; --curr_z) { ++ double max_dist = coords_z[curr_z + 1] - source_min; ++ if (max_dist <= source_move) { ++ // if we reach here, then we will never have a case where ++ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1] ++ // thus, we can return immediately ++ ++ // this optimization is important since this loop is possibly bounded by size, and _not_ by ++ // a calculated max index based off of source_move - so it would be possible to check ++ // the whole intersected shape for collisions when we didn't need to! ++ return source_move; ++ } ++ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON ++ max_dist = Math.max(max_dist, source_move); ++ } ++ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { ++ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { ++ final int index = curr_z + curr_y*size_z + curr_x*mul_x; ++ // note: JLS states long shift operators ANDS shift by 63 ++ if ((bitset[index >>> 6] & (1L << index)) != 0L) { ++ return max_dist; ++ } ++ } ++ } ++ } ++ ++ return source_move; ++ } ++ } ++ ++ // does not use epsilon ++ public static boolean strictlyContains(final VoxelShape voxel, final Vec3 point) { ++ return strictlyContains(voxel, point.x, point.y, point.z); ++ } ++ ++ // does not use epsilon ++ public static boolean strictlyContains(final VoxelShape voxel, double x, double y, double z) { ++ final AABB single_aabb = voxel.getSingleAABBRepresentation(); ++ if (single_aabb != null) { ++ return single_aabb.contains(x, y, z); ++ } ++ ++ if (voxel.isEmpty()) { ++ // bitset is clear, no point in searching ++ return false; ++ } ++ ++ // offset input ++ x -= voxel.offsetX(); ++ y -= voxel.offsetY(); ++ z -= voxel.offsetZ(); ++ ++ final double[] coords_x = voxel.rootCoordinatesX(); ++ final double[] coords_y = voxel.rootCoordinatesY(); ++ final double[] coords_z = voxel.rootCoordinatesZ(); ++ ++ final CachedShapeData cached_shape_data = voxel.getCachedVoxelData(); ++ ++ // note: size = coords.length - 1 ++ final int size_x = cached_shape_data.sizeX(); ++ final int size_y = cached_shape_data.sizeY(); ++ final int size_z = cached_shape_data.sizeZ(); ++ ++ // note: should mirror AABB#contains, which is that for any point X that X >= min and X < max. ++ // specifically, it cannot collide on the max bounds of the shape ++ ++ final int index_x = findFloor(coords_x, x, 0, size_x); ++ if (index_x < 0 || index_x >= size_x) { ++ return false; ++ } ++ ++ final int index_y = findFloor(coords_y, y, 0, size_y); ++ if (index_y < 0 || index_y >= size_y) { ++ return false; ++ } ++ ++ final int index_z = findFloor(coords_z, z, 0, size_z); ++ if (index_z < 0 || index_z >= size_z) { ++ return false; ++ } ++ ++ // index = z + y*size_z + x*(size_z*size_y) ++ ++ final int index = index_z + index_y*size_z + index_x*(size_z*size_y); ++ ++ final long[] bitset = cached_shape_data.voxelSet(); ++ ++ return (bitset[index >>> 6] & (1L << index)) != 0L; ++ } ++ ++ private static int makeBitset(final boolean ft, final boolean tf, final boolean tt) { ++ // idx ff -> 0 ++ // idx ft -> 1 ++ // idx tf -> 2 ++ // idx tt -> 3 ++ return ((ft ? 1 : 0) << 1) | ((tf ? 1 : 0) << 2) | ((tt ? 1 : 0) << 3); ++ } ++ ++ private static BitSetDiscreteVoxelShape merge(final CachedShapeData shapeDataFirst, final CachedShapeData shapeDataSecond, ++ final MergedVoxelCoordinateList mergedX, final MergedVoxelCoordinateList mergedY, ++ final MergedVoxelCoordinateList mergedZ, ++ final int booleanOp) { ++ final int sizeX = mergedX.voxels; ++ final int sizeY = mergedY.voxels; ++ final int sizeZ = mergedZ.voxels; ++ ++ final long[] s1Voxels = shapeDataFirst.voxelSet(); ++ final long[] s2Voxels = shapeDataSecond.voxelSet(); ++ ++ final int s1Mul1 = shapeDataFirst.sizeZ(); ++ final int s1Mul2 = s1Mul1 * shapeDataFirst.sizeY(); ++ ++ final int s2Mul1 = shapeDataSecond.sizeZ(); ++ final int s2Mul2 = s2Mul1 * shapeDataSecond.sizeY(); ++ ++ // note: indices may contain -1, but nothing > size ++ final BitSetDiscreteVoxelShape ret = new BitSetDiscreteVoxelShape(sizeX, sizeY, sizeZ); ++ ++ boolean empty = true; ++ ++ int mergedIdx = 0; ++ for (int idxX = 0; idxX < sizeX; ++idxX) { ++ final int s1x = mergedX.firstIndices[idxX]; ++ final int s2x = mergedX.secondIndices[idxX]; ++ boolean setX = false; ++ for (int idxY = 0; idxY < sizeY; ++idxY) { ++ final int s1y = mergedY.firstIndices[idxY]; ++ final int s2y = mergedY.secondIndices[idxY]; ++ boolean setY = false; ++ for (int idxZ = 0; idxZ < sizeZ; ++idxZ) { ++ final int s1z = mergedZ.firstIndices[idxZ]; ++ final int s2z = mergedZ.secondIndices[idxZ]; ++ ++ int idx; ++ ++ final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx) & 1L); ++ final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx) & 1L); ++ ++ // idx ff -> 0 ++ // idx ft -> 1 ++ // idx tf -> 2 ++ // idx tt -> 3 ++ ++ final boolean res = (booleanOp & (1 << (isS2Full | (isS1Full << 1)))) != 0; ++ setY |= res; ++ setX |= res; ++ ++ if (res) { ++ empty = false; ++ // inline and optimize fill operation ++ ret.zMin = Math.min(ret.zMin, idxZ); ++ ret.zMax = Math.max(ret.zMax, idxZ + 1); ++ ret.storage.set(mergedIdx); ++ } ++ ++ ++mergedIdx; ++ } ++ if (setY) { ++ ret.yMin = Math.min(ret.yMin, idxY); ++ ret.yMax = Math.max(ret.yMax, idxY + 1); ++ } ++ } ++ if (setX) { ++ ret.xMin = Math.min(ret.xMin, idxX); ++ ret.xMax = Math.max(ret.xMax, idxX + 1); ++ } ++ } ++ ++ return empty ? null : ret; ++ } ++ ++ private static boolean isMergeEmpty(final CachedShapeData shapeDataFirst, final CachedShapeData shapeDataSecond, ++ final MergedVoxelCoordinateList mergedX, final MergedVoxelCoordinateList mergedY, ++ final MergedVoxelCoordinateList mergedZ, ++ final int booleanOp) { ++ final int sizeX = mergedX.voxels; ++ final int sizeY = mergedY.voxels; ++ final int sizeZ = mergedZ.voxels; ++ ++ final long[] s1Voxels = shapeDataFirst.voxelSet(); ++ final long[] s2Voxels = shapeDataSecond.voxelSet(); ++ ++ final int s1Mul1 = shapeDataFirst.sizeZ(); ++ final int s1Mul2 = s1Mul1 * shapeDataFirst.sizeY(); ++ ++ final int s2Mul1 = shapeDataSecond.sizeZ(); ++ final int s2Mul2 = s2Mul1 * shapeDataSecond.sizeY(); ++ ++ // note: indices may contain -1, but nothing > size ++ for (int idxX = 0; idxX < sizeX; ++idxX) { ++ final int s1x = mergedX.firstIndices[idxX]; ++ final int s2x = mergedX.secondIndices[idxX]; ++ for (int idxY = 0; idxY < sizeY; ++idxY) { ++ final int s1y = mergedY.firstIndices[idxY]; ++ final int s2y = mergedY.secondIndices[idxY]; ++ for (int idxZ = 0; idxZ < sizeZ; ++idxZ) { ++ final int s1z = mergedZ.firstIndices[idxZ]; ++ final int s2z = mergedZ.secondIndices[idxZ]; ++ ++ int idx; ++ ++ final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx) & 1L); ++ final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx) & 1L); ++ ++ // idx ff -> 0 ++ // idx ft -> 1 ++ // idx tf -> 2 ++ // idx tt -> 3 ++ ++ final boolean res = (booleanOp & (1 << (isS2Full | (isS1Full << 1)))) != 0; ++ ++ if (res) { ++ return false; ++ } ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ public static VoxelShape joinOptimized(final VoxelShape first, final VoxelShape second, final BooleanOp operator) { ++ return joinUnoptimized(first, second, operator).optimize(); ++ } ++ ++ public static VoxelShape joinUnoptimized(final VoxelShape first, final VoxelShape second, final BooleanOp operator) { ++ final boolean ff = operator.apply(false, false); ++ if (ff) { ++ // technically, should be an infinite box but that's clearly an error ++ throw new UnsupportedOperationException("Ambiguous operator: (false, false) -> true"); ++ } ++ ++ final boolean tt = operator.apply(true, true); ++ ++ if (first == second) { ++ return tt ? first : Shapes.empty(); ++ } ++ ++ final boolean ft = operator.apply(false, true); ++ final boolean tf = operator.apply(true, false); ++ ++ if (first.isEmpty()) { ++ return ft ? second : Shapes.empty(); ++ } ++ if (second.isEmpty()) { ++ return tf ? first : Shapes.empty(); ++ } ++ ++ if (!tt) { ++ // try to check for no intersection, since tt = false ++ final AABB aabbF = first.getSingleAABBRepresentation(); ++ final AABB aabbS = second.getSingleAABBRepresentation(); ++ ++ final boolean intersect; ++ ++ final boolean hasAABBF = aabbF != null; ++ final boolean hasAABBS = aabbS != null; ++ if (hasAABBF | hasAABBS) { ++ if (hasAABBF & hasAABBS) { ++ intersect = voxelShapeIntersect(aabbF, aabbS); ++ } else if (hasAABBF) { ++ intersect = voxelShapeIntersectNoEmpty(second, aabbF); ++ } else { ++ intersect = voxelShapeIntersectNoEmpty(first, aabbS); ++ } ++ } else { ++ // expect cached bounds ++ intersect = voxelShapeIntersect(first.bounds(), second.bounds()); ++ } ++ ++ if (!intersect) { ++ if (!tf & !ft) { ++ return Shapes.empty(); ++ } ++ if (!tf | !ft) { ++ return tf ? first : second; ++ } ++ } ++ } ++ ++ final MergedVoxelCoordinateList mergedX = MergedVoxelCoordinateList.merge( ++ first.rootCoordinatesX(), first.offsetX(), ++ second.rootCoordinatesX(), second.offsetX(), ++ ft, tf ++ ); ++ if (mergedX == MergedVoxelCoordinateList.EMPTY) { ++ return Shapes.empty(); ++ } ++ final MergedVoxelCoordinateList mergedY = MergedVoxelCoordinateList.merge( ++ first.rootCoordinatesY(), first.offsetY(), ++ second.rootCoordinatesY(), second.offsetY(), ++ ft, tf ++ ); ++ if (mergedY == MergedVoxelCoordinateList.EMPTY) { ++ return Shapes.empty(); ++ } ++ final MergedVoxelCoordinateList mergedZ = MergedVoxelCoordinateList.merge( ++ first.rootCoordinatesZ(), first.offsetZ(), ++ second.rootCoordinatesZ(), second.offsetZ(), ++ ft, tf ++ ); ++ if (mergedZ == MergedVoxelCoordinateList.EMPTY) { ++ return Shapes.empty(); ++ } ++ ++ final CachedShapeData shapeDataFirst = first.getCachedVoxelData(); ++ final CachedShapeData shapeDataSecond = second.getCachedVoxelData(); ++ ++ final BitSetDiscreteVoxelShape mergedShape = merge( ++ shapeDataFirst, shapeDataSecond, ++ mergedX, mergedY, mergedZ, ++ makeBitset(ft, tf, tt) ++ ); ++ ++ if (mergedShape == null) { ++ return Shapes.empty(); ++ } ++ ++ return new ArrayVoxelShape( ++ mergedShape, mergedX.wrapCoords(), mergedY.wrapCoords(), mergedZ.wrapCoords() ++ ); ++ } ++ ++ public static boolean isJoinNonEmpty(final VoxelShape first, final VoxelShape second, final BooleanOp operator) { ++ final boolean ff = operator.apply(false, false); ++ if (ff) { ++ // technically, should be an infinite box but that's clearly an error ++ throw new UnsupportedOperationException("Ambiguous operator: (false, false) -> true"); ++ } ++ final boolean firstEmpty = first.isEmpty(); ++ final boolean secondEmpty = second.isEmpty(); ++ if (firstEmpty | secondEmpty) { ++ return operator.apply(!firstEmpty, !secondEmpty); ++ } ++ ++ final boolean tt = operator.apply(true, true); ++ ++ if (first == second) { ++ return tt; ++ } ++ ++ final boolean ft = operator.apply(false, true); ++ final boolean tf = operator.apply(true, false); ++ ++ // try to check intersection ++ final AABB aabbF = first.getSingleAABBRepresentation(); ++ final AABB aabbS = second.getSingleAABBRepresentation(); ++ ++ final boolean intersect; ++ ++ final boolean hasAABBF = aabbF != null; ++ final boolean hasAABBS = aabbS != null; ++ if (hasAABBF | hasAABBS) { ++ if (hasAABBF & hasAABBS) { ++ intersect = voxelShapeIntersect(aabbF, aabbS); ++ } else if (hasAABBF) { ++ intersect = voxelShapeIntersectNoEmpty(second, aabbF); ++ } else { ++ // hasAABBS -> true ++ intersect = voxelShapeIntersectNoEmpty(first, aabbS); ++ } ++ ++ if (!intersect) { ++ // is only non-empty if we take from first or second, as there is no overlap AND both shapes are non-empty ++ return tf | ft; ++ } else if (tt) { ++ // intersect = true && tt = true -> non-empty merged shape ++ return true; ++ } ++ } else { ++ // expect cached bounds ++ intersect = voxelShapeIntersect(first.bounds(), second.bounds()); ++ if (!intersect) { ++ // is only non-empty if we take from first or second, as there is no intersection ++ return tf | ft; ++ } ++ } ++ ++ final MergedVoxelCoordinateList mergedX = MergedVoxelCoordinateList.merge( ++ first.rootCoordinatesX(), first.offsetX(), ++ second.rootCoordinatesX(), second.offsetX(), ++ ft, tf ++ ); ++ if (mergedX == MergedVoxelCoordinateList.EMPTY) { ++ return false; ++ } ++ final MergedVoxelCoordinateList mergedY = MergedVoxelCoordinateList.merge( ++ first.rootCoordinatesY(), first.offsetY(), ++ second.rootCoordinatesY(), second.offsetY(), ++ ft, tf ++ ); ++ if (mergedY == MergedVoxelCoordinateList.EMPTY) { ++ return false; ++ } ++ final MergedVoxelCoordinateList mergedZ = MergedVoxelCoordinateList.merge( ++ first.rootCoordinatesZ(), first.offsetZ(), ++ second.rootCoordinatesZ(), second.offsetZ(), ++ ft, tf ++ ); ++ if (mergedZ == MergedVoxelCoordinateList.EMPTY) { ++ return false; ++ } ++ ++ final CachedShapeData shapeDataFirst = first.getCachedVoxelData(); ++ final CachedShapeData shapeDataSecond = second.getCachedVoxelData(); ++ ++ return !isMergeEmpty( ++ shapeDataFirst, shapeDataSecond, ++ mergedX, mergedY, mergedZ, ++ makeBitset(ft, tf, tt) ++ ); ++ } ++ ++ private static final class MergedVoxelCoordinateList { ++ ++ private static final int[][] SIMPLE_INDICES_CACHE = new int[64][]; ++ static { ++ for (int i = 0; i < SIMPLE_INDICES_CACHE.length; ++i) { ++ SIMPLE_INDICES_CACHE[i] = getIndices(i); ++ } ++ } ++ ++ private static final MergedVoxelCoordinateList EMPTY = new MergedVoxelCoordinateList( ++ new double[] { 0.0 }, 0.0, new int[0], new int[0], 0 ++ ); ++ ++ private static int[] getIndices(final int length) { ++ final int[] ret = new int[length]; ++ ++ for (int i = 1; i < length; ++i) { ++ ret[i] = i; ++ } ++ ++ return ret; ++ } ++ ++ // indices above voxel size are always set to -1 ++ public final double[] coordinates; ++ public final double coordinateOffset; ++ public final int[] firstIndices; ++ public final int[] secondIndices; ++ public final int voxels; ++ ++ private MergedVoxelCoordinateList(final double[] coordinates, final double coordinateOffset, ++ final int[] firstIndices, final int[] secondIndices, final int voxels) { ++ this.coordinates = coordinates; ++ this.coordinateOffset = coordinateOffset; ++ this.firstIndices = firstIndices; ++ this.secondIndices = secondIndices; ++ this.voxels = voxels; ++ } ++ ++ public DoubleList wrapCoords() { ++ if (this.coordinateOffset == 0.0) { ++ return DoubleArrayList.wrap(this.coordinates, this.voxels + 1); ++ } ++ return new OffsetDoubleList(DoubleArrayList.wrap(this.coordinates, this.voxels + 1), this.coordinateOffset); ++ } ++ ++ // assume coordinates.length > 1 ++ public static MergedVoxelCoordinateList getForSingle(final double[] coordinates, final double offset) { ++ final int voxels = coordinates.length - 1; ++ final int[] indices = voxels < SIMPLE_INDICES_CACHE.length ? SIMPLE_INDICES_CACHE[voxels] : getIndices(voxels); ++ ++ return new MergedVoxelCoordinateList(coordinates, offset, indices, indices, voxels); ++ } ++ ++ // assume coordinates.length > 1 ++ public static MergedVoxelCoordinateList merge(final double[] firstCoordinates, final double firstOffset, ++ final double[] secondCoordinates, final double secondOffset, ++ final boolean ft, final boolean tf) { ++ if (firstCoordinates == secondCoordinates && firstOffset == secondOffset) { ++ return getForSingle(firstCoordinates, firstOffset); ++ } ++ ++ final int firstCount = firstCoordinates.length; ++ final int secondCount = secondCoordinates.length; ++ ++ final int voxelsFirst = firstCount - 1; ++ final int voxelsSecond = secondCount - 1; ++ ++ final int maxCount = firstCount + secondCount; ++ ++ final double[] coordinates = new double[maxCount]; ++ final int[] firstIndices = new int[maxCount]; ++ final int[] secondIndices = new int[maxCount]; ++ ++ final boolean notTF = !tf; ++ final boolean notFT = !ft; ++ ++ int firstIndex = 0; ++ int secondIndex = 0; ++ int resultSize = 0; ++ ++ // note: operations on NaN are false ++ double last = Double.NaN; ++ ++ for (;;) { ++ final boolean noneLeftFirst = firstIndex >= firstCount; ++ final boolean noneLeftSecond = secondIndex >= secondCount; ++ ++ if ((noneLeftFirst & noneLeftSecond) | (noneLeftSecond & notTF) | (noneLeftFirst & notFT)) { ++ break; ++ } ++ ++ final boolean firstZero = firstIndex == 0; ++ final boolean secondZero = secondIndex == 0; ++ ++ final double select; ++ ++ if (noneLeftFirst) { ++ // noneLeftSecond -> false ++ // notFT -> false ++ select = secondCoordinates[secondIndex] + secondOffset; ++ ++secondIndex; ++ } else if (noneLeftSecond) { ++ // noneLeftFirst -> false ++ // notTF -> false ++ select = firstCoordinates[firstIndex] + firstOffset; ++ ++firstIndex; ++ } else { ++ // noneLeftFirst | noneLeftSecond -> false ++ // notTF -> ?? ++ // notFT -> ?? ++ final boolean breakFirst = notTF & secondZero; ++ final boolean breakSecond = notFT & firstZero; ++ ++ final double first = firstCoordinates[firstIndex] + firstOffset; ++ final double second = secondCoordinates[secondIndex] + secondOffset; ++ final boolean useFirst = first < (second + COLLISION_EPSILON); ++ final boolean cont = (useFirst & breakFirst) | (!useFirst & breakSecond); ++ ++ select = useFirst ? first : second; ++ firstIndex += useFirst ? 1 : 0; ++ secondIndex += 1 ^ (useFirst ? 1 : 0); ++ ++ if (cont) { ++ continue; ++ } ++ } ++ ++ int prevFirst = firstIndex - 1; ++ prevFirst = prevFirst >= voxelsFirst ? -1 : prevFirst; ++ int prevSecond = secondIndex - 1; ++ prevSecond = prevSecond >= voxelsSecond ? -1 : prevSecond; ++ ++ if (last >= (select - COLLISION_EPSILON)) { ++ // note: any operations on NaN is false ++ firstIndices[resultSize - 1] = prevFirst; ++ secondIndices[resultSize - 1] = prevSecond; ++ } else { ++ firstIndices[resultSize] = prevFirst; ++ secondIndices[resultSize] = prevSecond; ++ coordinates[resultSize] = select; ++ ++ ++resultSize; ++ last = select; ++ } ++ } ++ ++ return resultSize <= 1 ? EMPTY : new MergedVoxelCoordinateList(coordinates, 0.0, firstIndices, secondIndices, resultSize - 1); ++ } ++ } ++ ++ public static AABB offsetX(final AABB box, final double dx) { ++ return new AABB(box.minX + dx, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false); ++ } ++ ++ public static AABB offsetY(final AABB box, final double dy) { ++ return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false); ++ } ++ ++ public static AABB offsetZ(final AABB box, final double dz) { ++ return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.maxZ + dz, false); ++ } ++ ++ public static AABB expandRight(final AABB box, final double dx) { // dx > 0.0 ++ return new AABB(box.minX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false); ++ } ++ ++ public static AABB expandLeft(final AABB box, final double dx) { // dx < 0.0 ++ return new AABB(box.minX - dx, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ); ++ } ++ ++ public static AABB expandUpwards(final AABB box, final double dy) { // dy > 0.0 ++ return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false); ++ } ++ ++ public static AABB expandDownwards(final AABB box, final double dy) { // dy < 0.0 ++ return new AABB(box.minX, box.minY - dy, box.minZ, box.maxX, box.maxY, box.maxZ, false); ++ } ++ ++ public static AABB expandForwards(final AABB box, final double dz) { // dz > 0.0 ++ return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ + dz, false); ++ } ++ ++ public static AABB expandBackwards(final AABB box, final double dz) { // dz < 0.0 ++ return new AABB(box.minX, box.minY, box.minZ - dz, box.maxX, box.maxY, box.maxZ, false); ++ } ++ ++ public static AABB cutRight(final AABB box, final double dx) { // dx > 0.0 ++ return new AABB(box.maxX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false); ++ } ++ ++ public static AABB cutLeft(final AABB box, final double dx) { // dx < 0.0 ++ return new AABB(box.minX + dx, box.minY, box.minZ, box.minX, box.maxY, box.maxZ, false); ++ } ++ ++ public static AABB cutUpwards(final AABB box, final double dy) { // dy > 0.0 ++ return new AABB(box.minX, box.maxY, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false); ++ } ++ ++ public static AABB cutDownwards(final AABB box, final double dy) { // dy < 0.0 ++ return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.minY, box.maxZ, false); ++ } ++ ++ public static AABB cutForwards(final AABB box, final double dz) { // dz > 0.0 ++ return new AABB(box.minX, box.minY, box.maxZ, box.maxX, box.maxY, box.maxZ + dz, false); ++ } ++ ++ public static AABB cutBackwards(final AABB box, final double dz) { // dz < 0.0 ++ return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.minZ, false); ++ } ++ ++ public static double performAABBCollisionsX(final AABB currentBoundingBox, double value, final List potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ if (Math.abs(value) < COLLISION_EPSILON) { ++ return 0.0; ++ } ++ final AABB target = potentialCollisions.get(i); ++ value = collideX(target, currentBoundingBox, value); ++ } ++ ++ return value; ++ } ++ ++ public static double performAABBCollisionsY(final AABB currentBoundingBox, double value, final List potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ if (Math.abs(value) < COLLISION_EPSILON) { ++ return 0.0; ++ } ++ final AABB target = potentialCollisions.get(i); ++ value = collideY(target, currentBoundingBox, value); ++ } ++ ++ return value; ++ } ++ ++ public static double performAABBCollisionsZ(final AABB currentBoundingBox, double value, final List potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ if (Math.abs(value) < COLLISION_EPSILON) { ++ return 0.0; ++ } ++ final AABB target = potentialCollisions.get(i); ++ value = collideZ(target, currentBoundingBox, value); ++ } ++ ++ return value; ++ } ++ ++ public static double performVoxelCollisionsX(final AABB currentBoundingBox, double value, final List potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ if (Math.abs(value) < COLLISION_EPSILON) { ++ return 0.0; ++ } ++ final VoxelShape target = potentialCollisions.get(i); ++ value = collideX(target, currentBoundingBox, value); ++ } ++ ++ return value; ++ } ++ ++ public static double performVoxelCollisionsY(final AABB currentBoundingBox, double value, final List potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ if (Math.abs(value) < COLLISION_EPSILON) { ++ return 0.0; ++ } ++ final VoxelShape target = potentialCollisions.get(i); ++ value = collideY(target, currentBoundingBox, value); ++ } ++ ++ return value; ++ } ++ ++ public static double performVoxelCollisionsZ(final AABB currentBoundingBox, double value, final List potentialCollisions) { ++ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { ++ if (Math.abs(value) < COLLISION_EPSILON) { ++ return 0.0; ++ } ++ final VoxelShape target = potentialCollisions.get(i); ++ value = collideZ(target, currentBoundingBox, value); ++ } ++ ++ return value; ++ } ++ ++ public static Vec3 performVoxelCollisions(final Vec3 moveVector, AABB axisalignedbb, final List potentialCollisions) { ++ double x = moveVector.x; ++ double y = moveVector.y; ++ double z = moveVector.z; ++ ++ if (y != 0.0) { ++ y = performVoxelCollisionsY(axisalignedbb, y, potentialCollisions); ++ if (y != 0.0) { ++ axisalignedbb = offsetY(axisalignedbb, y); ++ } ++ } ++ ++ final boolean xSmaller = Math.abs(x) < Math.abs(z); ++ ++ if (xSmaller && z != 0.0) { ++ z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions); ++ if (z != 0.0) { ++ axisalignedbb = offsetZ(axisalignedbb, z); ++ } ++ } ++ ++ if (x != 0.0) { ++ x = performVoxelCollisionsX(axisalignedbb, x, potentialCollisions); ++ if (!xSmaller && x != 0.0) { ++ axisalignedbb = offsetX(axisalignedbb, x); ++ } ++ } ++ ++ if (!xSmaller && z != 0.0) { ++ z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions); ++ } ++ ++ return new Vec3(x, y, z); ++ } ++ ++ public static Vec3 performAABBCollisions(final Vec3 moveVector, AABB axisalignedbb, final List potentialCollisions) { ++ double x = moveVector.x; ++ double y = moveVector.y; ++ double z = moveVector.z; ++ ++ if (y != 0.0) { ++ y = performAABBCollisionsY(axisalignedbb, y, potentialCollisions); ++ if (y != 0.0) { ++ axisalignedbb = offsetY(axisalignedbb, y); ++ } ++ } ++ ++ final boolean xSmaller = Math.abs(x) < Math.abs(z); ++ ++ if (xSmaller && z != 0.0) { ++ z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions); ++ if (z != 0.0) { ++ axisalignedbb = offsetZ(axisalignedbb, z); ++ } ++ } ++ ++ if (x != 0.0) { ++ x = performAABBCollisionsX(axisalignedbb, x, potentialCollisions); ++ if (!xSmaller && x != 0.0) { ++ axisalignedbb = offsetX(axisalignedbb, x); ++ } ++ } ++ ++ if (!xSmaller && z != 0.0) { ++ z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions); ++ } ++ ++ return new Vec3(x, y, z); ++ } ++ ++ public static Vec3 performCollisions(final Vec3 moveVector, AABB axisalignedbb, ++ final List voxels, ++ final List aabbs) { ++ if (voxels.isEmpty()) { ++ // fast track only AABBs ++ return performAABBCollisions(moveVector, axisalignedbb, aabbs); ++ } ++ ++ double x = moveVector.x; ++ double y = moveVector.y; ++ double z = moveVector.z; ++ ++ if (y != 0.0) { ++ y = performAABBCollisionsY(axisalignedbb, y, aabbs); ++ y = performVoxelCollisionsY(axisalignedbb, y, voxels); ++ if (y != 0.0) { ++ axisalignedbb = offsetY(axisalignedbb, y); ++ } ++ } ++ ++ final boolean xSmaller = Math.abs(x) < Math.abs(z); ++ ++ if (xSmaller && z != 0.0) { ++ z = performAABBCollisionsZ(axisalignedbb, z, aabbs); ++ z = performVoxelCollisionsZ(axisalignedbb, z, voxels); ++ if (z != 0.0) { ++ axisalignedbb = offsetZ(axisalignedbb, z); ++ } ++ } ++ ++ if (x != 0.0) { ++ x = performAABBCollisionsX(axisalignedbb, x, aabbs); ++ x = performVoxelCollisionsX(axisalignedbb, x, voxels); ++ if (!xSmaller && x != 0.0) { ++ axisalignedbb = offsetX(axisalignedbb, x); ++ } ++ } ++ ++ if (!xSmaller && z != 0.0) { ++ z = performAABBCollisionsZ(axisalignedbb, z, aabbs); ++ z = performVoxelCollisionsZ(axisalignedbb, z, voxels); ++ } ++ ++ return new Vec3(x, y, z); ++ } ++ ++ public static boolean isCollidingWithBorder(final WorldBorder worldborder, final AABB boundingBox) { ++ return isCollidingWithBorder(worldborder, boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ); ++ } ++ ++ public static boolean isCollidingWithBorder(final WorldBorder worldborder, final double boxMinX, final double boxMaxX, ++ final double boxMinZ, final double boxMaxZ) { ++ // border size is rounded like the collide voxel shape of the border ++ final double borderMinX = Math.floor(worldborder.getMinX()); // -X ++ final double borderMaxX = Math.ceil(worldborder.getMaxX()); // +X ++ ++ final double borderMinZ = Math.floor(worldborder.getMinZ()); // -Z ++ final double borderMaxZ = Math.ceil(worldborder.getMaxZ()); // +Z ++ ++ // inverted check for world border enclosing the specified box expanded by -EPSILON ++ return (borderMinX - boxMinX) > CollisionUtil.COLLISION_EPSILON || (borderMaxX - boxMaxX) < -CollisionUtil.COLLISION_EPSILON || ++ (borderMinZ - boxMinZ) > CollisionUtil.COLLISION_EPSILON || (borderMaxZ - boxMaxZ) < -CollisionUtil.COLLISION_EPSILON; ++ } ++ ++ /* Math.max/min specify that any NaN argument results in a NaN return, unlike these functions */ ++ private static double min(final double x, final double y) { ++ return x < y ? x : y; ++ } ++ ++ private static double max(final double x, final double y) { ++ return x > y ? x : y; ++ } ++ ++ public static final int COLLISION_FLAG_LOAD_CHUNKS = 1 << 0; ++ public static final int COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS = 1 << 1; ++ public static final int COLLISION_FLAG_CHECK_BORDER = 1 << 2; ++ public static final int COLLISION_FLAG_CHECK_ONLY = 1 << 3; ++ ++ public static boolean getCollisionsForBlocksOrWorldBorder(final Level world, final Entity entity, final AABB aabb, ++ final List intoVoxel, final List intoAABB, ++ final int collisionFlags, final BiPredicate predicate) { ++ final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0; ++ boolean ret = false; ++ ++ if ((collisionFlags & COLLISION_FLAG_CHECK_BORDER) != 0) { ++ final WorldBorder worldBorder = world.getWorldBorder(); ++ if (CollisionUtil.isCollidingWithBorder(worldBorder, aabb) && entity != null && worldBorder.isInsideCloseToBorder(entity, aabb)) { ++ if (checkOnly) { ++ return true; ++ } else { ++ final VoxelShape borderShape = worldBorder.getCollisionShape(); ++ intoVoxel.add(borderShape); ++ ret = true; ++ } ++ } ++ } ++ ++ final int minSection = world.minSection; ++ ++ final int minBlockX = Mth.floor(aabb.minX - COLLISION_EPSILON) - 1; ++ final int maxBlockX = Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1; ++ ++ final int minBlockY = Math.max((minSection << 4) - 1, Mth.floor(aabb.minY - COLLISION_EPSILON) - 1); ++ final int maxBlockY = Math.min((world.maxSection << 4) + 16, Mth.floor(aabb.maxY + COLLISION_EPSILON) + 1); ++ ++ final int minBlockZ = Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1; ++ final int maxBlockZ = Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1; ++ ++ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); ++ final CollisionContext collisionShape = new LazyEntityCollisionContext(entity); ++ ++ // special cases: ++ if (minBlockY > maxBlockY) { ++ // no point in checking ++ return ret; ++ } ++ ++ final int minChunkX = minBlockX >> 4; ++ final int maxChunkX = maxBlockX >> 4; ++ ++ final int minChunkY = minBlockY >> 4; ++ final int maxChunkY = maxBlockY >> 4; ++ ++ final int minChunkZ = minBlockZ >> 4; ++ final int maxChunkZ = maxBlockZ >> 4; ++ ++ final boolean loadChunks = (collisionFlags & COLLISION_FLAG_LOAD_CHUNKS) != 0; ++ final ServerChunkCache chunkSource = (ServerChunkCache)world.getChunkSource(); ++ ++ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { ++ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { ++ final ChunkAccess chunk = loadChunks ? chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, true) : chunkSource.getChunkAtIfLoadedImmediately(currChunkX, currChunkZ); ++ ++ if (chunk == null) { ++ if ((collisionFlags & COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS) != 0) { ++ if (checkOnly) { ++ return true; ++ } else { ++ intoAABB.add(getBoxForChunk(currChunkX, currChunkZ)); ++ ret = true; ++ } ++ } ++ continue; ++ } ++ ++ final LevelChunkSection[] sections = chunk.getSections(); ++ ++ // bound y ++ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) { ++ final int sectionIdx = currChunkY - minSection; ++ if (sectionIdx < 0 || sectionIdx >= sections.length) { ++ continue; ++ } ++ final LevelChunkSection section = sections[sectionIdx]; ++ if (section == null || section.hasOnlyAir()) { ++ // empty ++ continue; ++ } ++ ++ final boolean hasSpecial = section.getSpecialCollidingBlocks() != 0; ++ final int sectionAdjust = !hasSpecial ? 1 : 0; ++ ++ final PalettedContainer blocks = section.states; ++ ++ final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) + sectionAdjust : 0; ++ final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) - sectionAdjust : 15; ++ final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) + sectionAdjust : 0; ++ final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) - sectionAdjust : 15; ++ final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) + sectionAdjust : 0; ++ final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) - sectionAdjust : 15; ++ ++ for (int currY = minYIterate; currY <= maxYIterate; ++currY) { ++ final int blockY = currY | (currChunkY << 4); ++ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) { ++ final int blockZ = currZ | (currChunkZ << 4); ++ for (int currX = minXIterate; currX <= maxXIterate; ++currX) { ++ final int localBlockIndex = (currX) | (currZ << 4) | ((currY) << 8); ++ final int blockX = currX | (currChunkX << 4); ++ ++ final int edgeCount = hasSpecial ? ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) + ++ ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) + ++ ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0) : 0; ++ if (edgeCount == 3) { ++ continue; ++ } ++ ++ final BlockState blockData = blocks.get(localBlockIndex); ++ ++ if (blockData.emptyCollisionShape()) { ++ continue; ++ } ++ ++ if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON))) { ++ VoxelShape blockCollision = blockData.getConstantCollisionShape(); ++ ++ if (blockCollision == null) { ++ mutablePos.set(blockX, blockY, blockZ); ++ blockCollision = blockData.getCollisionShape(world, mutablePos, collisionShape); ++ } ++ ++ AABB singleAABB = blockCollision.getSingleAABBRepresentation(); ++ if (singleAABB != null) { ++ singleAABB = singleAABB.move((double)blockX, (double)blockY, (double)blockZ); ++ if (!voxelShapeIntersect(aabb, singleAABB)) { ++ continue; ++ } ++ ++ if (predicate != null) { ++ mutablePos.set(blockX, blockY, blockZ); ++ if (!predicate.test(blockData, mutablePos)) { ++ continue; ++ } ++ } ++ ++ if (checkOnly) { ++ return true; ++ } else { ++ ret = true; ++ intoAABB.add(singleAABB); ++ continue; ++ } ++ } ++ ++ if (blockCollision.isEmpty()) { ++ continue; ++ } ++ ++ final VoxelShape blockCollisionOffset = blockCollision.move((double)blockX, (double)blockY, (double)blockZ); ++ ++ if (!voxelShapeIntersectNoEmpty(blockCollisionOffset, aabb)) { ++ continue; ++ } ++ ++ if (predicate != null) { ++ mutablePos.set(blockX, blockY, blockZ); ++ if (!predicate.test(blockData, mutablePos)) { ++ continue; ++ } ++ } ++ ++ if (checkOnly) { ++ return true; ++ } else { ++ ret = true; ++ intoVoxel.add(blockCollisionOffset); ++ continue; ++ } ++ } ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ return ret; ++ } ++ ++ public static boolean getEntityHardCollisions(final CollisionGetter getter, final Entity entity, AABB aabb, ++ final List into, final int collisionFlags, final Predicate predicate) { ++ final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0; ++ if (!(getter instanceof EntityGetter entityGetter)) { ++ return false; ++ } ++ ++ boolean ret = false; ++ ++ // to comply with vanilla intersection rules, expand by -epsilon so that we only get stuff we definitely collide with. ++ // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems ++ // specifically with boat collisions. ++ aabb = aabb.inflate(-COLLISION_EPSILON, -COLLISION_EPSILON, -COLLISION_EPSILON); ++ final List entities; ++ if (entity != null && entity.hardCollides()) { ++ entities = entityGetter.getEntities(entity, aabb, predicate); ++ } else { ++ entities = entityGetter.getHardCollidingEntities(entity, aabb, predicate); ++ } ++ ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ final Entity otherEntity = entities.get(i); ++ ++ if (otherEntity.isSpectator()) { ++ continue; ++ } ++ ++ if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) { ++ if (checkOnly) { ++ return true; ++ } else { ++ into.add(otherEntity.getBoundingBox()); ++ ret = true; ++ } ++ } ++ } ++ ++ return ret; ++ } ++ ++ public static boolean getCollisions(final Level world, final Entity entity, final AABB aabb, ++ final List intoVoxel, final List intoAABB, final int collisionFlags, ++ final BiPredicate blockPredicate, ++ final Predicate entityPredicate) { ++ if ((collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0) { ++ return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate) ++ || getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate); ++ } else { ++ return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate) ++ | getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate); ++ } ++ } ++ ++ public static final class LazyEntityCollisionContext extends EntityCollisionContext { ++ ++ private CollisionContext delegate; ++ private boolean delegated; ++ ++ public LazyEntityCollisionContext(final Entity entity) { ++ super(false, 0.0, null, null, entity); ++ } ++ ++ public boolean isDelegated() { ++ final boolean delegated = this.delegated; ++ this.delegated = false; ++ return delegated; ++ } ++ ++ public CollisionContext getDelegate() { ++ this.delegated = true; ++ final Entity entity = this.getEntity(); ++ return this.delegate == null ? this.delegate = (entity == null ? CollisionContext.empty() : CollisionContext.of(entity)) : this.delegate; ++ } ++ ++ @Override ++ public boolean isDescending() { ++ return this.getDelegate().isDescending(); ++ } ++ ++ @Override ++ public boolean isAbove(final VoxelShape shape, final BlockPos pos, final boolean defaultValue) { ++ return this.getDelegate().isAbove(shape, pos, defaultValue); ++ } ++ ++ @Override ++ public boolean isHoldingItem(final Item item) { ++ return this.getDelegate().isHoldingItem(item); ++ } ++ ++ @Override ++ public boolean canStandOnFluid(final FluidState state, final FluidState fluidState) { ++ return this.getDelegate().canStandOnFluid(state, fluidState); ++ } ++ } ++ ++ private CollisionUtil() { ++ throw new RuntimeException(); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/collisions/CachedShapeData.java b/src/main/java/io/papermc/paper/util/collisions/CachedShapeData.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1cb96b09375770f92f3e494ce2f28d9ff8699581 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/collisions/CachedShapeData.java +@@ -0,0 +1,10 @@ ++package io.papermc.paper.util.collisions; ++ ++public record CachedShapeData( ++ int sizeX, int sizeY, int sizeZ, ++ long[] voxelSet, ++ int minFullX, int minFullY, int minFullZ, ++ int maxFullX, int maxFullY, int maxFullZ, ++ boolean isEmpty, boolean hasSingleAABB ++) { ++} +diff --git a/src/main/java/io/papermc/paper/util/collisions/CachedToAABBs.java b/src/main/java/io/papermc/paper/util/collisions/CachedToAABBs.java +new file mode 100644 +index 0000000000000000000000000000000000000000..85c448a775f60ca4b4a4f2baf17487ef45bdd383 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/collisions/CachedToAABBs.java +@@ -0,0 +1,39 @@ ++package io.papermc.paper.util.collisions; ++ ++import net.minecraft.world.phys.AABB; ++import java.util.ArrayList; ++import java.util.List; ++ ++public record CachedToAABBs( ++ List aabbs, ++ boolean isOffset, ++ double offX, double offY, double offZ ++) { ++ ++ public CachedToAABBs removeOffset() { ++ final List toOffset = this.aabbs; ++ final double offX = this.offX; ++ final double offY = this.offY; ++ final double offZ = this.offZ; ++ ++ final List ret = new ArrayList<>(toOffset.size()); ++ ++ for (int i = 0, len = toOffset.size(); i < len; ++i) { ++ ret.add(toOffset.get(i).move(offX, offY, offZ)); ++ } ++ ++ return new CachedToAABBs(ret, false, 0.0, 0.0, 0.0); ++ } ++ ++ public static CachedToAABBs offset(final CachedToAABBs cache, final double offX, final double offY, final double offZ) { ++ if (offX == 0.0 && offY == 0.0 && offZ == 0.0) { ++ return cache; ++ } ++ ++ final double resX = cache.offX + offX; ++ final double resY = cache.offY + offY; ++ final double resZ = cache.offZ + offZ; ++ ++ return new CachedToAABBs(cache.aabbs, true, resX, resY, resZ); ++ } ++} +diff --git a/src/main/java/io/papermc/paper/util/collisions/FlatBitsetUtil.java b/src/main/java/io/papermc/paper/util/collisions/FlatBitsetUtil.java +new file mode 100644 +index 0000000000000000000000000000000000000000..ff9d2dad39dcc02b2371458b7b5f64c6090e8012 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/collisions/FlatBitsetUtil.java +@@ -0,0 +1,109 @@ ++package io.papermc.paper.util.collisions; ++ ++import java.util.Objects; ++ ++public final class FlatBitsetUtil { ++ ++ private static final int LOG2_LONG = 6; ++ private static final long ALL_SET = -1L; ++ private static final int BITS_PER_LONG = Long.SIZE; ++ ++ // from inclusive ++ // to exclusive ++ public static int firstSet(final long[] bitset, final int from, final int to) { ++ if ((from | to | (to - from)) < 0) { ++ throw new IndexOutOfBoundsException(); ++ } ++ ++ int bitsetIdx = from >>> LOG2_LONG; ++ int bitIdx = from & ~(BITS_PER_LONG - 1); ++ ++ long tmp = bitset[bitsetIdx] & (ALL_SET << from); ++ for (;;) { ++ if (tmp != 0L) { ++ final int ret = bitIdx | Long.numberOfTrailingZeros(tmp); ++ return ret >= to ? -1 : ret; ++ } ++ ++ bitIdx += BITS_PER_LONG; ++ ++ if (bitIdx >= to) { ++ return -1; ++ } ++ ++ tmp = bitset[++bitsetIdx]; ++ } ++ } ++ ++ // from inclusive ++ // to exclusive ++ public static int firstClear(final long[] bitset, final int from, final int to) { ++ if ((from | to | (to - from)) < 0) { ++ throw new IndexOutOfBoundsException(); ++ } ++ // like firstSet, but invert the bitset ++ ++ int bitsetIdx = from >>> LOG2_LONG; ++ int bitIdx = from & ~(BITS_PER_LONG - 1); ++ ++ long tmp = (~bitset[bitsetIdx]) & (ALL_SET << from); ++ for (;;) { ++ if (tmp != 0L) { ++ final int ret = bitIdx | Long.numberOfTrailingZeros(tmp); ++ return ret >= to ? -1 : ret; ++ } ++ ++ bitIdx += BITS_PER_LONG; ++ ++ if (bitIdx >= to) { ++ return -1; ++ } ++ ++ tmp = ~bitset[++bitsetIdx]; ++ } ++ } ++ ++ // from inclusive ++ // to exclusive ++ public static void clearRange(final long[] bitset, final int from, int to) { ++ if ((from | to | (to - from)) < 0) { ++ throw new IndexOutOfBoundsException(); ++ } ++ ++ if (from == to) { ++ return; ++ } ++ ++ --to; ++ ++ final int fromBitsetIdx = from >>> LOG2_LONG; ++ final int toBitsetIdx = to >>> LOG2_LONG; ++ ++ final long keepFirst = ~(ALL_SET << from); ++ final long keepLast = ~(ALL_SET >>> ((BITS_PER_LONG - 1) ^ to)); ++ ++ Objects.checkFromToIndex(fromBitsetIdx, toBitsetIdx, bitset.length); ++ ++ if (fromBitsetIdx == toBitsetIdx) { ++ // special case: need to keep both first and last ++ bitset[fromBitsetIdx] &= (keepFirst | keepLast); ++ } else { ++ bitset[fromBitsetIdx] &= keepFirst; ++ ++ for (int i = fromBitsetIdx + 1; i < toBitsetIdx; ++i) { ++ bitset[i] = 0L; ++ } ++ ++ bitset[toBitsetIdx] &= keepLast; ++ } ++ } ++ ++ // from inclusive ++ // to exclusive ++ public static boolean isRangeSet(final long[] bitset, final int from, final int to) { ++ return firstClear(bitset, from, to) == -1; ++ } ++ ++ ++ private FlatBitsetUtil() {} ++} +diff --git a/src/main/java/io/papermc/paper/util/collisions/MergedORCache.java b/src/main/java/io/papermc/paper/util/collisions/MergedORCache.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1f42bdfdb052056e62a939ab0d1944f8a064fe6c +--- /dev/null ++++ b/src/main/java/io/papermc/paper/util/collisions/MergedORCache.java +@@ -0,0 +1,10 @@ ++package io.papermc.paper.util.collisions; ++ ++import net.minecraft.world.phys.shapes.VoxelShape; ++ ++public record MergedORCache( ++ VoxelShape key, ++ VoxelShape result ++) { ++ ++} +diff --git a/src/main/java/net/minecraft/core/Direction.java b/src/main/java/net/minecraft/core/Direction.java +index 03c45ee77276462818a6f774b5945b25924aa3f0..ab289a6ca85459e03acb2089c6b9e931caa9c873 100644 +--- a/src/main/java/net/minecraft/core/Direction.java ++++ b/src/main/java/net/minecraft/core/Direction.java +@@ -60,6 +60,21 @@ public enum Direction implements StringRepresentable { + private final int adjY; + private final int adjZ; + // Paper end - Perf: Inline shift direction fields ++ // Paper start - optimise collisions ++ private static final int RANDOM_OFFSET = 2017601568; ++ private Direction opposite; ++ static { ++ for (final Direction direction : VALUES) { ++ direction.opposite = from3DDataValue(direction.oppositeIndex); ++ } ++ } ++ ++ private final int id = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(this.ordinal() + RANDOM_OFFSET) + RANDOM_OFFSET); ++ ++ public final int uniqueId() { ++ return this.id; ++ } ++ // Paper end - optimise collisions + + private Direction( + final int id, +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index e06b9b0c7b1c3a200b89819e0e521f1c88dc981b..ed42a78aebeda86b77f27cba4fcefbc6c17f86fc 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -512,7 +512,7 @@ public class ServerPlayer extends Player { + + if (blockposition1 != null) { + this.moveTo(blockposition1, world.getSharedSpawnAngle(), 0.0F); // Paper - MC-200092 - fix first spawn pos yaw being ignored +- if (world.noCollision((Entity) this)) { ++ if (world.noCollision(this, this.getBoundingBox(), true)) { // Paper - make sure this loads chunks, we default to NOT loading now + break; + } + } +@@ -520,7 +520,7 @@ public class ServerPlayer extends Player { + } else { + this.moveTo(blockposition, world.getSharedSpawnAngle(), 0.0F); // Paper - MC-200092 - fix first spawn pos yaw being ignored + +- while (!world.noCollision((Entity) this) && this.getY() < (double) (world.getMaxBuildHeight() - 1)) { ++ while (!world.noCollision(this, this.getBoundingBox(), true) && this.getY() < (double) (world.getMaxBuildHeight() - 1)) { // Paper - make sure this loads chunks, we default to NOT loading now + this.setPos(this.getX(), this.getY() + 1.0D, this.getZ()); + } + } +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 60ba289e724463129dfb27aa5e3b6daf3dd7386e..68446b7532dfbda303293aa9e756644c6fcdffca 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -935,7 +935,7 @@ public abstract class PlayerList { + entityplayer1.forceSetPositionRotation(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch()); + + worldserver1.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.POST_TELEPORT, new net.minecraft.world.level.ChunkPos(location.getBlockX() >> 4, location.getBlockZ() >> 4), 1, entityplayer.getId()); // Paper +- while (avoidSuffocation && !worldserver1.noCollision((Entity) entityplayer1) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) { ++ while (avoidSuffocation && !worldserver1.noCollision(entityplayer1, entityplayer1.getBoundingBox(), true) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) { // Paper - make sure this loads chunks, we default to NOT loading now + // CraftBukkit end + entityplayer1.setPos(entityplayer1.getX(), entityplayer1.getY() + 1.0D, entityplayer1.getZ()); + } +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 0fbcf60a994f67bdd81d40e4a8bf38f0cbb8993d..8ad2d17615ff489b2fbbb13480dd0b217a42d805 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -1261,9 +1261,44 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + float f = this.getBlockSpeedFactor(); + + this.setDeltaMovement(this.getDeltaMovement().multiply((double) f, 1.0D, (double) f)); +- if (this.level().getBlockStatesIfLoaded(this.getBoundingBox().deflate(1.0E-6D)).noneMatch((iblockdata2) -> { +- return iblockdata2.is(BlockTags.FIRE) || iblockdata2.is(Blocks.LAVA); +- })) { ++ // Paper start - remove expensive streams from here ++ boolean noneMatch = true; ++ AABB fireSearchBox = this.getBoundingBox().deflate(1.0E-6D); ++ { ++ int minX = Mth.floor(fireSearchBox.minX); ++ int minY = Mth.floor(fireSearchBox.minY); ++ int minZ = Mth.floor(fireSearchBox.minZ); ++ int maxX = Mth.floor(fireSearchBox.maxX); ++ int maxY = Mth.floor(fireSearchBox.maxY); ++ int maxZ = Mth.floor(fireSearchBox.maxZ); ++ fire_search_loop: ++ for (int fz = minZ; fz <= maxZ; ++fz) { ++ for (int fx = minX; fx <= maxX; ++fx) { ++ for (int fy = minY; fy <= maxY; ++fy) { ++ net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)this.level.getChunkIfLoadedImmediately(fx >> 4, fz >> 4); ++ if (chunk == null) { ++ // Vanilla rets an empty stream if all the chunks are not loaded, so noneMatch will be true ++ // even if we're in lava/fire ++ noneMatch = true; ++ break fire_search_loop; ++ } ++ if (!noneMatch) { ++ // don't do get type, we already know we're in fire - we just need to check the chunks ++ // loaded state ++ continue; ++ } ++ ++ BlockState type = chunk.getBlockStateFinal(fx, fy, fz); ++ if (type.is(BlockTags.FIRE) || type.is(Blocks.LAVA)) { ++ noneMatch = false; ++ // can't break, we need to retain vanilla behavior by ensuring ALL chunks are loaded ++ } ++ } ++ } ++ } ++ } ++ if (noneMatch) { ++ // Paper end - remove expensive streams from here + if (this.remainingFireTicks <= 0) { + this.setRemainingFireTicks(-this.getFireImmuneTicks()); + } +@@ -1443,32 +1478,82 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + private Vec3 collide(Vec3 movement) { +- AABB axisalignedbb = this.getBoundingBox(); +- List list = this.level().getEntityCollisions(this, axisalignedbb.expandTowards(movement)); +- Vec3 vec3d1 = movement.lengthSqr() == 0.0D ? movement : Entity.collideBoundingBox(this, movement, axisalignedbb, this.level(), list); +- boolean flag = movement.x != vec3d1.x; +- boolean flag1 = movement.y != vec3d1.y; +- boolean flag2 = movement.z != vec3d1.z; +- boolean flag3 = this.onGround() || flag1 && movement.y < 0.0D; ++ // Paper start - optimise collisions ++ final boolean xZero = movement.x == 0.0; ++ final boolean yZero = movement.y == 0.0; ++ final boolean zZero = movement.z == 0.0; ++ if (xZero & yZero & zZero) { ++ return movement; ++ } ++ ++ final Level world = this.level; ++ final AABB currBoundingBox = this.getBoundingBox(); ++ ++ if (io.papermc.paper.util.CollisionUtil.isEmpty(currBoundingBox)) { ++ return movement; ++ } ++ ++ final List potentialCollisionsBB = new java.util.ArrayList<>(); ++ final List potentialCollisionsVoxel = new java.util.ArrayList<>(); ++ final double stepHeight = (double)this.maxUpStep(); ++ final AABB collisionBox; ++ final boolean onGround = this.onGround; ++ ++ if (xZero & zZero) { ++ if (movement.y > 0.0) { ++ collisionBox = io.papermc.paper.util.CollisionUtil.cutUpwards(currBoundingBox, movement.y); ++ } else { ++ collisionBox = io.papermc.paper.util.CollisionUtil.cutDownwards(currBoundingBox, movement.y); ++ } ++ } else { ++ // note: xZero == false or zZero == false ++ if (stepHeight > 0.0 && (onGround || (movement.y < 0.0))) { ++ // don't bother getting the collisions if we don't need them. ++ if (movement.y <= 0.0) { ++ collisionBox = io.papermc.paper.util.CollisionUtil.expandUpwards(currBoundingBox.expandTowards(movement.x, movement.y, movement.z), stepHeight); ++ } else { ++ collisionBox = currBoundingBox.expandTowards(movement.x, Math.max(stepHeight, movement.y), movement.z); ++ } ++ } else { ++ collisionBox = currBoundingBox.expandTowards(movement.x, movement.y, movement.z); ++ } ++ } ++ ++ io.papermc.paper.util.CollisionUtil.getCollisions( ++ world, this, collisionBox, potentialCollisionsVoxel, potentialCollisionsBB, ++ io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, ++ null, null ++ ); ++ ++ if (potentialCollisionsVoxel.isEmpty() && potentialCollisionsBB.isEmpty()) { ++ return movement; ++ } + +- if (this.maxUpStep() > 0.0F && flag3 && (flag || flag2)) { +- Vec3 vec3d2 = Entity.collideBoundingBox(this, new Vec3(movement.x, (double) this.maxUpStep(), movement.z), axisalignedbb, this.level(), list); +- Vec3 vec3d3 = Entity.collideBoundingBox(this, new Vec3(0.0D, (double) this.maxUpStep(), 0.0D), axisalignedbb.expandTowards(movement.x, 0.0D, movement.z), this.level(), list); ++ final Vec3 limitedMoveVector = io.papermc.paper.util.CollisionUtil.performCollisions(movement, currBoundingBox, potentialCollisionsVoxel, potentialCollisionsBB); + +- if (vec3d3.y < (double) this.maxUpStep()) { +- Vec3 vec3d4 = Entity.collideBoundingBox(this, new Vec3(movement.x, 0.0D, movement.z), axisalignedbb.move(vec3d3), this.level(), list).add(vec3d3); ++ if (stepHeight > 0.0 ++ && (onGround || (limitedMoveVector.y != movement.y && movement.y < 0.0)) ++ && (limitedMoveVector.x != movement.x || limitedMoveVector.z != movement.z)) { ++ Vec3 vec3d2 = io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(movement.x, stepHeight, movement.z), currBoundingBox, potentialCollisionsVoxel, potentialCollisionsBB); ++ final Vec3 vec3d3 = io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(0.0, stepHeight, 0.0), currBoundingBox.expandTowards(movement.x, 0.0, movement.z), potentialCollisionsVoxel, potentialCollisionsBB); ++ ++ if (vec3d3.y < stepHeight) { ++ final Vec3 vec3d4 = io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(movement.x, 0.0D, movement.z), currBoundingBox.move(vec3d3), potentialCollisionsVoxel, potentialCollisionsBB).add(vec3d3); + + if (vec3d4.horizontalDistanceSqr() > vec3d2.horizontalDistanceSqr()) { + vec3d2 = vec3d4; + } + } + +- if (vec3d2.horizontalDistanceSqr() > vec3d1.horizontalDistanceSqr()) { +- return vec3d2.add(Entity.collideBoundingBox(this, new Vec3(0.0D, -vec3d2.y + movement.y, 0.0D), axisalignedbb.move(vec3d2), this.level(), list)); ++ if (vec3d2.horizontalDistanceSqr() > limitedMoveVector.horizontalDistanceSqr()) { ++ return vec3d2.add(io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(0.0D, -vec3d2.y + movement.y, 0.0D), currBoundingBox.move(vec3d2), potentialCollisionsVoxel, potentialCollisionsBB)); + } +- } + +- return vec3d1; ++ return limitedMoveVector; ++ } else { ++ return limitedMoveVector; ++ } ++ // Paper end - optimise collisions + } + + public static Vec3 collideBoundingBox(@Nullable Entity entity, Vec3 movement, AABB entityBoundingBox, Level world, List collisions) { +@@ -2735,11 +2820,70 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + float f = this.dimensions.width() * 0.8F; + AABB axisalignedbb = AABB.ofSize(this.getEyePosition(), (double) f, 1.0E-6D, (double) f); + +- return BlockPos.betweenClosedStream(axisalignedbb).anyMatch((blockposition) -> { +- BlockState iblockdata = this.level().getBlockState(blockposition); ++ // Paper start - optimise collisions ++ if (io.papermc.paper.util.CollisionUtil.isEmpty(axisalignedbb)) { ++ return false; ++ } + +- return !iblockdata.isAir() && iblockdata.isSuffocating(this.level(), blockposition) && Shapes.joinIsNotEmpty(iblockdata.getCollisionShape(this.level(), blockposition).move((double) blockposition.getX(), (double) blockposition.getY(), (double) blockposition.getZ()), Shapes.create(axisalignedbb), BooleanOp.AND); +- }); ++ final BlockPos.MutableBlockPos tempPos = new BlockPos.MutableBlockPos(); ++ ++ final int minX = Mth.floor(axisalignedbb.minX); ++ final int minY = Mth.floor(axisalignedbb.minY); ++ final int minZ = Mth.floor(axisalignedbb.minZ); ++ final int maxX = Mth.floor(axisalignedbb.maxX); ++ final int maxY = Mth.floor(axisalignedbb.maxY); ++ final int maxZ = Mth.floor(axisalignedbb.maxZ); ++ ++ final net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.level.getChunkSource(); ++ ++ long lastChunkKey = ChunkPos.INVALID_CHUNK_POS; ++ net.minecraft.world.level.chunk.LevelChunk lastChunk = null; ++ for (int fz = minZ; fz <= maxZ; ++fz) { ++ tempPos.setZ(fz); ++ for (int fx = minX; fx <= maxX; ++fx) { ++ final int newChunkX = fx >> 4; ++ final int newChunkZ = fz >> 4; ++ final net.minecraft.world.level.chunk.LevelChunk chunk = lastChunkKey == (lastChunkKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(newChunkX, newChunkZ)) ? ++ lastChunk : (lastChunk = chunkProvider.getChunkAtIfLoadedImmediately(newChunkX, newChunkZ)); ++ tempPos.setX(fx); ++ if (chunk == null) { ++ continue; ++ } ++ for (int fy = minY; fy <= maxY; ++fy) { ++ tempPos.setY(fy); ++ ++ final BlockState state = chunk.getBlockState(tempPos); ++ ++ if (state.emptyCollisionShape() || !state.isSuffocating(this.level, tempPos)) { ++ continue; ++ } ++ ++ // Yes, it does not use the Entity context stuff. ++ final VoxelShape collisionShape = state.getCollisionShape(this.level, tempPos); ++ ++ if (collisionShape.isEmpty()) { ++ continue; ++ } ++ ++ final AABB toCollide = axisalignedbb.move(-(double)fx, -(double)fy, -(double)fz); ++ ++ final AABB singleAABB = collisionShape.getSingleAABBRepresentation(); ++ if (singleAABB != null) { ++ if (io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(singleAABB, toCollide)) { ++ return true; ++ } ++ continue; ++ } ++ ++ if (io.papermc.paper.util.CollisionUtil.voxelShapeIntersectNoEmpty(collisionShape, toCollide)) { ++ return true; ++ } ++ continue; ++ } ++ } ++ } ++ // Paper end - optimise collisions ++ return false; + } + } + +diff --git a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java +index 2ed6845f16fab175e2e9e96e76391e63ab4a43e2..c2bd2e303f956d390319f6bbbe9a6492ebec5154 100644 +--- a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java ++++ b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java +@@ -360,7 +360,7 @@ public class ArmorStand extends LivingEntity { + @Override + protected void pushEntities() { + if (!this.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return; // Paper - Option to prevent armor stands from doing entity lookups +- List list = this.level().getEntities((Entity) this, this.getBoundingBox(), ArmorStand.RIDABLE_MINECARTS); ++ List list = this.level().getEntitiesOfClass(AbstractMinecart.class, this.getBoundingBox(), ArmorStand.RIDABLE_MINECARTS); // Paper - optimise collisions + Iterator iterator = list.iterator(); + + while (iterator.hasNext()) { +diff --git a/src/main/java/net/minecraft/world/entity/monster/Spider.java b/src/main/java/net/minecraft/world/entity/monster/Spider.java +index f0127f1b55999aa4a841341ad02cbcde45702b50..ef8911f7bcf6a97496675abb4689bb09cf322e85 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/Spider.java ++++ b/src/main/java/net/minecraft/world/entity/monster/Spider.java +@@ -82,7 +82,7 @@ public class Spider extends Monster { + public void tick() { + super.tick(); + if (!this.level().isClientSide) { +- this.setClimbing(this.horizontalCollision && (this.level().paperConfig().entities.behavior.allowSpiderWorldBorderClimbing)); // Paper - Add config option for spider worldborder climbing ++ this.setClimbing(this.horizontalCollision && (this.level().paperConfig().entities.behavior.allowSpiderWorldBorderClimbing || !io.papermc.paper.util.CollisionUtil.isCollidingWithBorder(this.level().getWorldBorder(), this.getBoundingBox().inflate(io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)))); // Paper - Add config option for spider worldborder climbing & Inflate by +EPSILON as collision will just barely place us outside border + } + + } +diff --git a/src/main/java/net/minecraft/world/level/BlockCollisions.java b/src/main/java/net/minecraft/world/level/BlockCollisions.java +index cd89623a44f02d7db77f0d0f87545cf80841f403..48710a60561824a3670ebef3601f284dd7089481 100644 +--- a/src/main/java/net/minecraft/world/level/BlockCollisions.java ++++ b/src/main/java/net/minecraft/world/level/BlockCollisions.java +@@ -99,7 +99,7 @@ public class BlockCollisions extends AbstractIterator { + // Paper end + VoxelShape voxelShape = blockState.getCollisionShape(this.collisionGetter, this.pos, this.context); + if (voxelShape == Shapes.block()) { +- if (this.box.intersects((double)i, (double)j, (double)k, (double)i + 1.0, (double)j + 1.0, (double)k + 1.0)) { ++ if (io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(this.box, i, j, k, i + 1.0, j + 1.0, k + 1.0)) { // Paper - keep vanilla behavior for voxelshape intersection - See comment in CollisionUtil + return this.resultProvider.apply(this.pos, voxelShape.move((double)i, (double)j, (double)k)); + } + } else { +diff --git a/src/main/java/net/minecraft/world/level/ClipContext.java b/src/main/java/net/minecraft/world/level/ClipContext.java +index 3fa2964b979053ecbefc946c7fe76828de86d8f1..2634ba06147c936b0fae08a190c7820c832e6b23 100644 +--- a/src/main/java/net/minecraft/world/level/ClipContext.java ++++ b/src/main/java/net/minecraft/world/level/ClipContext.java +@@ -17,8 +17,8 @@ public class ClipContext { + + private final Vec3 from; + private final Vec3 to; +- private final ClipContext.Block block; +- private final ClipContext.Fluid fluid; ++ public final ClipContext.Block block; // Paper - optimise collisions - public ++ public final ClipContext.Fluid fluid; // Paper - optimise collisions - public + private final CollisionContext collisionContext; + + public ClipContext(Vec3 start, Vec3 end, ClipContext.Block shapeType, ClipContext.Fluid fluidHandling, Entity entity) { +diff --git a/src/main/java/net/minecraft/world/level/CollisionGetter.java b/src/main/java/net/minecraft/world/level/CollisionGetter.java +index 1ad0c976c6e2d6d31397dff850a9de7c16d16fba..dc877fe2e3c53b353baa59c125232e425fee67d7 100644 +--- a/src/main/java/net/minecraft/world/level/CollisionGetter.java ++++ b/src/main/java/net/minecraft/world/level/CollisionGetter.java +@@ -35,6 +35,12 @@ public interface CollisionGetter extends BlockGetter { + return this.isUnobstructed(entity, Shapes.create(entity.getBoundingBox())); + } + ++ // Paper start - optimise collisions ++ default boolean noCollision(Entity entity, AABB box, boolean loadChunks) { ++ return this.noCollision(entity, box); ++ } ++ // Paper end - optimise collisions ++ + default boolean noCollision(AABB box) { + return this.noCollision(null, box); + } +diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java +index 9a28912f52824acdc80a62243b136e6f365bf567..21843501355a0c0c8d594e3e5312e97861c9a777 100644 +--- a/src/main/java/net/minecraft/world/level/EntityGetter.java ++++ b/src/main/java/net/minecraft/world/level/EntityGetter.java +@@ -46,20 +46,36 @@ public interface EntityGetter { + } + + default boolean isUnobstructed(@Nullable Entity except, VoxelShape shape) { ++ // Paper start - optimise collisions + if (shape.isEmpty()) { +- return true; +- } else { +- for (Entity entity : this.getEntities(except, shape.bounds())) { +- if (!entity.isRemoved() +- && entity.blocksBuilding +- && (except == null || !entity.isPassengerOfSameVehicle(except)) +- && Shapes.joinIsNotEmpty(shape, Shapes.create(entity.getBoundingBox()), BooleanOp.AND)) { +- return false; ++ return false; ++ } ++ ++ final AABB singleAABB = shape.getSingleAABBRepresentation(); ++ final List entities = this.getEntities( ++ except, ++ singleAABB == null ? shape.bounds() : singleAABB.inflate(-io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) ++ ); ++ ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ final Entity otherEntity = entities.get(i); ++ ++ if (otherEntity.isRemoved() || !otherEntity.blocksBuilding || (except != null && otherEntity.isPassengerOfSameVehicle(except))) { ++ continue; ++ } ++ ++ if (singleAABB == null) { ++ final AABB entityBB = otherEntity.getBoundingBox(); ++ if (io.papermc.paper.util.CollisionUtil.isEmpty(entityBB) || !io.papermc.paper.util.CollisionUtil.voxelShapeIntersectNoEmpty(shape, entityBB)) { ++ continue; + } + } + +- return true; ++ return false; + } ++ ++ return true; ++ // Paper end - optimise collisions + } + + default List getEntitiesOfClass(Class entityClass, AABB box) { +@@ -67,23 +83,41 @@ public interface EntityGetter { + } + + default List getEntityCollisions(@Nullable Entity entity, AABB box) { +- if (box.getSize() < 1.0E-7) { +- return List.of(); ++ // Paper start - optimise collisions ++ // first behavior change is to correctly check for empty AABB ++ if (io.papermc.paper.util.CollisionUtil.isEmpty(box)) { ++ // reduce indirection by always returning type with same class ++ return new java.util.ArrayList<>(); ++ } ++ ++ // to comply with vanilla intersection rules, expand by -epsilon so that we only get stuff we definitely collide with. ++ // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems ++ // specifically with boat collisions. ++ box = box.inflate(-io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON); ++ ++ final List entities; ++ if (entity != null && entity.hardCollides()) { ++ entities = this.getEntities(entity, box, null); + } else { +- Predicate predicate = entity == null ? EntitySelector.CAN_BE_COLLIDED_WITH : EntitySelector.NO_SPECTATORS.and(entity::canCollideWith); +- List list = this.getEntities(entity, box.inflate(1.0E-7), predicate); +- if (list.isEmpty()) { +- return List.of(); +- } else { +- Builder builder = ImmutableList.builderWithExpectedSize(list.size()); +- +- for (Entity entity2 : list) { +- builder.add(Shapes.create(entity2.getBoundingBox())); +- } ++ entities = this.getHardCollidingEntities(entity, box, null); ++ } ++ ++ final List ret = new java.util.ArrayList<>(Math.min(25, entities.size())); + +- return builder.build(); ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ final Entity otherEntity = entities.get(i); ++ ++ if (otherEntity.isSpectator()) { ++ continue; ++ } ++ ++ if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) { ++ ret.add(Shapes.create(otherEntity.getBoundingBox())); + } + } ++ ++ return ret; ++ // Paper end - optimise collisions + } + + // Paper start - Affects Spawning API +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 4001bec504b8fcda1eec03f49abd4e4d82cf6336..07281d73fcbca2ea5f8bce25f6bf961d258bf8a0 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -287,6 +287,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + this.entityLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.entityMaxTickTime); + this.tileLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.tileMaxTickTime); + this.chunkPacketBlockController = this.paperConfig().anticheat.antiXray.enabled ? new com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor) : com.destroystokyo.paper.antixray.ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray ++ // Paper start - optimise collisions ++ this.minSection = io.papermc.paper.util.WorldUtil.getMinSection(this); ++ this.maxSection = io.papermc.paper.util.WorldUtil.getMaxSection(this); ++ // Paper end - optimise collisions + } + + // Paper start - Cancel hit for vanished players +@@ -328,6 +332,366 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + return true; + } + // Paper end - Cancel hit for vanished players ++ // Paper start - optimise collisions ++ public final int minSection; ++ public final int maxSection; ++ ++ @Override ++ public final boolean isUnobstructed(final Entity entity) { ++ final AABB boundingBox = entity.getBoundingBox(); ++ if (io.papermc.paper.util.CollisionUtil.isEmpty(boundingBox)) { ++ return false; ++ } ++ ++ final List entities = this.getEntities( ++ entity, ++ boundingBox.inflate(-io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON), ++ null ++ ); ++ ++ for (int i = 0, len = entities.size(); i < len; ++i) { ++ final Entity otherEntity = entities.get(i); ++ ++ if (otherEntity.isSpectator() || otherEntity.isRemoved() || !otherEntity.blocksBuilding || otherEntity.isPassengerOfSameVehicle(entity)) { ++ continue; ++ } ++ ++ return false; ++ } ++ ++ return true; ++ } ++ ++ private static net.minecraft.world.phys.BlockHitResult miss(final ClipContext clipContext) { ++ final Vec3 to = clipContext.getTo(); ++ final Vec3 from = clipContext.getFrom(); ++ ++ return net.minecraft.world.phys.BlockHitResult.miss(to, Direction.getNearest(from.x - to.x, from.y - to.y, from.z - to.z), BlockPos.containing(to.x, to.y, to.z)); ++ } ++ ++ private static final FluidState AIR_FLUIDSTATE = Fluids.EMPTY.defaultFluidState(); ++ ++ private static net.minecraft.world.phys.BlockHitResult fastClip(final Vec3 from, final Vec3 to, final Level level, ++ final ClipContext clipContext) { ++ final double adjX = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.x - to.x); ++ final double adjY = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.y - to.y); ++ final double adjZ = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.z - to.z); ++ ++ if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) { ++ return miss(clipContext); ++ } ++ ++ final double toXAdj = to.x - adjX; ++ final double toYAdj = to.y - adjY; ++ final double toZAdj = to.z - adjZ; ++ final double fromXAdj = from.x + adjX; ++ final double fromYAdj = from.y + adjY; ++ final double fromZAdj = from.z + adjZ; ++ ++ int currX = Mth.floor(fromXAdj); ++ int currY = Mth.floor(fromYAdj); ++ int currZ = Mth.floor(fromZAdj); ++ ++ final BlockPos.MutableBlockPos currPos = new BlockPos.MutableBlockPos(); ++ ++ final double diffX = toXAdj - fromXAdj; ++ final double diffY = toYAdj - fromYAdj; ++ final double diffZ = toZAdj - fromZAdj; ++ ++ final double dxDouble = Math.signum(diffX); ++ final double dyDouble = Math.signum(diffY); ++ final double dzDouble = Math.signum(diffZ); ++ ++ final int dx = (int)dxDouble; ++ final int dy = (int)dyDouble; ++ final int dz = (int)dzDouble; ++ ++ final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX; ++ final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY; ++ final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ; ++ ++ double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj)); ++ double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj)); ++ double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj)); ++ ++ net.minecraft.world.level.chunk.LevelChunkSection[] lastChunk = null; ++ net.minecraft.world.level.chunk.PalettedContainer lastSection = null; ++ int lastChunkX = Integer.MIN_VALUE; ++ int lastChunkY = Integer.MIN_VALUE; ++ int lastChunkZ = Integer.MIN_VALUE; ++ ++ final int minSection = level.minSection; ++ final net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)level.getChunkSource(); ++ ++ for (;;) { ++ currPos.set(currX, currY, currZ); ++ ++ final int newChunkX = currX >> 4; ++ final int newChunkY = currY >> 4; ++ final int newChunkZ = currZ >> 4; ++ ++ final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)); ++ final int chunkYDiff = newChunkY ^ lastChunkY; ++ ++ if ((chunkDiff | chunkYDiff) != 0) { ++ if (chunkDiff != 0) { ++ LevelChunk chunk = chunkProvider.getChunkAtIfLoadedImmediately(newChunkX, newChunkZ); ++ lastChunk = chunk == null ? null : chunk.getSections(); // diff: don't load chunks for this ++ } ++ final int sectionY = newChunkY - minSection; ++ lastSection = lastChunk != null && sectionY >= 0 && sectionY < lastChunk.length ? lastChunk[sectionY].states : null; ++ ++ lastChunkX = newChunkX; ++ lastChunkY = newChunkY; ++ lastChunkZ = newChunkZ; ++ } ++ ++ final BlockState blockState; ++ if (lastSection != null && !(blockState = lastSection.get((currX & 15) | ((currZ & 15) << 4) | ((currY & 15) << (4+4)))).isAir()) { ++ final net.minecraft.world.phys.shapes.VoxelShape blockCollision = clipContext.getBlockShape(blockState, level, currPos); ++ ++ final net.minecraft.world.phys.BlockHitResult blockHit = blockCollision.isEmpty() ? null : level.clipWithInteractionOverride(from, to, currPos, blockCollision, blockState); ++ ++ final net.minecraft.world.phys.shapes.VoxelShape fluidCollision; ++ final FluidState fluidState; ++ if (clipContext.fluid != ClipContext.Fluid.NONE && (fluidState = blockState.getFluidState()) != AIR_FLUIDSTATE) { ++ fluidCollision = clipContext.getFluidShape(fluidState, level, currPos); ++ ++ final net.minecraft.world.phys.BlockHitResult fluidHit = fluidCollision.clip(from, to, currPos); ++ ++ if (fluidHit != null) { ++ if (blockHit == null) { ++ return fluidHit; ++ } ++ ++ return from.distanceToSqr(blockHit.getLocation()) <= from.distanceToSqr(fluidHit.getLocation()) ? blockHit : fluidHit; ++ } ++ } ++ ++ if (blockHit != null) { ++ return blockHit; ++ } ++ } // else: usually fall here ++ ++ if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) { ++ return miss(clipContext); ++ } ++ ++ // inc the smallest normalized coordinate ++ ++ if (normalizedCurrX < normalizedCurrY) { ++ if (normalizedCurrX < normalizedCurrZ) { ++ currX += dx; ++ normalizedCurrX += normalizedDiffX; ++ } else { ++ // x < y && x >= z <--> z < y && z <= x ++ currZ += dz; ++ normalizedCurrZ += normalizedDiffZ; ++ } ++ } else if (normalizedCurrY < normalizedCurrZ) { ++ // y <= x && y < z ++ currY += dy; ++ normalizedCurrY += normalizedDiffY; ++ } else { ++ // y <= x && z <= y <--> z <= y && z <= x ++ currZ += dz; ++ normalizedCurrZ += normalizedDiffZ; ++ } ++ } ++ } ++ ++ @Override ++ public final net.minecraft.world.phys.BlockHitResult clip(final ClipContext clipContext) { ++ // can only do this in this class, as not everything that implements BlockGetter can retrieve chunks ++ return fastClip(clipContext.getFrom(), clipContext.getTo(), this, clipContext); ++ } ++ ++ @Override ++ public final boolean noCollision(final Entity entity, final AABB box, final boolean loadChunks) { ++ int flags = io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_ONLY; ++ if (entity != null) { ++ flags |= io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER; ++ } ++ if (loadChunks) { ++ flags |= io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_LOAD_CHUNKS; ++ } ++ if (io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, null, flags, null)) { ++ return false; ++ } ++ ++ return !io.papermc.paper.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, flags, null); ++ } ++ ++ @Override ++ public final boolean collidesWithSuffocatingBlock(final Entity entity, final AABB box) { ++ return io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, null, ++ io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_ONLY, ++ (final BlockState state, final BlockPos pos) -> { ++ return state.isSuffocating(Level.this, pos); ++ } ++ ); ++ } ++ ++ private static net.minecraft.world.phys.shapes.VoxelShape inflateAABBToVoxel(final AABB aabb, final double x, final double y, final double z) { ++ return net.minecraft.world.phys.shapes.Shapes.create( ++ aabb.minX - x, ++ aabb.minY - y, ++ aabb.minZ - z, ++ ++ aabb.maxX + x, ++ aabb.maxY + y, ++ aabb.maxZ + z ++ ); ++ } ++ ++ @Override ++ public final java.util.Optional findFreePosition(final Entity entity, final net.minecraft.world.phys.shapes.VoxelShape boundsShape, final Vec3 fromPosition, ++ final double rangeX, final double rangeY, final double rangeZ) { ++ if (boundsShape.isEmpty()) { ++ return java.util.Optional.empty(); ++ } ++ ++ final double expandByX = rangeX * 0.5; ++ final double expandByY = rangeY * 0.5; ++ final double expandByZ = rangeZ * 0.5; ++ ++ // note: it is useless to look at shapes outside of range / 2.0 ++ final AABB collectionVolume = boundsShape.bounds().inflate(expandByX, expandByY, expandByZ); ++ ++ final List aabbs = new java.util.ArrayList<>(); ++ final List voxels = new java.util.ArrayList<>(); ++ ++ io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder( ++ this, entity, collectionVolume, voxels, aabbs, ++ io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, ++ null ++ ); ++ ++ // push voxels into aabbs ++ for (int i = 0, len = voxels.size(); i < len; ++i) { ++ aabbs.addAll(voxels.get(i).toAabbs()); ++ } ++ ++ // expand AABBs ++ final net.minecraft.world.phys.shapes.VoxelShape first = aabbs.isEmpty() ? net.minecraft.world.phys.shapes.Shapes.empty() : inflateAABBToVoxel(aabbs.get(0), expandByX, expandByY, expandByZ); ++ final net.minecraft.world.phys.shapes.VoxelShape[] rest = new net.minecraft.world.phys.shapes.VoxelShape[Math.max(0, aabbs.size() - 1)]; ++ ++ for (int i = 1, len = aabbs.size(); i < len; ++i) { ++ rest[i - 1] = inflateAABBToVoxel(aabbs.get(i), expandByX, expandByY, expandByZ); ++ } ++ ++ // use optimized implementation of ORing the shapes together ++ final net.minecraft.world.phys.shapes.VoxelShape joined = net.minecraft.world.phys.shapes.Shapes.or(first, rest); ++ ++ // find free space ++ // can use unoptimized join here (instead of join()), as closestPointTo uses toAabbs() ++ final net.minecraft.world.phys.shapes.VoxelShape freeSpace = net.minecraft.world.phys.shapes.Shapes.joinUnoptimized( ++ boundsShape, joined, net.minecraft.world.phys.shapes.BooleanOp.ONLY_FIRST ++ ); ++ ++ return freeSpace.closestPointTo(fromPosition); ++ } ++ ++ @Override ++ public final java.util.Optional findSupportingBlock(final Entity entity, final AABB aabb) { ++ final int minBlockX = Mth.floor(aabb.minX - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) - 1; ++ final int maxBlockX = Mth.floor(aabb.maxX + io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) + 1; ++ ++ final int minBlockY = Mth.floor(aabb.minY - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) - 1; ++ final int maxBlockY = Mth.floor(aabb.maxY + io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) + 1; ++ ++ final int minBlockZ = Mth.floor(aabb.minZ - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) - 1; ++ final int maxBlockZ = Mth.floor(aabb.maxZ + io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) + 1; ++ ++ io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext collisionContext = null; ++ ++ final BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); ++ BlockPos selected = null; ++ double selectedDistance = Double.MAX_VALUE; ++ ++ final Vec3 entityPos = entity.position(); ++ ++ LevelChunk lastChunk = null; ++ int lastChunkX = Integer.MIN_VALUE; ++ int lastChunkZ = Integer.MIN_VALUE; ++ ++ final net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.getChunkSource(); ++ ++ for (int currZ = minBlockZ; currZ <= maxBlockZ; ++currZ) { ++ pos.setZ(currZ); ++ for (int currX = minBlockX; currX <= maxBlockX; ++currX) { ++ pos.setX(currX); ++ ++ final int newChunkX = currX >> 4; ++ final int newChunkZ = currZ >> 4; ++ ++ final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)); ++ ++ if (chunkDiff != 0) { ++ lastChunk = chunkProvider.getChunkAtIfLoadedImmediately(newChunkX, newChunkZ); ++ } ++ ++ if (lastChunk == null) { ++ continue; ++ } ++ for (int currY = minBlockY; currY <= maxBlockY; ++currY) { ++ int edgeCount = ((currX == minBlockX || currX == maxBlockX) ? 1 : 0) + ++ ((currY == minBlockY || currY == maxBlockY) ? 1 : 0) + ++ ((currZ == minBlockZ || currZ == maxBlockZ) ? 1 : 0); ++ if (edgeCount == 3) { ++ continue; ++ } ++ ++ pos.setY(currY); ++ ++ final double distance = pos.distToCenterSqr(entityPos); ++ if (distance > selectedDistance || (distance == selectedDistance && selected.compareTo(pos) >= 0)) { ++ continue; ++ } ++ ++ final BlockState state = lastChunk.getBlockState(currX, currY, currZ); ++ if (state.emptyCollisionShape()) { ++ continue; ++ } ++ ++ if ((edgeCount != 1 || state.hasLargeCollisionShape()) && (edgeCount != 2 || state.getBlock() == Blocks.MOVING_PISTON)) { ++ if (collisionContext == null) { ++ collisionContext = new io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext(entity); ++ } ++ final net.minecraft.world.phys.shapes.VoxelShape blockCollision = state.getCollisionShape(lastChunk, pos, collisionContext); ++ if (blockCollision.isEmpty()) { ++ continue; ++ } ++ ++ // avoid VoxelShape#move by shifting the entity collision shape instead ++ final AABB shiftedAABB = aabb.move(-(double)currX, -(double)currY, -(double)currZ); ++ ++ final AABB singleAABB = blockCollision.getSingleAABBRepresentation(); ++ if (singleAABB != null) { ++ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(singleAABB, shiftedAABB)) { ++ continue; ++ } ++ ++ selected = pos.immutable(); ++ selectedDistance = distance; ++ continue; ++ } ++ ++ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersectNoEmpty(blockCollision, shiftedAABB)) { ++ continue; ++ } ++ ++ selected = pos.immutable(); ++ selectedDistance = distance; ++ continue; ++ } ++ } ++ } ++ } ++ ++ return java.util.Optional.ofNullable(selected); ++ } ++ // Paper end - optimise collisions + @Override + public boolean isClientSide() { + return this.isClientSide; +@@ -949,7 +1313,17 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + @Override + public boolean noCollision(@Nullable Entity entity, AABB box) { + if (entity instanceof net.minecraft.world.entity.decoration.ArmorStand && !entity.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return false; +- return LevelAccessor.super.noCollision(entity, box); ++ // Paper start - optimise collisions ++ int flags = io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_ONLY; ++ if (entity != null) { ++ flags |= io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER; ++ } ++ if (io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, null, flags, null)) { ++ return false; ++ } ++ ++ return !io.papermc.paper.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, flags, null); ++ // Paper end - optimise collisions + } + // Paper end - Option to prevent armor stands from doing entity lookups + +diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java +index 054593fc0b8d13f6bf449cc20a1f7ddfd5f1d1f0..cf8b8c8efd1c9c81eb5f02d75bd75875eb66771f 100644 +--- a/src/main/java/net/minecraft/world/level/block/Block.java ++++ b/src/main/java/net/minecraft/world/level/block/Block.java +@@ -280,7 +280,7 @@ public class Block extends BlockBehaviour implements ItemLike { + } + + public static boolean isShapeFullBlock(VoxelShape shape) { +- return (Boolean) Block.SHAPE_FULL_BLOCK_CACHE.getUnchecked(shape); ++ return shape.isFullBlock(); // Paper - optimise collisions + } + + public void animateTick(BlockState state, Level world, BlockPos pos, RandomSource random) {} +diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +index 55efd0d379bac79935f62446cd3479d1e59361a4..2034ca2edd3aff61d94416266e75402babd3e741 100644 +--- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java ++++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +@@ -813,6 +813,10 @@ public abstract class BlockBehaviour implements FeatureElement { + this.instrument = blockbase_info.instrument; + this.replaceable = blockbase_info.replaceable; + this.conditionallyFullOpaque = this.canOcclude & this.useShapeForLightOcclusion; // Paper ++ // Paper start - optimise collisions ++ this.id1 = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(ID_GENERATOR.getAndIncrement() + RANDOM_OFFSET) + RANDOM_OFFSET); ++ this.id2 = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(ID_GENERATOR.getAndIncrement() + RANDOM_OFFSET) + RANDOM_OFFSET); ++ // Paper end - optimise collisions + } + // Paper start - Perf: impl cached craft block data, lazy load to fix issue with loading at the wrong time + private org.bukkit.craftbukkit.block.data.CraftBlockData cachedCraftBlockData; +@@ -861,6 +865,52 @@ public abstract class BlockBehaviour implements FeatureElement { + return this.conditionallyFullOpaque; + } + // Paper end - starlight ++ // Paper start - optimise collisions ++ private static final int RANDOM_OFFSET = 704237939; ++ private static final Direction[] DIRECTIONS_CACHED = Direction.values(); ++ private static final java.util.concurrent.atomic.AtomicInteger ID_GENERATOR = new java.util.concurrent.atomic.AtomicInteger(); ++ private final int id1, id2; ++ private boolean occludesFullBlock; ++ private boolean emptyCollisionShape; ++ private VoxelShape constantCollisionShape; ++ private AABB constantAABBCollision; ++ private static void initCaches(final VoxelShape shape) { ++ shape.isFullBlock(); ++ shape.occludesFullBlock(); ++ shape.toAabbs(); ++ if (!shape.isEmpty()) { ++ shape.bounds(); ++ } ++ } ++ ++ public final boolean hasCache() { ++ return this.cache != null; ++ } ++ ++ public final boolean occludesFullBlock() { ++ return this.occludesFullBlock; ++ } ++ ++ public final boolean emptyCollisionShape() { ++ return this.emptyCollisionShape; ++ } ++ ++ public final int uniqueId1() { ++ return this.id1; ++ } ++ ++ public final int uniqueId2() { ++ return this.id2; ++ } ++ ++ public final VoxelShape getConstantCollisionShape() { ++ return this.constantCollisionShape; ++ } ++ ++ public final AABB getConstantCollisionAABB() { ++ return this.constantAABBCollision; ++ } ++ // Paper end - optimise collisions + + public void initCache() { + this.fluidState = ((Block) this.owner).getFluidState(this.asState()); +@@ -872,6 +922,39 @@ public abstract class BlockBehaviour implements FeatureElement { + this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque() ? -1 : this.cache.lightBlock; // Paper - starlight - cache opacity for light + + this.legacySolid = this.calculateSolid(); ++ // Paper start - optimise collisions ++ if (this.cache != null) { ++ final VoxelShape collisionShape = this.cache.collisionShape; ++ try { ++ this.constantCollisionShape = this.getCollisionShape(null, null, null); ++ this.constantAABBCollision = this.constantCollisionShape == null ? null : this.constantCollisionShape.getSingleAABBRepresentation(); ++ } catch (final Throwable throwable) { ++ this.constantCollisionShape = null; ++ this.constantAABBCollision = null; ++ } ++ this.occludesFullBlock = collisionShape.occludesFullBlock(); ++ this.emptyCollisionShape = collisionShape.isEmpty(); ++ // init caches ++ initCaches(collisionShape); ++ if (collisionShape != Shapes.empty() && collisionShape != Shapes.block()) { ++ for (final Direction direction : DIRECTIONS_CACHED) { ++ // initialise the directional face shape cache as well ++ final VoxelShape shape = Shapes.getFaceShape(collisionShape, direction); ++ initCaches(shape); ++ } ++ } ++ if (this.cache.occlusionShapes != null) { ++ for (final VoxelShape shape : this.cache.occlusionShapes) { ++ initCaches(shape); ++ } ++ } ++ } else { ++ this.occludesFullBlock = false; ++ this.emptyCollisionShape = false; ++ this.constantCollisionShape = null; ++ this.constantAABBCollision = null; ++ } ++ // Paper end - optimise collisions + } + + public Block getBlock() { +diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +index 8cd6c1d838e0332125fde3fc36475034aa4effa0..a2a5aef769ee8bb638a5a9f3da9812fa4a85dda5 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java ++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +@@ -26,6 +26,22 @@ public class LevelChunkSection { + // CraftBukkit start - read/write + private PalettedContainer> biomes; + public final com.destroystokyo.paper.util.maplist.IBlockDataList tickingList = new com.destroystokyo.paper.util.maplist.IBlockDataList(); // Paper ++ // Paper start - optimise collisions ++ private int specialCollidingBlocks; ++ ++ private void updateBlockCallback(final int x, final int y, final int z, final BlockState oldState, final BlockState newState) { ++ if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(newState)) { ++ ++this.specialCollidingBlocks; ++ } ++ if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(oldState)) { ++ --this.specialCollidingBlocks; ++ } ++ } ++ ++ public final int getSpecialCollidingBlocks() { ++ return this.specialCollidingBlocks; ++ } ++ // Paper end - optimise collisions + + public LevelChunkSection(PalettedContainer datapaletteblock, PalettedContainer> palettedcontainerro) { + // CraftBukkit end +@@ -62,8 +78,8 @@ public class LevelChunkSection { + return this.setBlockState(x, y, z, state, true); + } + +- public BlockState setBlockState(int x, int y, int z, BlockState state, boolean lock) { +- BlockState iblockdata1; ++ public BlockState setBlockState(int x, int y, int z, BlockState state, boolean lock) { // Paper - state -> new state ++ BlockState iblockdata1; // Paper - iblockdata1 -> oldState + + if (lock) { + iblockdata1 = (BlockState) this.states.getAndSet(x, y, z, state); +@@ -102,6 +118,7 @@ public class LevelChunkSection { + ++this.tickingFluidCount; + } + ++ this.updateBlockCallback(x, y, z, iblockdata1, state); // Paper - optimise collisions + return iblockdata1; + } + +@@ -147,6 +164,11 @@ public class LevelChunkSection { + } + } + ++ // Paper start - optimise collisions ++ if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(iblockdata)) { ++ ++this.specialCollidingBlocks; ++ } ++ // Paper end - optimise collisions + }); + } + // Paper end - unfuck this +diff --git a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java +index 1c0712295695727ee9c4d430d4157b8e17cbd71f..c2943d892b067b3f1fb3b93301a092e912d71f08 100644 +--- a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java ++++ b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java +@@ -240,6 +240,17 @@ public abstract class FlowingFluid extends Fluid { + } + + private boolean canPassThroughWall(Direction face, BlockGetter world, BlockPos pos, BlockState state, BlockPos fromPos, BlockState fromState) { ++ // Paper start - optimise collisions ++ if (state.emptyCollisionShape() & fromState.emptyCollisionShape()) { ++ // don't even try to cache simple cases ++ return true; ++ } ++ ++ if (state.occludesFullBlock() | fromState.occludesFullBlock()) { ++ // don't even try to cache simple cases ++ return false; ++ } ++ // Paper end - optimise collisions + Object2ByteLinkedOpenHashMap object2bytelinkedopenhashmap; + + if (!state.getBlock().hasDynamicShape() && !fromState.getBlock().hasDynamicShape()) { +diff --git a/src/main/java/net/minecraft/world/phys/AABB.java b/src/main/java/net/minecraft/world/phys/AABB.java +index 62752e28a68400f0e1a44f0196f0e51e3dd702b8..92394960fc76886f393cba02ac33c57739a4b383 100644 +--- a/src/main/java/net/minecraft/world/phys/AABB.java ++++ b/src/main/java/net/minecraft/world/phys/AABB.java +@@ -25,6 +25,17 @@ public class AABB { + this.maxZ = Math.max(z1, z2); + } + ++ // Paper start ++ public AABB(double minX, double minY, double minZ, double maxX, double maxY, double maxZ, boolean dummy) { ++ this.minX = minX; ++ this.minY = minY; ++ this.minZ = minZ; ++ this.maxX = maxX; ++ this.maxY = maxY; ++ this.maxZ = maxZ; ++ } ++ // Paper end ++ + public AABB(BlockPos pos) { + this((double)pos.getX(), (double)pos.getY(), (double)pos.getZ(), (double)(pos.getX() + 1), (double)(pos.getY() + 1), (double)(pos.getZ() + 1)); + } +@@ -321,7 +332,7 @@ public class AABB { + } + + @Nullable +- private static Direction getDirection( ++ public static Direction getDirection( // Paper - optimise collisions - public + AABB box, Vec3 intersectingVector, double[] traceDistanceResult, @Nullable Direction approachDirection, double deltaX, double deltaY, double deltaZ + ) { + if (deltaX > 1.0E-7) { +diff --git a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java +index fc7f986812bdf74e0aea3bd09a1d53ba6def697f..0583d40a235aaecd9d6081486bbfb7355709a5ac 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java +@@ -20,7 +20,7 @@ public class ArrayVoxelShape extends VoxelShape { + ); + } + +- ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints) { ++ public ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints) { // Paper - optimise collisions - public + super(shape); + int i = shape.getXSize() + 1; + int j = shape.getYSize() + 1; +@@ -34,6 +34,7 @@ public class ArrayVoxelShape extends VoxelShape { + new IllegalArgumentException("Lengths of point arrays must be consistent with the size of the VoxelShape.") + ); + } ++ this.initCache(); // Paper - optimise collisions + } + + @Override +@@ -49,4 +50,5 @@ public class ArrayVoxelShape extends VoxelShape { + throw new IllegalArgumentException(); + } + } ++ + } +diff --git a/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java +index e8f3307727e7e3da9a7629cafc6e1ee53790b75d..a71421947b161697d43bd8602e2af95aa272ed3f 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java +@@ -4,13 +4,13 @@ import java.util.BitSet; + import net.minecraft.core.Direction; + + public final class BitSetDiscreteVoxelShape extends DiscreteVoxelShape { +- private final BitSet storage; +- private int xMin; +- private int yMin; +- private int zMin; +- private int xMax; +- private int yMax; +- private int zMax; ++ public final BitSet storage; // Paper - optimise collisions - public ++ public int xMin; // Paper - optimise collisions - public ++ public int yMin; // Paper - optimise collisions - public ++ public int zMin; // Paper - optimise collisions - public ++ public int xMax; // Paper - optimise collisions - public ++ public int yMax; // Paper - optimise collisions - public ++ public int zMax; // Paper - optimise collisions - public + + public BitSetDiscreteVoxelShape(int sizeX, int sizeY, int sizeZ) { + super(sizeX, sizeY, sizeZ); +@@ -151,45 +151,106 @@ public final class BitSetDiscreteVoxelShape extends DiscreteVoxelShape { + } + + protected static void forAllBoxes(DiscreteVoxelShape voxelSet, DiscreteVoxelShape.IntLineConsumer callback, boolean coalesce) { +- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = new BitSetDiscreteVoxelShape(voxelSet); ++ // Paper start - optimise collisions ++ // called with the shape of a VoxelShape, so we can expect the cache to exist ++ final io.papermc.paper.util.collisions.CachedShapeData cache = voxelSet.getOrCreateCachedShapeData(); ++ ++ final int sizeX = cache.sizeX(); ++ final int sizeY = cache.sizeY(); ++ final int sizeZ = cache.sizeZ(); ++ ++ int indexX; ++ int indexY = 0; ++ int indexZ; ++ ++ int incY = sizeZ; ++ int incX = sizeZ*sizeY; ++ ++ long[] bitset = cache.voxelSet(); ++ ++ // index = z + y*size_z + x*(size_z*size_y) ++ ++ if (!coalesce) { ++ // due to the odd selection of loop order (which does affect behavior, unfortunately) we can't simply ++ // increment an index in the Z loop, and have to perform this trash (keeping track of 3 counters) to avoid ++ // the multiplication ++ for (int y = 0; y < sizeY; ++y, indexY += incY) { ++ indexX = indexY; ++ for (int x = 0; x < sizeX; ++x, indexX += incX) { ++ indexZ = indexX; ++ for (int z = 0; z < sizeZ; ++z, ++indexZ) { ++ if ((bitset[indexZ >>> 6] & (1L << indexZ)) != 0L) { ++ callback.consume(x, y, z, x + 1, y + 1, z + 1); ++ } ++ } ++ } ++ } ++ } else { ++ // same notes about loop order as the above ++ // this branch is actually important to optimise, as it affects uncached toAabbs() (which affects optimize()) + +- for (int i = 0; i < bitSetDiscreteVoxelShape.ySize; i++) { +- for (int j = 0; j < bitSetDiscreteVoxelShape.xSize; j++) { +- int k = -1; ++ // only clone when we may write to it ++ bitset = bitset.clone(); + +- for (int l = 0; l <= bitSetDiscreteVoxelShape.zSize; l++) { +- if (bitSetDiscreteVoxelShape.isFullWide(j, i, l)) { +- if (coalesce) { +- if (k == -1) { +- k = l; +- } +- } else { +- callback.consume(j, i, l, j + 1, i + 1, l + 1); ++ for (int y = 0; y < sizeY; ++y, indexY += incY) { ++ indexX = indexY; ++ for (int x = 0; x < sizeX; ++x, indexX += incX) { ++ for (int zIdx = indexX, endIndex = indexX + sizeZ; zIdx < endIndex;) { ++ final int firstSetZ = io.papermc.paper.util.collisions.FlatBitsetUtil.firstSet(bitset, zIdx, endIndex); ++ ++ if (firstSetZ == -1) { ++ break; ++ } ++ ++ int lastSetZ = io.papermc.paper.util.collisions.FlatBitsetUtil.firstClear(bitset, firstSetZ, endIndex); ++ if (lastSetZ == -1) { ++ lastSetZ = endIndex; + } +- } else if (k != -1) { +- int m = j; +- int n = i; +- bitSetDiscreteVoxelShape.clearZStrip(k, l, j, i); +- +- while (bitSetDiscreteVoxelShape.isZStripFull(k, l, m + 1, i)) { +- bitSetDiscreteVoxelShape.clearZStrip(k, l, m + 1, i); +- m++; ++ ++ io.papermc.paper.util.collisions.FlatBitsetUtil.clearRange(bitset, firstSetZ, lastSetZ); ++ ++ // try to merge neighbouring on the X axis ++ int endX = x + 1; // exclusive ++ for (int neighbourIdxStart = firstSetZ + incX, neighbourIdxEnd = lastSetZ + incX; ++ endX < sizeX && io.papermc.paper.util.collisions.FlatBitsetUtil.isRangeSet(bitset, neighbourIdxStart, neighbourIdxEnd); ++ neighbourIdxStart += incX, neighbourIdxEnd += incX) { ++ ++ ++endX; ++ io.papermc.paper.util.collisions.FlatBitsetUtil.clearRange(bitset, neighbourIdxStart, neighbourIdxEnd); + } + +- while (bitSetDiscreteVoxelShape.isXZRectangleFull(j, m + 1, k, l, n + 1)) { +- for (int o = j; o <= m; o++) { +- bitSetDiscreteVoxelShape.clearZStrip(k, l, o, n + 1); ++ // try to merge neighbouring on the Y axis ++ ++ int endY; // exclusive ++ int firstSetZY, lastSetZY; ++ y_merge: ++ for (endY = y + 1, firstSetZY = firstSetZ + incY, lastSetZY = lastSetZ + incY; endY < sizeY; ++ firstSetZY += incY, lastSetZY += incY) { ++ ++ // test the whole XZ range ++ for (int testX = x, start = firstSetZY, end = lastSetZY; testX < endX; ++ ++testX, start += incX, end += incX) { ++ if (!io.papermc.paper.util.collisions.FlatBitsetUtil.isRangeSet(bitset, start, end)) { ++ break y_merge; ++ } + } + +- n++; ++ ++endY; ++ ++ // passed, so we can clear it ++ for (int testX = x, start = firstSetZY, end = lastSetZY; testX < endX; ++ ++testX, start += incX, end += incX) { ++ io.papermc.paper.util.collisions.FlatBitsetUtil.clearRange(bitset, start, end); ++ } + } + +- callback.consume(j, i, k, m + 1, n + 1, l); +- k = -1; ++ callback.consume(x, y, firstSetZ - indexX, endX, endY, lastSetZ - indexX); ++ zIdx = lastSetZ; + } + } + } + } ++ // Paper end - optimise collisions + } + + private boolean isZStripFull(int z1, int z2, int x, int y) { +diff --git a/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java +index 32632368f06b79f53342fde060bbcd1b7c64767a..b9af1d14c7815c99273bce8165cf384d669c1a75 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java +@@ -7,6 +7,7 @@ import net.minecraft.util.Mth; + public final class CubeVoxelShape extends VoxelShape { + protected CubeVoxelShape(DiscreteVoxelShape voxels) { + super(voxels); ++ this.initCache(); // Paper - optimise collisions + } + + @Override +diff --git a/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java +index 01693ba050b12b9debcdaefceeff9cbcd503b369..a7af766f1c28f2c9ca2a430808ac4c07fe24df68 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java +@@ -9,6 +9,71 @@ public abstract class DiscreteVoxelShape { + protected final int ySize; + protected final int zSize; + ++ // Paper start - optimise collisions ++ private io.papermc.paper.util.collisions.CachedShapeData cachedShapeData; ++ ++ public final io.papermc.paper.util.collisions.CachedShapeData getOrCreateCachedShapeData() { ++ if (this.cachedShapeData != null) { ++ return this.cachedShapeData; ++ } ++ ++ final DiscreteVoxelShape discreteVoxelShape = (DiscreteVoxelShape)(Object)this; ++ ++ final int sizeX = discreteVoxelShape.getXSize(); ++ final int sizeY = discreteVoxelShape.getYSize(); ++ final int sizeZ = discreteVoxelShape.getZSize(); ++ ++ final int maxIndex = sizeX * sizeY * sizeZ; // exclusive ++ ++ final int longsRequired = (maxIndex + (Long.SIZE - 1)) >>> 6; ++ long[] voxelSet; ++ ++ final boolean isEmpty = discreteVoxelShape.isEmpty(); ++ ++ if (discreteVoxelShape instanceof BitSetDiscreteVoxelShape bitsetShape) { ++ voxelSet = bitsetShape.storage.toLongArray(); ++ if (voxelSet.length < longsRequired) { ++ // happens when the later long values are 0L, so we need to resize ++ voxelSet = java.util.Arrays.copyOf(voxelSet, longsRequired); ++ } ++ } else { ++ voxelSet = new long[longsRequired]; ++ if (!isEmpty) { ++ final int mulX = sizeZ * sizeY; ++ for (int x = 0; x < sizeX; ++x) { ++ for (int y = 0; y < sizeY; ++y) { ++ for (int z = 0; z < sizeZ; ++z) { ++ if (discreteVoxelShape.isFull(x, y, z)) { ++ // index = z + y*size_z + x*(size_z*size_y) ++ final int index = z + y * sizeZ + x * mulX; ++ ++ voxelSet[index >>> 6] |= 1L << index; ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ final boolean hasSingleAABB = sizeX == 1 && sizeY == 1 && sizeZ == 1 && !isEmpty && discreteVoxelShape.isFull(0, 0, 0); ++ ++ final int minFullX = discreteVoxelShape.firstFull(Direction.Axis.X); ++ final int minFullY = discreteVoxelShape.firstFull(Direction.Axis.Y); ++ final int minFullZ = discreteVoxelShape.firstFull(Direction.Axis.Z); ++ ++ final int maxFullX = discreteVoxelShape.lastFull(Direction.Axis.X); ++ final int maxFullY = discreteVoxelShape.lastFull(Direction.Axis.Y); ++ final int maxFullZ = discreteVoxelShape.lastFull(Direction.Axis.Z); ++ ++ return this.cachedShapeData = new io.papermc.paper.util.collisions.CachedShapeData( ++ sizeX, sizeY, sizeZ, voxelSet, ++ minFullX, minFullY, minFullZ, ++ maxFullX, maxFullY, maxFullZ, ++ isEmpty, hasSingleAABB ++ ); ++ } ++ // Paper end - optimise collisions ++ + protected DiscreteVoxelShape(int sizeX, int sizeY, int sizeZ) { + if (sizeX >= 0 && sizeY >= 0 && sizeZ >= 0) { + this.xSize = sizeX; +diff --git a/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java b/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java +index 7ec02a7849437a18860aa0df7d9ddd71b2447d4c..5e45e49ab09344cb95736f4124b1c6e002ef5b82 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java +@@ -4,8 +4,8 @@ import it.unimi.dsi.fastutil.doubles.AbstractDoubleList; + import it.unimi.dsi.fastutil.doubles.DoubleList; + + public class OffsetDoubleList extends AbstractDoubleList { +- private final DoubleList delegate; +- private final double offset; ++ public final DoubleList delegate; // Paper - optimise collisions - public ++ public final double offset; // Paper - optimise collisions - public + + public OffsetDoubleList(DoubleList oldList, double offset) { + this.delegate = oldList; +diff --git a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java +index 86df4ef44d0a5107ee929dfd40d8ccb0779e8bfc..fbf1a559aefe444410b63a773374e011e4964e16 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java +@@ -16,9 +16,15 @@ public final class Shapes { + public static final double EPSILON = 1.0E-7; + public static final double BIG_EPSILON = 1.0E-6; + private static final VoxelShape BLOCK = Util.make(() -> { +- DiscreteVoxelShape discreteVoxelShape = new BitSetDiscreteVoxelShape(1, 1, 1); +- discreteVoxelShape.fill(0, 0, 0); +- return new CubeVoxelShape(discreteVoxelShape); ++ // Paper start - optimise collisions - force arrayvoxelshape ++ final DiscreteVoxelShape shape = new BitSetDiscreteVoxelShape(1, 1, 1); ++ shape.fill(0, 0, 0); ++ ++ return new ArrayVoxelShape( ++ shape, ++ io.papermc.paper.util.CollisionUtil.ZERO_ONE, io.papermc.paper.util.CollisionUtil.ZERO_ONE, io.papermc.paper.util.CollisionUtil.ZERO_ONE ++ ); ++ // Paper end - optimise collisions - force arrayvoxelshape + }); + public static final VoxelShape INFINITY = box( + Double.NEGATIVE_INFINITY, +@@ -35,6 +41,30 @@ public final class Shapes { + new DoubleArrayList(new double[]{0.0}) + ); + ++ // Paper start - optimise collisions - force arrayvoxelshape ++ private static final DoubleArrayList[] PARTS_BY_BITS = new DoubleArrayList[] { ++ DoubleArrayList.wrap(generateCubeParts(1 << 0)), ++ DoubleArrayList.wrap(generateCubeParts(1 << 1)), ++ DoubleArrayList.wrap(generateCubeParts(1 << 2)), ++ DoubleArrayList.wrap(generateCubeParts(1 << 3)) ++ }; ++ ++ private static double[] generateCubeParts(final int parts) { ++ // note: parts is a power of two, so we do not need to worry about loss of precision here ++ // note: parts is from [2^0, 2^3] ++ final double inc = 1.0 / (double)parts; ++ ++ final double[] ret = new double[parts + 1]; ++ double val = 0.0; ++ for (int i = 0; i <= parts; ++i) { ++ ret[i] = val; ++ val += inc; ++ } ++ ++ return ret; ++ } ++ // Paper end - optimise collisions - force arrayvoxelshape ++ + public static VoxelShape empty() { + return EMPTY; + } +@@ -53,35 +83,39 @@ public final class Shapes { + + public static VoxelShape create(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { + if (!(maxX - minX < 1.0E-7) && !(maxY - minY < 1.0E-7) && !(maxZ - minZ < 1.0E-7)) { +- int i = findBits(minX, maxX); +- int j = findBits(minY, maxY); +- int k = findBits(minZ, maxZ); +- if (i < 0 || j < 0 || k < 0) { +- return new ArrayVoxelShape( +- BLOCK.shape, +- DoubleArrayList.wrap(new double[]{minX, maxX}), +- DoubleArrayList.wrap(new double[]{minY, maxY}), +- DoubleArrayList.wrap(new double[]{minZ, maxZ}) +- ); +- } else if (i == 0 && j == 0 && k == 0) { +- return block(); ++ // Paper start - optimise collisions ++ // force ArrayVoxelShape in every case ++ final int bitsX = findBits(minX, maxX); ++ final int bitsY = findBits(minY, maxY); ++ final int bitsZ = findBits(minZ, maxZ); ++ if (bitsX >= 0 && bitsY >= 0 && bitsZ >= 0) { ++ if (bitsX == 0 && bitsY == 0 && bitsZ == 0) { ++ return BLOCK; ++ } else { ++ final int sizeX = 1 << bitsX; ++ final int sizeY = 1 << bitsY; ++ final int sizeZ = 1 << bitsZ; ++ final BitSetDiscreteVoxelShape shape = BitSetDiscreteVoxelShape.withFilledBounds( ++ sizeX, sizeY, sizeZ, ++ (int)Math.round(minX * (double)sizeX), (int)Math.round(minY * (double)sizeY), (int)Math.round(minZ * (double)sizeZ), ++ (int)Math.round(maxX * (double)sizeX), (int)Math.round(maxY * (double)sizeY), (int)Math.round(maxZ * (double)sizeZ) ++ ); ++ return new ArrayVoxelShape( ++ shape, ++ PARTS_BY_BITS[bitsX], ++ PARTS_BY_BITS[bitsY], ++ PARTS_BY_BITS[bitsZ] ++ ); ++ } + } else { +- int l = 1 << i; +- int m = 1 << j; +- int n = 1 << k; +- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.withFilledBounds( +- l, +- m, +- n, +- (int)Math.round(minX * (double)l), +- (int)Math.round(minY * (double)m), +- (int)Math.round(minZ * (double)n), +- (int)Math.round(maxX * (double)l), +- (int)Math.round(maxY * (double)m), +- (int)Math.round(maxZ * (double)n) ++ return new ArrayVoxelShape( ++ BLOCK.shape, ++ minX == 0.0 && maxX == 1.0 ? io.papermc.paper.util.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minX, maxX }), ++ minY == 0.0 && maxY == 1.0 ? io.papermc.paper.util.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minY, maxY }), ++ minZ == 0.0 && maxZ == 1.0 ? io.papermc.paper.util.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minZ, maxZ }) + ); +- return new CubeVoxelShape(bitSetDiscreteVoxelShape); + } ++ // Paper end - optimise collisions + } else { + return empty(); + } +@@ -120,79 +154,53 @@ public final class Shapes { + } + + public static VoxelShape or(VoxelShape first, VoxelShape... others) { +- return Arrays.stream(others).reduce(first, Shapes::or); ++ // Paper start - optimise collisions ++ int size = others.length; ++ if (size == 0) { ++ return first; ++ } ++ ++ // reduce complexity of joins by splitting the merges ++ ++ // add extra slot for first shape ++ ++size; ++ final VoxelShape[] tmp = Arrays.copyOf(others, size); ++ // insert first shape ++ tmp[size - 1] = first; ++ ++ while (size > 1) { ++ int newSize = 0; ++ for (int i = 0; i < size; i += 2) { ++ final int next = i + 1; ++ if (next >= size) { ++ // nothing to merge with, so leave it for next iteration ++ tmp[newSize++] = tmp[i]; ++ break; ++ } else { ++ // merge with adjacent ++ final VoxelShape one = tmp[i]; ++ final VoxelShape second = tmp[next]; ++ ++ tmp[newSize++] = Shapes.or(one, second); ++ } ++ } ++ size = newSize; ++ } ++ ++ return tmp[0]; ++ // Paper end - optimise collisions + } + + public static VoxelShape join(VoxelShape first, VoxelShape second, BooleanOp function) { +- return joinUnoptimized(first, second, function).optimize(); ++ return io.papermc.paper.util.CollisionUtil.joinOptimized(first, second, function); // Paper - optimise collisions + } + + public static VoxelShape joinUnoptimized(VoxelShape one, VoxelShape two, BooleanOp function) { +- if (function.apply(false, false)) { +- throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException()); +- } else if (one == two) { +- return function.apply(true, true) ? one : empty(); +- } else { +- boolean bl = function.apply(true, false); +- boolean bl2 = function.apply(false, true); +- if (one.isEmpty()) { +- return bl2 ? two : empty(); +- } else if (two.isEmpty()) { +- return bl ? one : empty(); +- } else { +- IndexMerger indexMerger = createIndexMerger(1, one.getCoords(Direction.Axis.X), two.getCoords(Direction.Axis.X), bl, bl2); +- IndexMerger indexMerger2 = createIndexMerger(indexMerger.size() - 1, one.getCoords(Direction.Axis.Y), two.getCoords(Direction.Axis.Y), bl, bl2); +- IndexMerger indexMerger3 = createIndexMerger( +- (indexMerger.size() - 1) * (indexMerger2.size() - 1), one.getCoords(Direction.Axis.Z), two.getCoords(Direction.Axis.Z), bl, bl2 +- ); +- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.join( +- one.shape, two.shape, indexMerger, indexMerger2, indexMerger3, function +- ); +- return (VoxelShape)(indexMerger instanceof DiscreteCubeMerger +- && indexMerger2 instanceof DiscreteCubeMerger +- && indexMerger3 instanceof DiscreteCubeMerger +- ? new CubeVoxelShape(bitSetDiscreteVoxelShape) +- : new ArrayVoxelShape(bitSetDiscreteVoxelShape, indexMerger.getList(), indexMerger2.getList(), indexMerger3.getList())); +- } +- } ++ return io.papermc.paper.util.CollisionUtil.joinUnoptimized(one, two, function); // Paper - optimise collisions + } + + public static boolean joinIsNotEmpty(VoxelShape shape1, VoxelShape shape2, BooleanOp predicate) { +- if (predicate.apply(false, false)) { +- throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException()); +- } else { +- boolean bl = shape1.isEmpty(); +- boolean bl2 = shape2.isEmpty(); +- if (!bl && !bl2) { +- if (shape1 == shape2) { +- return predicate.apply(true, true); +- } else { +- boolean bl3 = predicate.apply(true, false); +- boolean bl4 = predicate.apply(false, true); +- +- for (Direction.Axis axis : AxisCycle.AXIS_VALUES) { +- if (shape1.max(axis) < shape2.min(axis) - 1.0E-7) { +- return bl3 || bl4; +- } +- +- if (shape2.max(axis) < shape1.min(axis) - 1.0E-7) { +- return bl3 || bl4; +- } +- } +- +- IndexMerger indexMerger = createIndexMerger(1, shape1.getCoords(Direction.Axis.X), shape2.getCoords(Direction.Axis.X), bl3, bl4); +- IndexMerger indexMerger2 = createIndexMerger( +- indexMerger.size() - 1, shape1.getCoords(Direction.Axis.Y), shape2.getCoords(Direction.Axis.Y), bl3, bl4 +- ); +- IndexMerger indexMerger3 = createIndexMerger( +- (indexMerger.size() - 1) * (indexMerger2.size() - 1), shape1.getCoords(Direction.Axis.Z), shape2.getCoords(Direction.Axis.Z), bl3, bl4 +- ); +- return joinIsNotEmpty(indexMerger, indexMerger2, indexMerger3, shape1.shape, shape2.shape, predicate); +- } +- } else { +- return predicate.apply(!bl, !bl2); +- } +- } ++ return io.papermc.paper.util.CollisionUtil.isJoinNonEmpty(shape1, shape2, predicate); // Paper - optimise collisions + } + + private static boolean joinIsNotEmpty( +@@ -220,69 +228,119 @@ public final class Shapes { + } + + public static boolean blockOccudes(VoxelShape shape, VoxelShape neighbor, Direction direction) { +- if (shape == block() && neighbor == block()) { ++ // Paper start - optimise collisions ++ final boolean firstBlock = shape == BLOCK; ++ final boolean secondBlock = neighbor == BLOCK; ++ ++ if (firstBlock & secondBlock) { + return true; +- } else if (neighbor.isEmpty()) { ++ } ++ ++ if (shape.isEmpty() | neighbor.isEmpty()) { ++ return false; ++ } ++ ++ // we optimise getOpposite, so we can use it ++ // secondly, use our cache to retrieve sliced shape ++ final VoxelShape newFirst = shape.getFaceShapeClamped(direction); ++ if (newFirst.isEmpty()) { + return false; +- } else { +- Direction.Axis axis = direction.getAxis(); +- Direction.AxisDirection axisDirection = direction.getAxisDirection(); +- VoxelShape voxelShape = axisDirection == Direction.AxisDirection.POSITIVE ? shape : neighbor; +- VoxelShape voxelShape2 = axisDirection == Direction.AxisDirection.POSITIVE ? neighbor : shape; +- BooleanOp booleanOp = axisDirection == Direction.AxisDirection.POSITIVE ? BooleanOp.ONLY_FIRST : BooleanOp.ONLY_SECOND; +- return DoubleMath.fuzzyEquals(voxelShape.max(axis), 1.0, 1.0E-7) +- && DoubleMath.fuzzyEquals(voxelShape2.min(axis), 0.0, 1.0E-7) +- && !joinIsNotEmpty(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape2, axis, 0), booleanOp); + } ++ final VoxelShape newSecond = neighbor.getFaceShapeClamped(direction.getOpposite()); ++ if (newSecond.isEmpty()) { ++ return false; ++ } ++ ++ return !joinIsNotEmpty(newFirst, newSecond, BooleanOp.ONLY_FIRST); ++ // Paper end - optimise collisions + } + + public static VoxelShape getFaceShape(VoxelShape shape, Direction direction) { +- if (shape == block()) { +- return block(); +- } else { +- Direction.Axis axis = direction.getAxis(); +- boolean bl; +- int i; +- if (direction.getAxisDirection() == Direction.AxisDirection.POSITIVE) { +- bl = DoubleMath.fuzzyEquals(shape.max(axis), 1.0, 1.0E-7); +- i = shape.shape.getSize(axis) - 1; +- } else { +- bl = DoubleMath.fuzzyEquals(shape.min(axis), 0.0, 1.0E-7); +- i = 0; +- } ++ return shape.getFaceShapeClamped(direction); // Paper - optimise collisions ++ } + +- return (VoxelShape)(!bl ? empty() : new SliceShape(shape, axis, i)); +- } ++ // Paper start - optimise collisions ++ private static boolean mergedMayOccludeBlock(final VoxelShape shape1, final VoxelShape shape2) { ++ // if the combined bounds of the two shapes cannot occlude, then neither can the merged ++ final AABB bounds1 = shape1.bounds(); ++ final AABB bounds2 = shape2.bounds(); ++ ++ final double minX = Math.min(bounds1.minX, bounds2.minX); ++ final double minY = Math.min(bounds1.minY, bounds2.minY); ++ final double minZ = Math.min(bounds1.minZ, bounds2.minZ); ++ ++ final double maxX = Math.max(bounds1.maxX, bounds2.maxX); ++ final double maxY = Math.max(bounds1.maxY, bounds2.maxY); ++ final double maxZ = Math.max(bounds1.maxZ, bounds2.maxZ); ++ ++ return (minX <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && maxX >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) && ++ (minY <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && maxY >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) && ++ (minZ <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && maxZ >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)); + } ++ // Paper end - optimise collisions + + public static boolean mergedFaceOccludes(VoxelShape one, VoxelShape two, Direction direction) { +- if (one != block() && two != block()) { +- Direction.Axis axis = direction.getAxis(); +- Direction.AxisDirection axisDirection = direction.getAxisDirection(); +- VoxelShape voxelShape = axisDirection == Direction.AxisDirection.POSITIVE ? one : two; +- VoxelShape voxelShape2 = axisDirection == Direction.AxisDirection.POSITIVE ? two : one; +- if (!DoubleMath.fuzzyEquals(voxelShape.max(axis), 1.0, 1.0E-7)) { +- voxelShape = empty(); +- } ++ // Paper start - optimise collisions ++ // see if any of the shapes on their own occludes, only if cached ++ if (one.occludesFullBlockIfCached() || two.occludesFullBlockIfCached()) { ++ return true; ++ } + +- if (!DoubleMath.fuzzyEquals(voxelShape2.min(axis), 0.0, 1.0E-7)) { +- voxelShape2 = empty(); +- } ++ if (one.isEmpty() & two.isEmpty()) { ++ return false; ++ } + +- return !joinIsNotEmpty( +- block(), +- joinUnoptimized(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape2, axis, 0), BooleanOp.OR), +- BooleanOp.ONLY_FIRST +- ); +- } else { ++ // we optimise getOpposite, so we can use it ++ // secondly, use our cache to retrieve sliced shape ++ final VoxelShape newFirst = one.getFaceShapeClamped(direction); ++ final VoxelShape newSecond = two.getFaceShapeClamped(direction.getOpposite()); ++ ++ // see if any of the shapes on their own occludes, only if cached ++ if (newFirst.occludesFullBlockIfCached() || newSecond.occludesFullBlockIfCached()) { + return true; + } ++ ++ final boolean firstEmpty = newFirst.isEmpty(); ++ final boolean secondEmpty = newSecond.isEmpty(); ++ ++ if (firstEmpty & secondEmpty) { ++ return false; ++ } ++ ++ if (firstEmpty | secondEmpty) { ++ return secondEmpty ? newFirst.occludesFullBlock() : newSecond.occludesFullBlock(); ++ } ++ ++ if (newFirst == newSecond) { ++ return newFirst.occludesFullBlock(); ++ } ++ ++ return mergedMayOccludeBlock(newFirst, newSecond) && newFirst.orUnoptimized(newSecond).occludesFullBlock(); ++ // Paper end - optimise collisions + } + + public static boolean faceShapeOccludes(VoxelShape one, VoxelShape two) { +- return one == block() +- || two == block() +- || (!one.isEmpty() || !two.isEmpty()) && !joinIsNotEmpty(block(), joinUnoptimized(one, two, BooleanOp.OR), BooleanOp.ONLY_FIRST); ++ // Paper start - optimise collisions ++ if (one.occludesFullBlockIfCached() || two.occludesFullBlockIfCached()) { ++ return true; ++ } ++ ++ final boolean s1Empty = one.isEmpty(); ++ final boolean s2Empty = two.isEmpty(); ++ if (s1Empty & s2Empty) { ++ return false; ++ } ++ ++ if (s1Empty | s2Empty) { ++ return s2Empty ? one.occludesFullBlock() : two.occludesFullBlock(); ++ } ++ ++ if (one == two) { ++ return one.occludesFullBlock(); ++ } ++ ++ return mergedMayOccludeBlock(one, two) && (one.orUnoptimized(two)).occludesFullBlock(); ++ // Paper end - optimise collisions + } + + @VisibleForTesting +diff --git a/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java b/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java +index 53aa193f33a1a15376a59b8d6dd8cbc6cbec168b..a745ff8d115e1d0da6138e4f06726e0737bb1600 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java +@@ -12,6 +12,7 @@ public class SliceShape extends VoxelShape { + super(makeSlice(shape.shape, axis, sliceWidth)); + this.delegate = shape; + this.axis = axis; ++ this.initCache(); // Paper - optimise collisions + } + + private static DiscreteVoxelShape makeSlice(DiscreteVoxelShape voxelSet, Direction.Axis axis, int sliceWidth) { +diff --git a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java +index 2936c56e5690b42518010698e5177755422e4c5d..e6b17f32f2b6930739a98c6139442383c1847add 100644 +--- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java ++++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java +@@ -16,37 +16,438 @@ import net.minecraft.world.phys.BlockHitResult; + import net.minecraft.world.phys.Vec3; + + public abstract class VoxelShape { +- protected final DiscreteVoxelShape shape; ++ public final DiscreteVoxelShape shape; // Paper - optimise collisions - public + @Nullable + private VoxelShape[] faces; + +- VoxelShape(DiscreteVoxelShape voxels) { ++ // Paper start - optimise collisions ++ private double offsetX; ++ private double offsetY; ++ private double offsetZ; ++ @Nullable private AABB singleAABBRepresentation; ++ private double[] rootCoordinatesX; ++ private double[] rootCoordinatesY; ++ private double[] rootCoordinatesZ; ++ ++ private io.papermc.paper.util.collisions.CachedShapeData cachedShapeData; ++ private boolean isEmpty; ++ ++ private io.papermc.paper.util.collisions.CachedToAABBs cachedToAABBs; ++ private AABB cachedBounds; ++ ++ private Boolean isFullBlock; ++ ++ private Boolean occludesFullBlock; ++ ++ // must be power of two ++ private static final int MERGED_CACHE_SIZE = 16; ++ ++ private io.papermc.paper.util.collisions.MergedORCache[] mergedORCache; ++ ++ public final double offsetX() { ++ return this.offsetX; ++ } ++ ++ public final double offsetY() { ++ return this.offsetY; ++ } ++ ++ public final double offsetZ() { ++ return this.offsetZ; ++ } ++ ++ public final AABB getSingleAABBRepresentation() { ++ return this.singleAABBRepresentation; ++ } ++ ++ public final double[] rootCoordinatesX() { ++ return this.rootCoordinatesX; ++ } ++ ++ public final double[] rootCoordinatesY() { ++ return this.rootCoordinatesY; ++ } ++ ++ public final double[] rootCoordinatesZ() { ++ return this.rootCoordinatesZ; ++ } ++ ++ private static double[] extractRawArray(final DoubleList list) { ++ if (list instanceof it.unimi.dsi.fastutil.doubles.DoubleArrayList rawList) { ++ final double[] raw = rawList.elements(); ++ final int expected = rawList.size(); ++ if (raw.length == expected) { ++ return raw; ++ } else { ++ return java.util.Arrays.copyOf(raw, expected); ++ } ++ } else { ++ return list.toDoubleArray(); ++ } ++ } ++ ++ public final void initCache() { ++ this.cachedShapeData = this.shape.getOrCreateCachedShapeData(); ++ this.isEmpty = this.cachedShapeData.isEmpty(); ++ ++ final DoubleList xList = this.getCoords(Direction.Axis.X); ++ final DoubleList yList = this.getCoords(Direction.Axis.Y); ++ final DoubleList zList = this.getCoords(Direction.Axis.Z); ++ ++ if (xList instanceof OffsetDoubleList offsetDoubleList) { ++ this.offsetX = offsetDoubleList.offset; ++ this.rootCoordinatesX = extractRawArray(offsetDoubleList.delegate); ++ } else { ++ this.rootCoordinatesX = extractRawArray(xList); ++ } ++ ++ if (yList instanceof OffsetDoubleList offsetDoubleList) { ++ this.offsetY = offsetDoubleList.offset; ++ this.rootCoordinatesY = extractRawArray(offsetDoubleList.delegate); ++ } else { ++ this.rootCoordinatesY = extractRawArray(yList); ++ } ++ ++ if (zList instanceof OffsetDoubleList offsetDoubleList) { ++ this.offsetZ = offsetDoubleList.offset; ++ this.rootCoordinatesZ = extractRawArray(offsetDoubleList.delegate); ++ } else { ++ this.rootCoordinatesZ = extractRawArray(zList); ++ } ++ ++ if (this.cachedShapeData.hasSingleAABB()) { ++ this.singleAABBRepresentation = new AABB( ++ this.rootCoordinatesX[0] + this.offsetX, this.rootCoordinatesY[0] + this.offsetY, this.rootCoordinatesZ[0] + this.offsetZ, ++ this.rootCoordinatesX[1] + this.offsetX, this.rootCoordinatesY[1] + this.offsetY, this.rootCoordinatesZ[1] + this.offsetZ ++ ); ++ this.cachedBounds = this.singleAABBRepresentation; ++ } ++ } ++ ++ public final io.papermc.paper.util.collisions.CachedShapeData getCachedVoxelData() { ++ return this.cachedShapeData; ++ } ++ ++ private VoxelShape[] faceShapeClampedCache; ++ ++ public final VoxelShape getFaceShapeClamped(final Direction direction) { ++ if (this.isEmpty) { ++ return (VoxelShape)(Object)this; ++ } ++ if ((VoxelShape)(Object)this == Shapes.block()) { ++ return (VoxelShape)(Object)this; ++ } ++ ++ VoxelShape[] cache = this.faceShapeClampedCache; ++ if (cache != null) { ++ final VoxelShape ret = cache[direction.ordinal()]; ++ if (ret != null) { ++ return ret; ++ } ++ } ++ ++ ++ if (cache == null) { ++ this.faceShapeClampedCache = cache = new VoxelShape[6]; ++ } ++ ++ final Direction.Axis axis = direction.getAxis(); ++ ++ final VoxelShape ret; ++ ++ if (direction.getAxisDirection() == Direction.AxisDirection.POSITIVE) { ++ if (DoubleMath.fuzzyEquals(this.max(axis), 1.0, io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) { ++ ret = tryForceBlock(new SliceShape((VoxelShape)(Object)this, axis, this.shape.getSize(axis) - 1)); ++ } else { ++ ret = Shapes.empty(); ++ } ++ } else { ++ if (DoubleMath.fuzzyEquals(this.min(axis), 0.0, io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) { ++ ret = tryForceBlock(new SliceShape((VoxelShape)(Object)this, axis, 0)); ++ } else { ++ ret = Shapes.empty(); ++ } ++ } ++ ++ cache[direction.ordinal()] = ret; ++ ++ return ret; ++ } ++ ++ private static VoxelShape tryForceBlock(final VoxelShape other) { ++ if (other == Shapes.block()) { ++ return other; ++ } ++ ++ final AABB otherAABB = other.getSingleAABBRepresentation(); ++ if (otherAABB == null) { ++ return other; ++ } ++ ++ if (Shapes.block().getSingleAABBRepresentation().equals(otherAABB)) { ++ return Shapes.block(); ++ } ++ ++ return other; ++ } ++ ++ private boolean computeOccludesFullBlock() { ++ if (this.isEmpty) { ++ this.occludesFullBlock = Boolean.FALSE; ++ return false; ++ } ++ ++ if (this.isFullBlock()) { ++ this.occludesFullBlock = Boolean.TRUE; ++ return true; ++ } ++ ++ final AABB singleAABB = this.singleAABBRepresentation; ++ if (singleAABB != null) { ++ // check if the bounding box encloses the full cube ++ final boolean ret = ++ (singleAABB.minY <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && singleAABB.maxY >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) && ++ (singleAABB.minX <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && singleAABB.maxX >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) && ++ (singleAABB.minZ <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && singleAABB.maxZ >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)); ++ this.occludesFullBlock = Boolean.valueOf(ret); ++ return ret; ++ } ++ ++ final boolean ret = !Shapes.joinIsNotEmpty(Shapes.block(), ((VoxelShape)(Object)this), BooleanOp.ONLY_FIRST); ++ this.occludesFullBlock = Boolean.valueOf(ret); ++ return ret; ++ } ++ ++ public final boolean occludesFullBlock() { ++ final Boolean ret = this.occludesFullBlock; ++ if (ret != null) { ++ return ret.booleanValue(); ++ } ++ ++ return this.computeOccludesFullBlock(); ++ } ++ ++ public final boolean occludesFullBlockIfCached() { ++ final Boolean ret = this.occludesFullBlock; ++ return ret != null ? ret.booleanValue() : false; ++ } ++ ++ private static int hash(final VoxelShape key) { ++ return it.unimi.dsi.fastutil.HashCommon.mix(System.identityHashCode(key)); ++ } ++ ++ public final VoxelShape orUnoptimized(final VoxelShape other) { ++ // don't cache simple cases ++ if (((VoxelShape)(Object)this) == other) { ++ return other; ++ } ++ ++ if (this.isEmpty) { ++ return other; ++ } ++ ++ if (other.isEmpty()) { ++ return (VoxelShape)(Object)this; ++ } ++ ++ // try this cache first ++ final int thisCacheKey = hash(other) & (MERGED_CACHE_SIZE - 1); ++ final io.papermc.paper.util.collisions.MergedORCache cached = this.mergedORCache == null ? null : this.mergedORCache[thisCacheKey]; ++ if (cached != null && cached.key() == other) { ++ return cached.result(); ++ } ++ ++ // try other cache ++ final int otherCacheKey = hash(this) & (MERGED_CACHE_SIZE - 1); ++ final io.papermc.paper.util.collisions.MergedORCache otherCache = other.mergedORCache == null ? null : other.mergedORCache[otherCacheKey]; ++ if (otherCache != null && otherCache.key() == this) { ++ return otherCache.result(); ++ } ++ ++ // note: unsure if joinUnoptimized(1, 2, OR) == joinUnoptimized(2, 1, OR) for all cases ++ final VoxelShape result = Shapes.joinUnoptimized(this, other, BooleanOp.OR); ++ ++ if (cached != null && otherCache == null) { ++ // try to use second cache instead of replacing an entry in this cache ++ if (other.mergedORCache == null) { ++ other.mergedORCache = new io.papermc.paper.util.collisions.MergedORCache[MERGED_CACHE_SIZE]; ++ } ++ other.mergedORCache[otherCacheKey] = new io.papermc.paper.util.collisions.MergedORCache(this, result); ++ } else { ++ // line is not occupied or other cache line is full ++ // always bias to replace this cache, as this cache is the first we check ++ if (this.mergedORCache == null) { ++ this.mergedORCache = new io.papermc.paper.util.collisions.MergedORCache[MERGED_CACHE_SIZE]; ++ } ++ this.mergedORCache[thisCacheKey] = new io.papermc.paper.util.collisions.MergedORCache(other, result); ++ } ++ ++ return result; ++ } ++ ++ private boolean computeFullBlock() { ++ Boolean ret; ++ if (this.isEmpty) { ++ ret = Boolean.FALSE; ++ } else if ((VoxelShape)(Object)this == Shapes.block()) { ++ ret = Boolean.TRUE; ++ } else { ++ final AABB singleAABB = this.singleAABBRepresentation; ++ if (singleAABB == null) { ++ final io.papermc.paper.util.collisions.CachedShapeData shapeData = this.cachedShapeData; ++ final int sMinX = shapeData.minFullX(); ++ final int sMinY = shapeData.minFullY(); ++ final int sMinZ = shapeData.minFullZ(); ++ ++ final int sMaxX = shapeData.maxFullX(); ++ final int sMaxY = shapeData.maxFullY(); ++ final int sMaxZ = shapeData.maxFullZ(); ++ ++ if (Math.abs(this.rootCoordinatesX[sMinX] + this.offsetX) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && ++ Math.abs(this.rootCoordinatesY[sMinY] + this.offsetY) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && ++ Math.abs(this.rootCoordinatesZ[sMinZ] + this.offsetZ) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && ++ ++ Math.abs(1.0 - (this.rootCoordinatesX[sMaxX] + this.offsetX)) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && ++ Math.abs(1.0 - (this.rootCoordinatesY[sMaxY] + this.offsetY)) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && ++ Math.abs(1.0 - (this.rootCoordinatesZ[sMaxZ] + this.offsetZ)) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) { ++ ++ // index = z + y*sizeZ + x*(sizeZ*sizeY) ++ ++ final int sizeY = shapeData.sizeY(); ++ final int sizeZ = shapeData.sizeZ(); ++ ++ final long[] bitset = shapeData.voxelSet(); ++ ++ ret = Boolean.TRUE; ++ ++ check_full: ++ for (int x = sMinX; x < sMaxX; ++x) { ++ for (int y = sMinY; y < sMaxY; ++y) { ++ final int baseIndex = y*sizeZ + x*(sizeZ*sizeY); ++ if (!io.papermc.paper.util.collisions.FlatBitsetUtil.isRangeSet(bitset, baseIndex + sMinZ, baseIndex + sMaxZ)) { ++ ret = Boolean.FALSE; ++ break check_full; ++ } ++ } ++ } ++ } else { ++ ret = Boolean.FALSE; ++ } ++ } else { ++ ret = Boolean.valueOf( ++ Math.abs(singleAABB.minX) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && ++ Math.abs(singleAABB.minY) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && ++ Math.abs(singleAABB.minZ) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && ++ ++ Math.abs(1.0 - singleAABB.maxX) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && ++ Math.abs(1.0 - singleAABB.maxY) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && ++ Math.abs(1.0 - singleAABB.maxZ) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON ++ ); ++ } ++ } ++ ++ this.isFullBlock = ret; ++ ++ return ret.booleanValue(); ++ } ++ ++ public boolean isFullBlock() { ++ final Boolean ret = this.isFullBlock; ++ ++ if (ret != null) { ++ return ret.booleanValue(); ++ } ++ ++ return this.computeFullBlock(); ++ } ++ // Paper end - optimise collisions ++ ++ protected VoxelShape(DiscreteVoxelShape voxels) { // Paper - protected + this.shape = voxels; + } + + public double min(Direction.Axis axis) { +- int i = this.shape.firstFull(axis); +- return i >= this.shape.getSize(axis) ? Double.POSITIVE_INFINITY : this.get(axis, i); ++ // Paper start - optimise collisions ++ final io.papermc.paper.util.collisions.CachedShapeData shapeData = this.cachedShapeData; ++ switch (axis) { ++ case X: { ++ final int idx = shapeData.minFullX(); ++ return idx >= shapeData.sizeX() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesX[idx] + this.offsetX); ++ } ++ case Y: { ++ final int idx = shapeData.minFullY(); ++ return idx >= shapeData.sizeY() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesY[idx] + this.offsetY); ++ } ++ case Z: { ++ final int idx = shapeData.minFullZ(); ++ return idx >= shapeData.sizeZ() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesZ[idx] + this.offsetZ); ++ } ++ default: { ++ // should never get here ++ return Double.POSITIVE_INFINITY; ++ } ++ } ++ // Paper end - optimise collisions + } + + public double max(Direction.Axis axis) { +- int i = this.shape.lastFull(axis); +- return i <= 0 ? Double.NEGATIVE_INFINITY : this.get(axis, i); ++ // Paper start - optimise collisions ++ final io.papermc.paper.util.collisions.CachedShapeData shapeData = this.cachedShapeData; ++ switch (axis) { ++ case X: { ++ final int idx = shapeData.maxFullX(); ++ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesX[idx] + this.offsetX); ++ } ++ case Y: { ++ final int idx = shapeData.maxFullY(); ++ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesY[idx] + this.offsetY); ++ } ++ case Z: { ++ final int idx = shapeData.maxFullZ(); ++ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesZ[idx] + this.offsetZ); ++ } ++ default: { ++ // should never get here ++ return Double.NEGATIVE_INFINITY; ++ } ++ } ++ // Paper end - optimise collisions + } + + public AABB bounds() { +- if (this.isEmpty()) { +- throw (UnsupportedOperationException)Util.pauseInIde(new UnsupportedOperationException("No bounds for empty shape.")); +- } else { +- return new AABB( +- this.min(Direction.Axis.X), +- this.min(Direction.Axis.Y), +- this.min(Direction.Axis.Z), +- this.max(Direction.Axis.X), +- this.max(Direction.Axis.Y), +- this.max(Direction.Axis.Z) +- ); ++ // Paper start - optimise collisions ++ if (this.isEmpty) { ++ throw Util.pauseInIde(new UnsupportedOperationException("No bounds for empty shape.")); ++ } ++ AABB cached = this.cachedBounds; ++ if (cached != null) { ++ return cached; + } ++ ++ final io.papermc.paper.util.collisions.CachedShapeData shapeData = this.cachedShapeData; ++ ++ final double[] coordsX = this.rootCoordinatesX; ++ final double[] coordsY = this.rootCoordinatesY; ++ final double[] coordsZ = this.rootCoordinatesZ; ++ ++ final double offX = this.offsetX; ++ final double offY = this.offsetY; ++ final double offZ = this.offsetZ; ++ ++ // note: if not empty, then there is one full AABB so no bounds checks are needed on the minFull/maxFull indices ++ cached = new AABB( ++ coordsX[shapeData.minFullX()] + offX, ++ coordsY[shapeData.minFullY()] + offY, ++ coordsZ[shapeData.minFullZ()] + offZ, ++ ++ coordsX[shapeData.maxFullX()] + offX, ++ coordsY[shapeData.maxFullY()] + offY, ++ coordsZ[shapeData.maxFullZ()] + offZ ++ ); ++ ++ this.cachedBounds = cached; ++ return cached; ++ // Paper end - optimise collisions + } + + public VoxelShape singleEncompassing() { +@@ -69,28 +470,106 @@ public abstract class VoxelShape { + protected abstract DoubleList getCoords(Direction.Axis axis); + + public boolean isEmpty() { +- return this.shape.isEmpty(); ++ return this.isEmpty; // Paper - optimise collisions ++ } ++ ++ // Paper start - optimise collisions ++ private static DoubleList offsetList(final DoubleList src, final double by) { ++ if (src instanceof OffsetDoubleList offsetDoubleList) { ++ return new OffsetDoubleList(offsetDoubleList.delegate, by + offsetDoubleList.offset); ++ } ++ return new OffsetDoubleList(src, by); + } ++ // Paper end - optimise collisions + + public VoxelShape move(double x, double y, double z) { +- return (VoxelShape)(this.isEmpty() +- ? Shapes.empty() +- : new ArrayVoxelShape( ++ // Paper start - optimise collisions ++ if (this.isEmpty) { ++ return Shapes.empty(); ++ } ++ ++ final ArrayVoxelShape ret = new ArrayVoxelShape( + this.shape, +- new OffsetDoubleList(this.getCoords(Direction.Axis.X), x), +- new OffsetDoubleList(this.getCoords(Direction.Axis.Y), y), +- new OffsetDoubleList(this.getCoords(Direction.Axis.Z), z) +- )); ++ offsetList(this.getCoords(Direction.Axis.X), x), ++ offsetList(this.getCoords(Direction.Axis.Y), y), ++ offsetList(this.getCoords(Direction.Axis.Z), z) ++ ); ++ ++ final io.papermc.paper.util.collisions.CachedToAABBs cachedToAABBs = this.cachedToAABBs; ++ if (cachedToAABBs != null) { ++ ((VoxelShape)ret).cachedToAABBs = io.papermc.paper.util.collisions.CachedToAABBs.offset(cachedToAABBs, x, y, z); ++ } ++ ++ return ret; ++ // Paper end - optimise collisions + } + + public VoxelShape optimize() { +- VoxelShape[] voxelShapes = new VoxelShape[]{Shapes.empty()}; +- this.forAllBoxes( +- (minX, minY, minZ, maxX, maxY, maxZ) -> voxelShapes[0] = Shapes.joinUnoptimized( +- voxelShapes[0], Shapes.box(minX, minY, minZ, maxX, maxY, maxZ), BooleanOp.OR +- ) +- ); +- return voxelShapes[0]; ++ // Paper start - optimise collisions ++ // Optimise merge strategy to increase the number of simple joins, and additionally forward the toAabbs cache ++ // to result ++ if (this.isEmpty) { ++ return Shapes.empty(); ++ } ++ ++ if (this.singleAABBRepresentation != null) { ++ // note: the isFullBlock() is fuzzy, and Shapes.create() is also fuzzy which would return block() ++ return this.isFullBlock() ? Shapes.block() : this; ++ } ++ ++ final List aabbs = this.toAabbs(); ++ ++ if (aabbs.size() == 1) { ++ final AABB singleAABB = aabbs.get(0); ++ final VoxelShape ret = Shapes.create(singleAABB); ++ ++ // forward AABB cache ++ if (ret.cachedToAABBs == null) { ++ ret.cachedToAABBs = this.cachedToAABBs; ++ } ++ ++ return ret; ++ } else { ++ // reduce complexity of joins by splitting the merges (old complexity: n^2, new: nlogn) ++ ++ // set up flat array so that this merge is done in-place ++ final VoxelShape[] tmp = new VoxelShape[aabbs.size()]; ++ ++ // initialise as unmerged ++ for (int i = 0, len = aabbs.size(); i < len; ++i) { ++ tmp[i] = Shapes.create(aabbs.get(i)); ++ } ++ ++ int size = aabbs.size(); ++ while (size > 1) { ++ int newSize = 0; ++ for (int i = 0; i < size; i += 2) { ++ final int next = i + 1; ++ if (next >= size) { ++ // nothing to merge with, so leave it for next iteration ++ tmp[newSize++] = tmp[i]; ++ break; ++ } else { ++ // merge with adjacent ++ final VoxelShape first = tmp[i]; ++ final VoxelShape second = tmp[next]; ++ ++ tmp[newSize++] = Shapes.joinUnoptimized(first, second, BooleanOp.OR); ++ } ++ } ++ size = newSize; ++ } ++ ++ final VoxelShape ret = tmp[0]; ++ ++ // forward AABB cache ++ if (ret.cachedToAABBs == null) { ++ ret.cachedToAABBs = this.cachedToAABBs; ++ } ++ ++ return ret; ++ } ++ // Paper end - optimise collisions + } + + public void forAllEdges(Shapes.DoubleLineConsumer consumer) { +@@ -126,10 +605,43 @@ public abstract class VoxelShape { + ); + } + ++ // Paper start - optimise collisions ++ private List toAabbsUncached() { ++ final List ret = new java.util.ArrayList<>(); ++ if (this.singleAABBRepresentation != null) { ++ ret.add(this.singleAABBRepresentation); ++ } else { ++ this.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { ++ ret.add(new AABB(minX, minY, minZ, maxX, maxY, maxZ)); ++ }); ++ } ++ ++ // cache result ++ this.cachedToAABBs = new io.papermc.paper.util.collisions.CachedToAABBs(ret, false, 0.0, 0.0, 0.0); ++ ++ return ret; ++ } ++ // Paper end - optimise collisions ++ + public List toAabbs() { +- List list = Lists.newArrayList(); +- this.forAllBoxes((x1, y1, z1, x2, y2, z2) -> list.add(new AABB(x1, y1, z1, x2, y2, z2))); +- return list; ++ // Paper start - optimise collisions ++ io.papermc.paper.util.collisions.CachedToAABBs cachedToAABBs = this.cachedToAABBs; ++ if (cachedToAABBs != null) { ++ if (!cachedToAABBs.isOffset()) { ++ return cachedToAABBs.aabbs(); ++ } ++ ++ // all we need to do is offset the cache ++ cachedToAABBs = cachedToAABBs.removeOffset(); ++ // update cache ++ this.cachedToAABBs = cachedToAABBs; ++ ++ return cachedToAABBs.aabbs(); ++ } ++ ++ // make new cache ++ return this.toAabbsUncached(); ++ // Paper end - optimise collisions + } + + public double min(Direction.Axis axis, double from, double to) { +@@ -154,43 +666,85 @@ public abstract class VoxelShape { + return Mth.binarySearch(0, this.shape.getSize(axis) + 1, i -> coord < this.get(axis, i)) - 1; + } + ++ // Paper start - optimise collisions ++ /** ++ * Copy of AABB#clip but for one AABB ++ */ ++ private static BlockHitResult clip(final AABB aabb, final Vec3 from, final Vec3 to, final BlockPos offset) { ++ final double[] minDistanceArr = new double[] { 1.0 }; ++ final double diffX = to.x - from.x; ++ final double diffY = to.y - from.y; ++ final double diffZ = to.z - from.z; ++ ++ final Direction direction = AABB.getDirection(aabb.move(offset), from, minDistanceArr, null, diffX, diffY, diffZ); ++ ++ if (direction == null) { ++ return null; ++ } ++ ++ final double minDistance = minDistanceArr[0]; ++ return new BlockHitResult(from.add(minDistance * diffX, minDistance * diffY, minDistance * diffZ), direction, offset, false); ++ } ++ // Paper end - optimise collisions ++ + @Nullable + public BlockHitResult clip(Vec3 start, Vec3 end, BlockPos pos) { +- if (this.isEmpty()) { ++ // Paper start - optimise collisions ++ if (this.isEmpty) { + return null; +- } else { +- Vec3 vec3 = end.subtract(start); +- if (vec3.lengthSqr() < 1.0E-7) { +- return null; +- } else { +- Vec3 vec32 = start.add(vec3.scale(0.001)); +- return this.shape +- .isFullWide( +- this.findIndex(Direction.Axis.X, vec32.x - (double)pos.getX()), +- this.findIndex(Direction.Axis.Y, vec32.y - (double)pos.getY()), +- this.findIndex(Direction.Axis.Z, vec32.z - (double)pos.getZ()) +- ) +- ? new BlockHitResult(vec32, Direction.getNearest(vec3.x, vec3.y, vec3.z).getOpposite(), pos, true) +- : AABB.clip(this.toAabbs(), start, end, pos); ++ } ++ ++ final Vec3 directionOpposite = end.subtract(start); ++ if (directionOpposite.lengthSqr() < io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) { ++ return null; ++ } ++ ++ final Vec3 fromBehind = start.add(directionOpposite.scale(0.001)); ++ final double fromBehindOffsetX = fromBehind.x - (double)pos.getX(); ++ final double fromBehindOffsetY = fromBehind.y - (double)pos.getY(); ++ final double fromBehindOffsetZ = fromBehind.z - (double)pos.getZ(); ++ ++ final AABB singleAABB = this.singleAABBRepresentation; ++ if (singleAABB != null) { ++ if (singleAABB.contains(fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) { ++ return new BlockHitResult(fromBehind, Direction.getNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), pos, true); + } ++ return clip(singleAABB, start, end, pos); + } ++ ++ if (io.papermc.paper.util.CollisionUtil.strictlyContains(this, fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) { ++ return new BlockHitResult(fromBehind, Direction.getNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), pos, true); ++ } ++ ++ return AABB.clip(this.toAabbs(), start, end, pos); ++ // Paper end - optimise collisions + } + + public Optional closestPointTo(Vec3 target) { +- if (this.isEmpty()) { ++ // Paper start - optimise collisions ++ if (this.isEmpty) { + return Optional.empty(); +- } else { +- Vec3[] vec3s = new Vec3[1]; +- this.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { +- double d = Mth.clamp(target.x(), minX, maxX); +- double e = Mth.clamp(target.y(), minY, maxY); +- double f = Mth.clamp(target.z(), minZ, maxZ); +- if (vec3s[0] == null || target.distanceToSqr(d, e, f) < target.distanceToSqr(vec3s[0])) { +- vec3s[0] = new Vec3(d, e, f); +- } +- }); +- return Optional.of(vec3s[0]); + } ++ ++ Vec3 ret = null; ++ double retDistance = Double.MAX_VALUE; ++ ++ final List aabbs = this.toAabbs(); ++ for (int i = 0, len = aabbs.size(); i < len; ++i) { ++ final AABB aabb = aabbs.get(i); ++ final double x = Mth.clamp(target.x, aabb.minX, aabb.maxX); ++ final double y = Mth.clamp(target.y, aabb.minY, aabb.maxY); ++ final double z = Mth.clamp(target.z, aabb.minZ, aabb.maxZ); ++ ++ double dist = target.distanceToSqr(x, y, z); ++ if (dist < retDistance) { ++ ret = new Vec3(x, y, z); ++ retDistance = dist; ++ } ++ } ++ ++ return Optional.ofNullable(ret); ++ // Paper end - optimise collisions + } + + public VoxelShape getFaceShape(Direction facing) { +@@ -227,7 +781,28 @@ public abstract class VoxelShape { + } + + public double collide(Direction.Axis axis, AABB box, double maxDist) { +- return this.collideX(AxisCycle.between(axis, Direction.Axis.X), box, maxDist); ++ // Paper start - optimise collisions ++ if (this.isEmpty) { ++ return maxDist; ++ } ++ if (Math.abs(maxDist) < io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) { ++ return 0.0; ++ } ++ switch (axis) { ++ case X: { ++ return io.papermc.paper.util.CollisionUtil.collideX(this, box, maxDist); ++ } ++ case Y: { ++ return io.papermc.paper.util.CollisionUtil.collideY(this, box, maxDist); ++ } ++ case Z: { ++ return io.papermc.paper.util.CollisionUtil.collideZ(this, box, maxDist); ++ } ++ default: { ++ throw new RuntimeException("Unknown axis: " + axis); ++ } ++ } ++ // Paper end - optimise collisions + } + + protected double collideX(AxisCycle axisCycle, AABB box, double maxDist) { diff --git a/patches/server/1018-Detail-more-information-in-watchdog-dumps.patch b/patches/server/1018-Detail-more-information-in-watchdog-dumps.patch deleted file mode 100644 index 7859dad6a7..0000000000 --- a/patches/server/1018-Detail-more-information-in-watchdog-dumps.patch +++ /dev/null @@ -1,296 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Thu, 26 Mar 2020 21:59:32 -0700 -Subject: [PATCH] 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 - -diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java -index 63066dfde00569833546da01cd580434f8be6593..7fb162fa031fd76aa9a94f5fdaa3e32ceb9b9abc 100644 ---- a/src/main/java/net/minecraft/network/Connection.java -+++ b/src/main/java/net/minecraft/network/Connection.java -@@ -607,7 +607,13 @@ public class Connection extends SimpleChannelInboundHandler> { - if (!(this.packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) - || loginPacketListener.state != net.minecraft.server.network.ServerLoginPacketListenerImpl.State.VERIFYING - || Connection.joinAttemptsThisTick++ < MAX_PER_TICK) { -+ // Paper start - detailed watchdog information -+ net.minecraft.network.protocol.PacketUtils.packetProcessing.push(this.packetListener); -+ try { - tickablepacketlistener.tick(); -+ } finally { -+ net.minecraft.network.protocol.PacketUtils.packetProcessing.pop(); -+ } // Paper end - detailed watchdog information - } // Paper end - Buffer joins to world - } - -diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java -index e161ad0f53a21a68e8c78575ba5d3cdbdb11fca0..57e76b53e5e314c3e6b8856010f7a84188121582 100644 ---- a/src/main/java/net/minecraft/network/protocol/PacketUtils.java -+++ b/src/main/java/net/minecraft/network/protocol/PacketUtils.java -@@ -19,6 +19,24 @@ public class PacketUtils { - - private static final Logger LOGGER = LogUtils.getLogger(); - -+ // Paper start - detailed watchdog information -+ public static final java.util.concurrent.ConcurrentLinkedDeque packetProcessing = new java.util.concurrent.ConcurrentLinkedDeque<>(); -+ 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; -+ } -+ // Paper end - detailed watchdog information -+ - public PacketUtils() {} - - public static void ensureRunningOnSameThread(Packet packet, T listener, ServerLevel world) throws RunningOnDifferentThreadException { -@@ -28,6 +46,8 @@ public class PacketUtils { - public static void ensureRunningOnSameThread(Packet packet, T listener, BlockableEventLoop engine) throws RunningOnDifferentThreadException { - if (!engine.isSameThread()) { - engine.executeIfPossible(() -> { -+ packetProcessing.push(listener); // Paper - detailed watchdog information -+ try { // Paper - detailed watchdog information - if (listener instanceof ServerCommonPacketListenerImpl serverCommonPacketListener && serverCommonPacketListener.processedDisconnect) return; // CraftBukkit - Don't handle sync packets for kicked players - if (listener.shouldHandleMessage(packet)) { - co.aikar.timings.Timing timing = co.aikar.timings.MinecraftTimings.getPacketTiming(packet); // Paper - timings -@@ -47,6 +67,12 @@ public class PacketUtils { - } else { - PacketUtils.LOGGER.debug("Ignoring packet due to disconnection: {}", packet); - } -+ // Paper start - detailed watchdog information -+ } finally { -+ totalMainThreadPacketsProcessed.getAndIncrement(); -+ packetProcessing.pop(); -+ } -+ // Paper end - detailed watchdog information - - }); - throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD; -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 215f03dfe0e6441588679efaedbcf04c352b404c..aabaaf89f5e344791604a7e05122d9865bb0224e 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -1251,7 +1251,26 @@ public class ServerLevel extends Level implements WorldGenLevel { - - } - -+ // Paper start - log detailed entity tick information -+ // TODO replace with varhandle -+ static final java.util.concurrent.atomic.AtomicReference currentlyTickingEntity = new java.util.concurrent.atomic.AtomicReference<>(); -+ -+ public static List getCurrentlyTickingEntities() { -+ Entity ticking = currentlyTickingEntity.get(); -+ List ret = java.util.Arrays.asList(ticking == null ? new Entity[0] : new Entity[] { ticking }); -+ -+ return ret; -+ } -+ // Paper end - log detailed entity tick information -+ - public void tickNonPassenger(Entity entity) { -+ // Paper start - log detailed entity tick information -+ io.papermc.paper.util.TickThread.ensureTickThread("Cannot tick an entity off-main"); -+ try { -+ if (currentlyTickingEntity.get() == null) { -+ currentlyTickingEntity.lazySet(entity); -+ } -+ // Paper end - log detailed entity tick information - ++TimingHistory.entityTicks; // Paper - timings - // Spigot start - co.aikar.timings.Timing timer; // Paper -@@ -1291,7 +1310,13 @@ public class ServerLevel extends Level implements WorldGenLevel { - this.tickPassenger(entity, entity1); - } - // } finally { timer.stopTiming(); } // Paper - timings - move up -- -+ // Paper start - log detailed entity tick information -+ } finally { -+ if (currentlyTickingEntity.get() == entity) { -+ currentlyTickingEntity.lazySet(null); -+ } -+ } -+ // Paper end - log detailed entity tick information - } - - private void tickPassenger(Entity vehicle, Entity passenger) { -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 257943b4c984d6faee29eca17c8f951e7f43168b..0fbcf60a994f67bdd81d40e4a8bf38f0cbb8993d 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -1074,8 +1074,43 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - return this.onGround; - } - -+ // Paper start - detailed watchdog information -+ public final Object posLock = new Object(); // Paper - log detailed entity tick information -+ -+ private Vec3 moveVector; -+ private double moveStartX; -+ private double moveStartY; -+ private double moveStartZ; -+ -+ public final Vec3 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; -+ } -+ // Paper end - detailed watchdog information -+ - public void move(MoverType movementType, Vec3 movement) { - final Vec3 originalMovement = movement; // Paper - Expose pre-collision velocity -+ // Paper start - detailed watchdog information -+ io.papermc.paper.util.TickThread.ensureTickThread("Cannot move an entity off-main"); -+ synchronized (this.posLock) { -+ this.moveStartX = this.getX(); -+ this.moveStartY = this.getY(); -+ this.moveStartZ = this.getZ(); -+ this.moveVector = movement; -+ } -+ try { -+ // Paper end - detailed watchdog information - if (this.noPhysics) { - this.setPos(this.getX() + movement.x, this.getY() + movement.y, this.getZ() + movement.z); - } else { -@@ -1245,6 +1280,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - this.level().getProfiler().pop(); - } - } -+ // Paper start - detailed watchdog information -+ } finally { -+ synchronized (this.posLock) { // Paper -+ this.moveVector = null; -+ } // Paper -+ } -+ // Paper end - detailed watchdog information - } - - private boolean isStateClimbable(BlockState state) { -@@ -4405,7 +4447,9 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - - public void setDeltaMovement(Vec3 velocity) { -+ synchronized (this.posLock) { // Paper - this.deltaMovement = velocity; -+ } // Paper - } - - public void addDeltaMovement(Vec3 velocity) { -@@ -4508,7 +4552,9 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - // Paper end - Fix MC-4 - if (this.position.x != x || this.position.y != y || this.position.z != z) { -+ synchronized (this.posLock) { // Paper - this.position = new Vec3(x, y, z); -+ } // Paper - int i = Mth.floor(x); - int j = Mth.floor(y); - int k = Mth.floor(z); -diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java -index 577f29519156f33b49ee319a64a52249630e28d8..6db566e3111ec08a99aa429624979cb83a85e272 100644 ---- a/src/main/java/org/spigotmc/WatchdogThread.java -+++ b/src/main/java/org/spigotmc/WatchdogThread.java -@@ -22,6 +22,78 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa - private volatile long lastTick; - private volatile boolean stopping; - -+ // Paper start - log detailed tick information -+ private void dumpEntity(net.minecraft.world.entity.Entity entity) { -+ Logger log = Bukkit.getServer().getLogger(); -+ double posX, posY, posZ; -+ net.minecraft.world.phys.Vec3 mot; -+ double moveStartX, moveStartY, moveStartZ; -+ net.minecraft.world.phys.Vec3 moveVec; -+ synchronized (entity.posLock) { -+ posX = entity.getX(); -+ posY = entity.getY(); -+ posZ = entity.getZ(); -+ mot = entity.getDeltaMovement(); -+ moveStartX = entity.getMoveStartX(); -+ moveStartY = entity.getMoveStartY(); -+ moveStartZ = entity.getMoveStartZ(); -+ moveVec = entity.getMoveVector(); -+ } -+ -+ String entityType = net.minecraft.world.entity.EntityType.getKey(entity.getType()).toString(); -+ java.util.UUID entityUUID = entity.getUUID(); -+ net.minecraft.world.level.Level world = entity.level(); -+ -+ log.log(Level.SEVERE, "Ticking entity: " + entityType + ", entity class: " + entity.getClass().getName()); -+ log.log(Level.SEVERE, "Entity status: removed: " + entity.isRemoved() + ", valid: " + entity.valid + ", alive: " + entity.isAlive() + ", is passenger: " + entity.isPassenger()); -+ log.log(Level.SEVERE, "Entity UUID: " + entityUUID); -+ 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)"); -+ log.log(Level.SEVERE, "Entity AABB: " + entity.getBoundingBox()); -+ 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()); -+ } -+ } -+ -+ private void dumpTickingInfo() { -+ Logger log = Bukkit.getServer().getLogger(); -+ -+ // ticking entities -+ for (net.minecraft.world.entity.Entity entity : net.minecraft.server.level.ServerLevel.getCurrentlyTickingEntities()) { -+ this.dumpEntity(entity); -+ net.minecraft.world.entity.Entity vehicle = entity.getVehicle(); -+ if (vehicle != null) { -+ log.log(Level.SEVERE, "Detailing vehicle for above entity:"); -+ this.dumpEntity(vehicle); -+ } -+ } -+ -+ // packet processors -+ for (net.minecraft.network.PacketListener packetListener : net.minecraft.network.protocol.PacketUtils.getCurrentPacketProcessors()) { -+ if (packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl) { -+ net.minecraft.server.level.ServerPlayer player = ((net.minecraft.server.network.ServerGamePacketListenerImpl)packetListener).player; -+ long totalPackets = net.minecraft.network.protocol.PacketUtils.getTotalProcessedPackets(); -+ if (player == null) { -+ log.log(Level.SEVERE, "Handling packet for player connection or ticking player connection (null player): " + packetListener); -+ log.log(Level.SEVERE, "Total packets processed on the main thread for all players: " + totalPackets); -+ } else { -+ this.dumpEntity(player); -+ net.minecraft.world.entity.Entity vehicle = player.getVehicle(); -+ if (vehicle != null) { -+ log.log(Level.SEVERE, "Detailing vehicle for above entity:"); -+ this.dumpEntity(vehicle); -+ } -+ 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); -+ } -+ } -+ } -+ // Paper end - log detailed tick information -+ - private WatchdogThread(long timeoutTime, boolean restart) - { - super( "Paper Watchdog Thread" ); -@@ -119,6 +191,7 @@ public final class WatchdogThread extends io.papermc.paper.util.TickThread // Pa - log.log( Level.SEVERE, "------------------------------" ); - log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper - io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(isLongTimeout); // Paper - rewrite chunk system -+ this.dumpTickingInfo(); // Paper - log detailed tick information - WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log ); - log.log( Level.SEVERE, "------------------------------" ); - // diff --git a/patches/server/1019-Collision-optimisations.patch b/patches/server/1019-Collision-optimisations.patch deleted file mode 100644 index a6179487ff..0000000000 --- a/patches/server/1019-Collision-optimisations.patch +++ /dev/null @@ -1,4726 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Mon, 4 May 2020 10:06:24 -0700 -Subject: [PATCH] Collision optimisations - -The collision patch has been designed with the assumption that -most shapes are either a single AABB or an ArrayVoxelShape -(typical voxel bitset representation). Like previously, -single AABB shapes are treated as AABBs. Unlike previously, the -VoxelShape class has been changed to carry shape data that -ArrayVoxelShape would, except in a discrete manner rather -than abstracted away (not hidden behind DoubleList and -the poorly named DiscreteVoxelShape). - -VoxelShape now carries three important states: - 1. The voxel bitset + its sizes for the X, Y, and Z axis - 2. The voxel coordinates (represented as an array and an offset per axis) - 3. Single AABB representation, if possible - -Note that if the single AABB representation is present, -it is used instead of the voxel bitset representation as -the single AABB representation is a special case of the -voxel bitset representation and can be optimised as such. - -This effectively turns every VoxelShape instance, regardless of -actual class, into a typical voxel bitset representation. -This allows all VoxelShape operations to be optimised -for voxel bitset representations without dealing with the -abstraction and indirection that was imposed on VoxelShape -by Mojang. The patch now effectively optimises all VoxelShape -operations. Below is a list of some of the operations optimised: - - Shape merging/ORing - - Shape optimisation - - Occlusion checking - - Non-single AABB VoxelShape collisions/intersection - - Shape raytracing - - Empty VoxelShape testing - -This patch also includes optimisations for raytracing, -which mostly boil down to removing indirection caused by the -interface BlockGetter which allows chunk caching. - -diff --git a/src/main/java/io/papermc/paper/util/CachedLists.java b/src/main/java/io/papermc/paper/util/CachedLists.java -index be668387f65a633c6ac497fca632a4767a1bf3a2..e08f4e39db4ee3fed62e37364d17dcc5c5683504 100644 ---- a/src/main/java/io/papermc/paper/util/CachedLists.java -+++ b/src/main/java/io/papermc/paper/util/CachedLists.java -@@ -1,8 +1,57 @@ - package io.papermc.paper.util; - -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.phys.AABB; -+import org.bukkit.Bukkit; -+import org.bukkit.craftbukkit.util.UnsafeList; -+import java.util.List; -+ - public final class CachedLists { - -- public static void reset() { -+ // Paper start - optimise collisions -+ 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; -+ } -+ // Paper end - optimise collisions -+ -+ public static void reset() { -+ // Paper start - optimise collisions -+ TEMP_COLLISION_LIST.completeReset(); -+ TEMP_GET_ENTITIES_LIST.completeReset(); -+ // Paper end - optimise collisions - } - } -diff --git a/src/main/java/io/papermc/paper/util/CollisionUtil.java b/src/main/java/io/papermc/paper/util/CollisionUtil.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ee8e9c0e3690e78f3cc621ddfca89ea4256d4803 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/util/CollisionUtil.java -@@ -0,0 +1,1851 @@ -+package io.papermc.paper.util; -+ -+import io.papermc.paper.util.collisions.CachedShapeData; -+import it.unimi.dsi.fastutil.doubles.DoubleArrayList; -+import it.unimi.dsi.fastutil.doubles.DoubleList; -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.Direction; -+import net.minecraft.server.level.ServerChunkCache; -+import net.minecraft.util.Mth; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.item.Item; -+import net.minecraft.world.level.CollisionGetter; -+import net.minecraft.world.level.EntityGetter; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.block.Blocks; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.level.border.WorldBorder; -+import net.minecraft.world.level.chunk.ChunkAccess; -+import net.minecraft.world.level.chunk.status.ChunkStatus; -+import net.minecraft.world.level.chunk.LevelChunkSection; -+import net.minecraft.world.level.chunk.PalettedContainer; -+import net.minecraft.world.level.material.FluidState; -+import net.minecraft.world.phys.AABB; -+import net.minecraft.world.phys.Vec3; -+import net.minecraft.world.phys.shapes.ArrayVoxelShape; -+import net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape; -+import net.minecraft.world.phys.shapes.BooleanOp; -+import net.minecraft.world.phys.shapes.CollisionContext; -+import net.minecraft.world.phys.shapes.DiscreteVoxelShape; -+import net.minecraft.world.phys.shapes.EntityCollisionContext; -+import net.minecraft.world.phys.shapes.OffsetDoubleList; -+import net.minecraft.world.phys.shapes.Shapes; -+import net.minecraft.world.phys.shapes.VoxelShape; -+import java.util.Arrays; -+import java.util.List; -+import java.util.function.BiPredicate; -+import java.util.function.Predicate; -+ -+public final class CollisionUtil { -+ -+ public static final double COLLISION_EPSILON = 1.0E-7; -+ public static final DoubleArrayList ZERO_ONE = DoubleArrayList.wrap(new double[] { 0.0, 1.0 }); -+ -+ public static boolean isSpecialCollidingBlock(final net.minecraft.world.level.block.state.BlockBehaviour.BlockStateBase block) { -+ return block.hasLargeCollisionShape() || block.getBlock() == Blocks.MOVING_PISTON; -+ } -+ -+ public static boolean isEmpty(final AABB aabb) { -+ return (aabb.maxX - aabb.minX) < COLLISION_EPSILON || (aabb.maxY - aabb.minY) < COLLISION_EPSILON || (aabb.maxZ - aabb.minZ) < COLLISION_EPSILON; -+ } -+ -+ public static boolean isEmpty(final double minX, final double minY, final double minZ, -+ final double maxX, final double maxY, final double maxZ) { -+ return (maxX - minX) < COLLISION_EPSILON || (maxY - minY) < COLLISION_EPSILON || (maxZ - minZ) < COLLISION_EPSILON; -+ } -+ -+ public static AABB getBoxForChunk(final int chunkX, final 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 AABB(x - 3*COLLISION_EPSILON, Double.NEGATIVE_INFINITY, z - 3*COLLISION_EPSILON, -+ x + (16.0 + 3*COLLISION_EPSILON), Double.POSITIVE_INFINITY, z + (16.0 + 3*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 static boolean voxelShapeIntersect(final double minX1, final double minY1, final double minZ1, final double maxX1, -+ final double maxY1, final double maxZ1, final double minX2, final double minY2, -+ final double minZ2, final double maxX2, final double maxY2, final double maxZ2) { -+ return (minX1 - maxX2) < -COLLISION_EPSILON && (maxX1 - minX2) > COLLISION_EPSILON && -+ (minY1 - maxY2) < -COLLISION_EPSILON && (maxY1 - minY2) > COLLISION_EPSILON && -+ (minZ1 - maxZ2) < -COLLISION_EPSILON && (maxZ1 - minZ2) > COLLISION_EPSILON; -+ } -+ -+ public static boolean voxelShapeIntersect(final AABB box, final double minX, final double minY, final double minZ, -+ final double maxX, final double maxY, final double maxZ) { -+ return (box.minX - maxX) < -COLLISION_EPSILON && (box.maxX - minX) > COLLISION_EPSILON && -+ (box.minY - maxY) < -COLLISION_EPSILON && (box.maxY - minY) > COLLISION_EPSILON && -+ (box.minZ - maxZ) < -COLLISION_EPSILON && (box.maxZ - minZ) > COLLISION_EPSILON; -+ } -+ -+ public static boolean voxelShapeIntersect(final AABB box1, final AABB box2) { -+ return (box1.minX - box2.maxX) < -COLLISION_EPSILON && (box1.maxX - box2.minX) > COLLISION_EPSILON && -+ (box1.minY - box2.maxY) < -COLLISION_EPSILON && (box1.maxY - box2.minY) > COLLISION_EPSILON && -+ (box1.minZ - box2.maxZ) < -COLLISION_EPSILON && (box1.maxZ - box2.minZ) > COLLISION_EPSILON; -+ } -+ -+ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON -+ public static double collideX(final AABB target, final AABB source, final double source_move) { -+ if ((source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON && -+ (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) { -+ if (source_move >= 0.0) { -+ final double max_move = target.minX - source.maxX; // < 0.0 if no strict collision -+ if (max_move < -COLLISION_EPSILON) { -+ return source_move; -+ } -+ return Math.min(max_move, source_move); -+ } else { -+ final double max_move = target.maxX - source.minX; // > 0.0 if no strict collision -+ if (max_move > COLLISION_EPSILON) { -+ return source_move; -+ } -+ return Math.max(max_move, source_move); -+ } -+ } -+ return source_move; -+ } -+ -+ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON -+ public static double collideY(final AABB target, final AABB source, final double source_move) { -+ if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON && -+ (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) { -+ if (source_move >= 0.0) { -+ final double max_move = target.minY - source.maxY; // < 0.0 if no strict collision -+ if (max_move < -COLLISION_EPSILON) { -+ return source_move; -+ } -+ return Math.min(max_move, source_move); -+ } else { -+ final double max_move = target.maxY - source.minY; // > 0.0 if no strict collision -+ if (max_move > COLLISION_EPSILON) { -+ return source_move; -+ } -+ return Math.max(max_move, source_move); -+ } -+ } -+ return source_move; -+ } -+ -+ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON -+ public static double collideZ(final AABB target, final AABB source, final double source_move) { -+ if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON && -+ (source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON) { -+ if (source_move >= 0.0) { -+ final double max_move = target.minZ - source.maxZ; // < 0.0 if no strict collision -+ if (max_move < -COLLISION_EPSILON) { -+ return source_move; -+ } -+ return Math.min(max_move, source_move); -+ } else { -+ final double max_move = target.maxZ - source.minZ; // > 0.0 if no strict collision -+ if (max_move > COLLISION_EPSILON) { -+ return source_move; -+ } -+ return Math.max(max_move, source_move); -+ } -+ } -+ return source_move; -+ } -+ -+ // startIndex and endIndex inclusive -+ // assumes indices are in range of array -+ private static int findFloor(final double[] values, final double value, int startIndex, int endIndex) { -+ do { -+ final int middle = (startIndex + endIndex) >>> 1; -+ final double middleVal = values[middle]; -+ -+ if (value < middleVal) { -+ endIndex = middle - 1; -+ } else { -+ startIndex = middle + 1; -+ } -+ } while (startIndex <= endIndex); -+ -+ return startIndex - 1; -+ } -+ -+ public static boolean voxelShapeIntersectNoEmpty(final VoxelShape voxel, final AABB aabb) { -+ if (voxel.isEmpty()) { -+ return false; -+ } -+ -+ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true -+ -+ // offsets that should be applied to coords -+ final double off_x = voxel.offsetX(); -+ final double off_y = voxel.offsetY(); -+ final double off_z = voxel.offsetZ(); -+ -+ final double[] coords_x = voxel.rootCoordinatesX(); -+ final double[] coords_y = voxel.rootCoordinatesY(); -+ final double[] coords_z = voxel.rootCoordinatesZ(); -+ -+ final CachedShapeData cached_shape_data = voxel.getCachedVoxelData(); -+ -+ // note: size = coords.length - 1 -+ final int size_x = cached_shape_data.sizeX(); -+ final int size_y = cached_shape_data.sizeY(); -+ final int size_z = cached_shape_data.sizeZ(); -+ -+ // note: voxel bitset with set index (x, y, z) indicates that -+ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1]) -+ // is collidable. this is the fundamental principle of operation for the voxel collision operation -+ -+ // note: we should be offsetting coords, but we can also just subtract from source as well - which is -+ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that) -+ // note: for intersection, one we find the floor of the min we can use that as the start index -+ // for the next check as source max >= source min -+ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size, -+ // as this implies that coords[coords.length - 1] < source min -+ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max -+ -+ final int floor_min_x = Math.max( -+ 0, -+ findFloor(coords_x, (aabb.minX - off_x) + COLLISION_EPSILON, 0, size_x) -+ ); -+ if (floor_min_x >= size_x) { -+ // cannot intersect -+ return false; -+ } -+ -+ final int ceil_max_x = Math.min( -+ size_x, -+ findFloor(coords_x, (aabb.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1 -+ ); -+ if (floor_min_x >= ceil_max_x) { -+ // cannot intersect -+ return false; -+ } -+ -+ final int floor_min_y = Math.max( -+ 0, -+ findFloor(coords_y, (aabb.minY - off_y) + COLLISION_EPSILON, 0, size_y) -+ ); -+ if (floor_min_y >= size_y) { -+ // cannot intersect -+ return false; -+ } -+ -+ final int ceil_max_y = Math.min( -+ size_y, -+ findFloor(coords_y, (aabb.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1 -+ ); -+ if (floor_min_y >= ceil_max_y) { -+ // cannot intersect -+ return false; -+ } -+ -+ final int floor_min_z = Math.max( -+ 0, -+ findFloor(coords_z, (aabb.minZ - off_z) + COLLISION_EPSILON, 0, size_z) -+ ); -+ if (floor_min_z >= size_z) { -+ // cannot intersect -+ return false; -+ } -+ -+ final int ceil_max_z = Math.min( -+ size_z, -+ findFloor(coords_z, (aabb.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1 -+ ); -+ if (floor_min_z >= ceil_max_z) { -+ // cannot intersect -+ return false; -+ } -+ -+ final long[] bitset = cached_shape_data.voxelSet(); -+ -+ // check bitset to check if any shapes in range are full -+ -+ final int mul_x = size_y*size_z; -+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { -+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { -+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { -+ final int index = curr_z + curr_y*size_z + curr_x*mul_x; -+ // note: JLS states long shift operators ANDS shift by 63 -+ if ((bitset[index >>> 6] & (1L << index)) != 0L) { -+ return true; -+ } -+ } -+ } -+ } -+ -+ return false; -+ } -+ -+ // assume !target.isEmpty() && abs(source_move) >= COLLISION_EPSILON -+ public static double collideX(final VoxelShape target, final AABB source, final double source_move) { -+ final AABB single_aabb = target.getSingleAABBRepresentation(); -+ if (single_aabb != null) { -+ return collideX(single_aabb, source, source_move); -+ } -+ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true -+ -+ // offsets that should be applied to coords -+ final double off_x = target.offsetX(); -+ final double off_y = target.offsetY(); -+ final double off_z = target.offsetZ(); -+ -+ final double[] coords_x = target.rootCoordinatesX(); -+ final double[] coords_y = target.rootCoordinatesY(); -+ final double[] coords_z = target.rootCoordinatesZ(); -+ -+ final CachedShapeData cached_shape_data = target.getCachedVoxelData(); -+ -+ // note: size = coords.length - 1 -+ final int size_x = cached_shape_data.sizeX(); -+ final int size_y = cached_shape_data.sizeY(); -+ final int size_z = cached_shape_data.sizeZ(); -+ -+ // note: voxel bitset with set index (x, y, z) indicates that -+ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1]) -+ // is collidable. this is the fundamental principle of operation for the voxel collision operation -+ -+ -+ // note: we should be offsetting coords, but we can also just subtract from source as well - which is -+ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that) -+ // note: for intersection, one we find the floor of the min we can use that as the start index -+ // for the next check as source max >= source min -+ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size, -+ // as this implies that coords[coords.length - 1] < source min -+ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max -+ -+ final int floor_min_y = Math.max( -+ 0, -+ findFloor(coords_y, (source.minY - off_y) + COLLISION_EPSILON, 0, size_y) -+ ); -+ if (floor_min_y >= size_y) { -+ // cannot intersect -+ return source_move; -+ } -+ -+ final int ceil_max_y = Math.min( -+ size_y, -+ findFloor(coords_y, (source.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1 -+ ); -+ if (floor_min_y >= ceil_max_y) { -+ // cannot intersect -+ return source_move; -+ } -+ -+ final int floor_min_z = Math.max( -+ 0, -+ findFloor(coords_z, (source.minZ - off_z) + COLLISION_EPSILON, 0, size_z) -+ ); -+ if (floor_min_z >= size_z) { -+ // cannot intersect -+ return source_move; -+ } -+ -+ final int ceil_max_z = Math.min( -+ size_z, -+ findFloor(coords_z, (source.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1 -+ ); -+ if (floor_min_z >= ceil_max_z) { -+ // cannot intersect -+ return source_move; -+ } -+ -+ // index = z + y*size_z + x*(size_z*size_y) -+ -+ final long[] bitset = cached_shape_data.voxelSet(); -+ -+ if (source_move > 0.0) { -+ final double source_max = source.maxX - off_x; -+ final int ceil_max_x = findFloor( -+ coords_x, source_max - COLLISION_EPSILON, 0, size_x -+ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max -+ -+ // note: only the order of the first loop matters -+ -+ // note: we cannot collide with the face at index size on the collision axis for forward movement -+ -+ final int mul_x = size_y*size_z; -+ for (int curr_x = ceil_max_x; curr_x < size_x; ++curr_x) { -+ double max_dist = coords_x[curr_x] - source_max; -+ if (max_dist >= source_move) { -+ // if we reach here, then we will never have a case where -+ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1] -+ // thus, we can return immediately -+ -+ // this optimization is important since this loop is bounded by size, and _not_ by -+ // a calculated max index based off of source_move - so it would be possible to check -+ // the whole intersected shape for collisions when we didn't need to! -+ return source_move; -+ } -+ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON -+ max_dist = Math.min(max_dist, source_move); -+ } -+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { -+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { -+ final int index = curr_z + curr_y*size_z + curr_x*mul_x; -+ // note: JLS states long shift operators ANDS shift by 63 -+ if ((bitset[index >>> 6] & (1L << index)) != 0L) { -+ return max_dist; -+ } -+ } -+ } -+ } -+ -+ return source_move; -+ } else { -+ final double source_min = source.minX - off_x; -+ final int floor_min_x = findFloor( -+ coords_x, source_min + COLLISION_EPSILON, 0, size_x -+ ); -+ -+ // note: only the order of the first loop matters -+ -+ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement -+ -+ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the -+ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1] -+ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid -+ final int mul_x = size_y*size_z; -+ for (int curr_x = floor_min_x - 1; curr_x >= 0; --curr_x) { -+ double max_dist = coords_x[curr_x + 1] - source_min; -+ if (max_dist <= source_move) { -+ // if we reach here, then we will never have a case where -+ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1] -+ // thus, we can return immediately -+ -+ // this optimization is important since this loop is possibly bounded by size, and _not_ by -+ // a calculated max index based off of source_move - so it would be possible to check -+ // the whole intersected shape for collisions when we didn't need to! -+ return source_move; -+ } -+ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON -+ max_dist = Math.max(max_dist, source_move); -+ } -+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { -+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { -+ final int index = curr_z + curr_y*size_z + curr_x*mul_x; -+ // note: JLS states long shift operators ANDS shift by 63 -+ if ((bitset[index >>> 6] & (1L << index)) != 0L) { -+ return max_dist; -+ } -+ } -+ } -+ } -+ -+ return source_move; -+ } -+ } -+ -+ public static double collideY(final VoxelShape target, final AABB source, final double source_move) { -+ final AABB single_aabb = target.getSingleAABBRepresentation(); -+ if (single_aabb != null) { -+ return collideY(single_aabb, source, source_move); -+ } -+ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true -+ -+ // offsets that should be applied to coords -+ final double off_x = target.offsetX(); -+ final double off_y = target.offsetY(); -+ final double off_z = target.offsetZ(); -+ -+ final double[] coords_x = target.rootCoordinatesX(); -+ final double[] coords_y = target.rootCoordinatesY(); -+ final double[] coords_z = target.rootCoordinatesZ(); -+ -+ final CachedShapeData cached_shape_data = target.getCachedVoxelData(); -+ -+ // note: size = coords.length - 1 -+ final int size_x = cached_shape_data.sizeX(); -+ final int size_y = cached_shape_data.sizeY(); -+ final int size_z = cached_shape_data.sizeZ(); -+ -+ // note: voxel bitset with set index (x, y, z) indicates that -+ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1]) -+ // is collidable. this is the fundamental principle of operation for the voxel collision operation -+ -+ -+ // note: we should be offsetting coords, but we can also just subtract from source as well - which is -+ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that) -+ // note: for intersection, one we find the floor of the min we can use that as the start index -+ // for the next check as source max >= source min -+ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size, -+ // as this implies that coords[coords.length - 1] < source min -+ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max -+ -+ final int floor_min_x = Math.max( -+ 0, -+ findFloor(coords_x, (source.minX - off_x) + COLLISION_EPSILON, 0, size_x) -+ ); -+ if (floor_min_x >= size_x) { -+ // cannot intersect -+ return source_move; -+ } -+ -+ final int ceil_max_x = Math.min( -+ size_x, -+ findFloor(coords_x, (source.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1 -+ ); -+ if (floor_min_x >= ceil_max_x) { -+ // cannot intersect -+ return source_move; -+ } -+ -+ final int floor_min_z = Math.max( -+ 0, -+ findFloor(coords_z, (source.minZ - off_z) + COLLISION_EPSILON, 0, size_z) -+ ); -+ if (floor_min_z >= size_z) { -+ // cannot intersect -+ return source_move; -+ } -+ -+ final int ceil_max_z = Math.min( -+ size_z, -+ findFloor(coords_z, (source.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1 -+ ); -+ if (floor_min_z >= ceil_max_z) { -+ // cannot intersect -+ return source_move; -+ } -+ -+ // index = z + y*size_z + x*(size_z*size_y) -+ -+ final long[] bitset = cached_shape_data.voxelSet(); -+ -+ if (source_move > 0.0) { -+ final double source_max = source.maxY - off_y; -+ final int ceil_max_y = findFloor( -+ coords_y, source_max - COLLISION_EPSILON, 0, size_y -+ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max -+ -+ // note: only the order of the first loop matters -+ -+ // note: we cannot collide with the face at index size on the collision axis for forward movement -+ -+ final int mul_x = size_y*size_z; -+ for (int curr_y = ceil_max_y; curr_y < size_y; ++curr_y) { -+ double max_dist = coords_y[curr_y] - source_max; -+ if (max_dist >= source_move) { -+ // if we reach here, then we will never have a case where -+ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1] -+ // thus, we can return immediately -+ -+ // this optimization is important since this loop is bounded by size, and _not_ by -+ // a calculated max index based off of source_move - so it would be possible to check -+ // the whole intersected shape for collisions when we didn't need to! -+ return source_move; -+ } -+ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON -+ max_dist = Math.min(max_dist, source_move); -+ } -+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { -+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { -+ final int index = curr_z + curr_y*size_z + curr_x*mul_x; -+ // note: JLS states long shift operators ANDS shift by 63 -+ if ((bitset[index >>> 6] & (1L << index)) != 0L) { -+ return max_dist; -+ } -+ } -+ } -+ } -+ -+ return source_move; -+ } else { -+ final double source_min = source.minY - off_y; -+ final int floor_min_y = findFloor( -+ coords_y, source_min + COLLISION_EPSILON, 0, size_y -+ ); -+ -+ // note: only the order of the first loop matters -+ -+ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement -+ -+ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the -+ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1] -+ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid -+ final int mul_x = size_y*size_z; -+ for (int curr_y = floor_min_y - 1; curr_y >= 0; --curr_y) { -+ double max_dist = coords_y[curr_y + 1] - source_min; -+ if (max_dist <= source_move) { -+ // if we reach here, then we will never have a case where -+ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1] -+ // thus, we can return immediately -+ -+ // this optimization is important since this loop is possibly bounded by size, and _not_ by -+ // a calculated max index based off of source_move - so it would be possible to check -+ // the whole intersected shape for collisions when we didn't need to! -+ return source_move; -+ } -+ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON -+ max_dist = Math.max(max_dist, source_move); -+ } -+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { -+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) { -+ final int index = curr_z + curr_y*size_z + curr_x*mul_x; -+ // note: JLS states long shift operators ANDS shift by 63 -+ if ((bitset[index >>> 6] & (1L << index)) != 0L) { -+ return max_dist; -+ } -+ } -+ } -+ } -+ -+ return source_move; -+ } -+ } -+ -+ public static double collideZ(final VoxelShape target, final AABB source, final double source_move) { -+ final AABB single_aabb = target.getSingleAABBRepresentation(); -+ if (single_aabb != null) { -+ return collideZ(single_aabb, source, source_move); -+ } -+ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true -+ -+ // offsets that should be applied to coords -+ final double off_x = target.offsetX(); -+ final double off_y = target.offsetY(); -+ final double off_z = target.offsetZ(); -+ -+ final double[] coords_x = target.rootCoordinatesX(); -+ final double[] coords_y = target.rootCoordinatesY(); -+ final double[] coords_z = target.rootCoordinatesZ(); -+ -+ final CachedShapeData cached_shape_data = target.getCachedVoxelData(); -+ -+ // note: size = coords.length - 1 -+ final int size_x = cached_shape_data.sizeX(); -+ final int size_y = cached_shape_data.sizeY(); -+ final int size_z = cached_shape_data.sizeZ(); -+ -+ // note: voxel bitset with set index (x, y, z) indicates that -+ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1]) -+ // is collidable. this is the fundamental principle of operation for the voxel collision operation -+ -+ -+ // note: we should be offsetting coords, but we can also just subtract from source as well - which is -+ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that) -+ // note: for intersection, one we find the floor of the min we can use that as the start index -+ // for the next check as source max >= source min -+ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size, -+ // as this implies that coords[coords.length - 1] < source min -+ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max -+ -+ final int floor_min_x = Math.max( -+ 0, -+ findFloor(coords_x, (source.minX - off_x) + COLLISION_EPSILON, 0, size_x) -+ ); -+ if (floor_min_x >= size_x) { -+ // cannot intersect -+ return source_move; -+ } -+ -+ final int ceil_max_x = Math.min( -+ size_x, -+ findFloor(coords_x, (source.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1 -+ ); -+ if (floor_min_x >= ceil_max_x) { -+ // cannot intersect -+ return source_move; -+ } -+ -+ final int floor_min_y = Math.max( -+ 0, -+ findFloor(coords_y, (source.minY - off_y) + COLLISION_EPSILON, 0, size_y) -+ ); -+ if (floor_min_y >= size_y) { -+ // cannot intersect -+ return source_move; -+ } -+ -+ final int ceil_max_y = Math.min( -+ size_y, -+ findFloor(coords_y, (source.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1 -+ ); -+ if (floor_min_y >= ceil_max_y) { -+ // cannot intersect -+ return source_move; -+ } -+ -+ // index = z + y*size_z + x*(size_z*size_y) -+ -+ final long[] bitset = cached_shape_data.voxelSet(); -+ -+ if (source_move > 0.0) { -+ final double source_max = source.maxZ - off_z; -+ final int ceil_max_z = findFloor( -+ coords_z, source_max - COLLISION_EPSILON, 0, size_z -+ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max -+ -+ // note: only the order of the first loop matters -+ -+ // note: we cannot collide with the face at index size on the collision axis for forward movement -+ -+ final int mul_x = size_y*size_z; -+ for (int curr_z = ceil_max_z; curr_z < size_z; ++curr_z) { -+ double max_dist = coords_z[curr_z] - source_max; -+ if (max_dist >= source_move) { -+ // if we reach here, then we will never have a case where -+ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1] -+ // thus, we can return immediately -+ -+ // this optimization is important since this loop is bounded by size, and _not_ by -+ // a calculated max index based off of source_move - so it would be possible to check -+ // the whole intersected shape for collisions when we didn't need to! -+ return source_move; -+ } -+ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON -+ max_dist = Math.min(max_dist, source_move); -+ } -+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { -+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { -+ final int index = curr_z + curr_y*size_z + curr_x*mul_x; -+ // note: JLS states long shift operators ANDS shift by 63 -+ if ((bitset[index >>> 6] & (1L << index)) != 0L) { -+ return max_dist; -+ } -+ } -+ } -+ } -+ -+ return source_move; -+ } else { -+ final double source_min = source.minZ - off_z; -+ final int floor_min_z = findFloor( -+ coords_z, source_min + COLLISION_EPSILON, 0, size_z -+ ); -+ -+ // note: only the order of the first loop matters -+ -+ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement -+ -+ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the -+ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1] -+ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid -+ final int mul_x = size_y*size_z; -+ for (int curr_z = floor_min_z - 1; curr_z >= 0; --curr_z) { -+ double max_dist = coords_z[curr_z + 1] - source_min; -+ if (max_dist <= source_move) { -+ // if we reach here, then we will never have a case where -+ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1] -+ // thus, we can return immediately -+ -+ // this optimization is important since this loop is possibly bounded by size, and _not_ by -+ // a calculated max index based off of source_move - so it would be possible to check -+ // the whole intersected shape for collisions when we didn't need to! -+ return source_move; -+ } -+ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON -+ max_dist = Math.max(max_dist, source_move); -+ } -+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) { -+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) { -+ final int index = curr_z + curr_y*size_z + curr_x*mul_x; -+ // note: JLS states long shift operators ANDS shift by 63 -+ if ((bitset[index >>> 6] & (1L << index)) != 0L) { -+ return max_dist; -+ } -+ } -+ } -+ } -+ -+ return source_move; -+ } -+ } -+ -+ // does not use epsilon -+ public static boolean strictlyContains(final VoxelShape voxel, final Vec3 point) { -+ return strictlyContains(voxel, point.x, point.y, point.z); -+ } -+ -+ // does not use epsilon -+ public static boolean strictlyContains(final VoxelShape voxel, double x, double y, double z) { -+ final AABB single_aabb = voxel.getSingleAABBRepresentation(); -+ if (single_aabb != null) { -+ return single_aabb.contains(x, y, z); -+ } -+ -+ if (voxel.isEmpty()) { -+ // bitset is clear, no point in searching -+ return false; -+ } -+ -+ // offset input -+ x -= voxel.offsetX(); -+ y -= voxel.offsetY(); -+ z -= voxel.offsetZ(); -+ -+ final double[] coords_x = voxel.rootCoordinatesX(); -+ final double[] coords_y = voxel.rootCoordinatesY(); -+ final double[] coords_z = voxel.rootCoordinatesZ(); -+ -+ final CachedShapeData cached_shape_data = voxel.getCachedVoxelData(); -+ -+ // note: size = coords.length - 1 -+ final int size_x = cached_shape_data.sizeX(); -+ final int size_y = cached_shape_data.sizeY(); -+ final int size_z = cached_shape_data.sizeZ(); -+ -+ // note: should mirror AABB#contains, which is that for any point X that X >= min and X < max. -+ // specifically, it cannot collide on the max bounds of the shape -+ -+ final int index_x = findFloor(coords_x, x, 0, size_x); -+ if (index_x < 0 || index_x >= size_x) { -+ return false; -+ } -+ -+ final int index_y = findFloor(coords_y, y, 0, size_y); -+ if (index_y < 0 || index_y >= size_y) { -+ return false; -+ } -+ -+ final int index_z = findFloor(coords_z, z, 0, size_z); -+ if (index_z < 0 || index_z >= size_z) { -+ return false; -+ } -+ -+ // index = z + y*size_z + x*(size_z*size_y) -+ -+ final int index = index_z + index_y*size_z + index_x*(size_z*size_y); -+ -+ final long[] bitset = cached_shape_data.voxelSet(); -+ -+ return (bitset[index >>> 6] & (1L << index)) != 0L; -+ } -+ -+ private static int makeBitset(final boolean ft, final boolean tf, final boolean tt) { -+ // idx ff -> 0 -+ // idx ft -> 1 -+ // idx tf -> 2 -+ // idx tt -> 3 -+ return ((ft ? 1 : 0) << 1) | ((tf ? 1 : 0) << 2) | ((tt ? 1 : 0) << 3); -+ } -+ -+ private static BitSetDiscreteVoxelShape merge(final CachedShapeData shapeDataFirst, final CachedShapeData shapeDataSecond, -+ final MergedVoxelCoordinateList mergedX, final MergedVoxelCoordinateList mergedY, -+ final MergedVoxelCoordinateList mergedZ, -+ final int booleanOp) { -+ final int sizeX = mergedX.voxels; -+ final int sizeY = mergedY.voxels; -+ final int sizeZ = mergedZ.voxels; -+ -+ final long[] s1Voxels = shapeDataFirst.voxelSet(); -+ final long[] s2Voxels = shapeDataSecond.voxelSet(); -+ -+ final int s1Mul1 = shapeDataFirst.sizeZ(); -+ final int s1Mul2 = s1Mul1 * shapeDataFirst.sizeY(); -+ -+ final int s2Mul1 = shapeDataSecond.sizeZ(); -+ final int s2Mul2 = s2Mul1 * shapeDataSecond.sizeY(); -+ -+ // note: indices may contain -1, but nothing > size -+ final BitSetDiscreteVoxelShape ret = new BitSetDiscreteVoxelShape(sizeX, sizeY, sizeZ); -+ -+ boolean empty = true; -+ -+ int mergedIdx = 0; -+ for (int idxX = 0; idxX < sizeX; ++idxX) { -+ final int s1x = mergedX.firstIndices[idxX]; -+ final int s2x = mergedX.secondIndices[idxX]; -+ boolean setX = false; -+ for (int idxY = 0; idxY < sizeY; ++idxY) { -+ final int s1y = mergedY.firstIndices[idxY]; -+ final int s2y = mergedY.secondIndices[idxY]; -+ boolean setY = false; -+ for (int idxZ = 0; idxZ < sizeZ; ++idxZ) { -+ final int s1z = mergedZ.firstIndices[idxZ]; -+ final int s2z = mergedZ.secondIndices[idxZ]; -+ -+ int idx; -+ -+ final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx) & 1L); -+ final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx) & 1L); -+ -+ // idx ff -> 0 -+ // idx ft -> 1 -+ // idx tf -> 2 -+ // idx tt -> 3 -+ -+ final boolean res = (booleanOp & (1 << (isS2Full | (isS1Full << 1)))) != 0; -+ setY |= res; -+ setX |= res; -+ -+ if (res) { -+ empty = false; -+ // inline and optimize fill operation -+ ret.zMin = Math.min(ret.zMin, idxZ); -+ ret.zMax = Math.max(ret.zMax, idxZ + 1); -+ ret.storage.set(mergedIdx); -+ } -+ -+ ++mergedIdx; -+ } -+ if (setY) { -+ ret.yMin = Math.min(ret.yMin, idxY); -+ ret.yMax = Math.max(ret.yMax, idxY + 1); -+ } -+ } -+ if (setX) { -+ ret.xMin = Math.min(ret.xMin, idxX); -+ ret.xMax = Math.max(ret.xMax, idxX + 1); -+ } -+ } -+ -+ return empty ? null : ret; -+ } -+ -+ private static boolean isMergeEmpty(final CachedShapeData shapeDataFirst, final CachedShapeData shapeDataSecond, -+ final MergedVoxelCoordinateList mergedX, final MergedVoxelCoordinateList mergedY, -+ final MergedVoxelCoordinateList mergedZ, -+ final int booleanOp) { -+ final int sizeX = mergedX.voxels; -+ final int sizeY = mergedY.voxels; -+ final int sizeZ = mergedZ.voxels; -+ -+ final long[] s1Voxels = shapeDataFirst.voxelSet(); -+ final long[] s2Voxels = shapeDataSecond.voxelSet(); -+ -+ final int s1Mul1 = shapeDataFirst.sizeZ(); -+ final int s1Mul2 = s1Mul1 * shapeDataFirst.sizeY(); -+ -+ final int s2Mul1 = shapeDataSecond.sizeZ(); -+ final int s2Mul2 = s2Mul1 * shapeDataSecond.sizeY(); -+ -+ // note: indices may contain -1, but nothing > size -+ for (int idxX = 0; idxX < sizeX; ++idxX) { -+ final int s1x = mergedX.firstIndices[idxX]; -+ final int s2x = mergedX.secondIndices[idxX]; -+ for (int idxY = 0; idxY < sizeY; ++idxY) { -+ final int s1y = mergedY.firstIndices[idxY]; -+ final int s2y = mergedY.secondIndices[idxY]; -+ for (int idxZ = 0; idxZ < sizeZ; ++idxZ) { -+ final int s1z = mergedZ.firstIndices[idxZ]; -+ final int s2z = mergedZ.secondIndices[idxZ]; -+ -+ int idx; -+ -+ final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx) & 1L); -+ final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx) & 1L); -+ -+ // idx ff -> 0 -+ // idx ft -> 1 -+ // idx tf -> 2 -+ // idx tt -> 3 -+ -+ final boolean res = (booleanOp & (1 << (isS2Full | (isS1Full << 1)))) != 0; -+ -+ if (res) { -+ return false; -+ } -+ } -+ } -+ } -+ -+ return true; -+ } -+ -+ public static VoxelShape joinOptimized(final VoxelShape first, final VoxelShape second, final BooleanOp operator) { -+ return joinUnoptimized(first, second, operator).optimize(); -+ } -+ -+ public static VoxelShape joinUnoptimized(final VoxelShape first, final VoxelShape second, final BooleanOp operator) { -+ final boolean ff = operator.apply(false, false); -+ if (ff) { -+ // technically, should be an infinite box but that's clearly an error -+ throw new UnsupportedOperationException("Ambiguous operator: (false, false) -> true"); -+ } -+ -+ final boolean tt = operator.apply(true, true); -+ -+ if (first == second) { -+ return tt ? first : Shapes.empty(); -+ } -+ -+ final boolean ft = operator.apply(false, true); -+ final boolean tf = operator.apply(true, false); -+ -+ if (first.isEmpty()) { -+ return ft ? second : Shapes.empty(); -+ } -+ if (second.isEmpty()) { -+ return tf ? first : Shapes.empty(); -+ } -+ -+ if (!tt) { -+ // try to check for no intersection, since tt = false -+ final AABB aabbF = first.getSingleAABBRepresentation(); -+ final AABB aabbS = second.getSingleAABBRepresentation(); -+ -+ final boolean intersect; -+ -+ final boolean hasAABBF = aabbF != null; -+ final boolean hasAABBS = aabbS != null; -+ if (hasAABBF | hasAABBS) { -+ if (hasAABBF & hasAABBS) { -+ intersect = voxelShapeIntersect(aabbF, aabbS); -+ } else if (hasAABBF) { -+ intersect = voxelShapeIntersectNoEmpty(second, aabbF); -+ } else { -+ intersect = voxelShapeIntersectNoEmpty(first, aabbS); -+ } -+ } else { -+ // expect cached bounds -+ intersect = voxelShapeIntersect(first.bounds(), second.bounds()); -+ } -+ -+ if (!intersect) { -+ if (!tf & !ft) { -+ return Shapes.empty(); -+ } -+ if (!tf | !ft) { -+ return tf ? first : second; -+ } -+ } -+ } -+ -+ final MergedVoxelCoordinateList mergedX = MergedVoxelCoordinateList.merge( -+ first.rootCoordinatesX(), first.offsetX(), -+ second.rootCoordinatesX(), second.offsetX(), -+ ft, tf -+ ); -+ if (mergedX == MergedVoxelCoordinateList.EMPTY) { -+ return Shapes.empty(); -+ } -+ final MergedVoxelCoordinateList mergedY = MergedVoxelCoordinateList.merge( -+ first.rootCoordinatesY(), first.offsetY(), -+ second.rootCoordinatesY(), second.offsetY(), -+ ft, tf -+ ); -+ if (mergedY == MergedVoxelCoordinateList.EMPTY) { -+ return Shapes.empty(); -+ } -+ final MergedVoxelCoordinateList mergedZ = MergedVoxelCoordinateList.merge( -+ first.rootCoordinatesZ(), first.offsetZ(), -+ second.rootCoordinatesZ(), second.offsetZ(), -+ ft, tf -+ ); -+ if (mergedZ == MergedVoxelCoordinateList.EMPTY) { -+ return Shapes.empty(); -+ } -+ -+ final CachedShapeData shapeDataFirst = first.getCachedVoxelData(); -+ final CachedShapeData shapeDataSecond = second.getCachedVoxelData(); -+ -+ final BitSetDiscreteVoxelShape mergedShape = merge( -+ shapeDataFirst, shapeDataSecond, -+ mergedX, mergedY, mergedZ, -+ makeBitset(ft, tf, tt) -+ ); -+ -+ if (mergedShape == null) { -+ return Shapes.empty(); -+ } -+ -+ return new ArrayVoxelShape( -+ mergedShape, mergedX.wrapCoords(), mergedY.wrapCoords(), mergedZ.wrapCoords() -+ ); -+ } -+ -+ public static boolean isJoinNonEmpty(final VoxelShape first, final VoxelShape second, final BooleanOp operator) { -+ final boolean ff = operator.apply(false, false); -+ if (ff) { -+ // technically, should be an infinite box but that's clearly an error -+ throw new UnsupportedOperationException("Ambiguous operator: (false, false) -> true"); -+ } -+ final boolean firstEmpty = first.isEmpty(); -+ final boolean secondEmpty = second.isEmpty(); -+ if (firstEmpty | secondEmpty) { -+ return operator.apply(!firstEmpty, !secondEmpty); -+ } -+ -+ final boolean tt = operator.apply(true, true); -+ -+ if (first == second) { -+ return tt; -+ } -+ -+ final boolean ft = operator.apply(false, true); -+ final boolean tf = operator.apply(true, false); -+ -+ // try to check intersection -+ final AABB aabbF = first.getSingleAABBRepresentation(); -+ final AABB aabbS = second.getSingleAABBRepresentation(); -+ -+ final boolean intersect; -+ -+ final boolean hasAABBF = aabbF != null; -+ final boolean hasAABBS = aabbS != null; -+ if (hasAABBF | hasAABBS) { -+ if (hasAABBF & hasAABBS) { -+ intersect = voxelShapeIntersect(aabbF, aabbS); -+ } else if (hasAABBF) { -+ intersect = voxelShapeIntersectNoEmpty(second, aabbF); -+ } else { -+ // hasAABBS -> true -+ intersect = voxelShapeIntersectNoEmpty(first, aabbS); -+ } -+ -+ if (!intersect) { -+ // is only non-empty if we take from first or second, as there is no overlap AND both shapes are non-empty -+ return tf | ft; -+ } else if (tt) { -+ // intersect = true && tt = true -> non-empty merged shape -+ return true; -+ } -+ } else { -+ // expect cached bounds -+ intersect = voxelShapeIntersect(first.bounds(), second.bounds()); -+ if (!intersect) { -+ // is only non-empty if we take from first or second, as there is no intersection -+ return tf | ft; -+ } -+ } -+ -+ final MergedVoxelCoordinateList mergedX = MergedVoxelCoordinateList.merge( -+ first.rootCoordinatesX(), first.offsetX(), -+ second.rootCoordinatesX(), second.offsetX(), -+ ft, tf -+ ); -+ if (mergedX == MergedVoxelCoordinateList.EMPTY) { -+ return false; -+ } -+ final MergedVoxelCoordinateList mergedY = MergedVoxelCoordinateList.merge( -+ first.rootCoordinatesY(), first.offsetY(), -+ second.rootCoordinatesY(), second.offsetY(), -+ ft, tf -+ ); -+ if (mergedY == MergedVoxelCoordinateList.EMPTY) { -+ return false; -+ } -+ final MergedVoxelCoordinateList mergedZ = MergedVoxelCoordinateList.merge( -+ first.rootCoordinatesZ(), first.offsetZ(), -+ second.rootCoordinatesZ(), second.offsetZ(), -+ ft, tf -+ ); -+ if (mergedZ == MergedVoxelCoordinateList.EMPTY) { -+ return false; -+ } -+ -+ final CachedShapeData shapeDataFirst = first.getCachedVoxelData(); -+ final CachedShapeData shapeDataSecond = second.getCachedVoxelData(); -+ -+ return !isMergeEmpty( -+ shapeDataFirst, shapeDataSecond, -+ mergedX, mergedY, mergedZ, -+ makeBitset(ft, tf, tt) -+ ); -+ } -+ -+ private static final class MergedVoxelCoordinateList { -+ -+ private static final int[][] SIMPLE_INDICES_CACHE = new int[64][]; -+ static { -+ for (int i = 0; i < SIMPLE_INDICES_CACHE.length; ++i) { -+ SIMPLE_INDICES_CACHE[i] = getIndices(i); -+ } -+ } -+ -+ private static final MergedVoxelCoordinateList EMPTY = new MergedVoxelCoordinateList( -+ new double[] { 0.0 }, 0.0, new int[0], new int[0], 0 -+ ); -+ -+ private static int[] getIndices(final int length) { -+ final int[] ret = new int[length]; -+ -+ for (int i = 1; i < length; ++i) { -+ ret[i] = i; -+ } -+ -+ return ret; -+ } -+ -+ // indices above voxel size are always set to -1 -+ public final double[] coordinates; -+ public final double coordinateOffset; -+ public final int[] firstIndices; -+ public final int[] secondIndices; -+ public final int voxels; -+ -+ private MergedVoxelCoordinateList(final double[] coordinates, final double coordinateOffset, -+ final int[] firstIndices, final int[] secondIndices, final int voxels) { -+ this.coordinates = coordinates; -+ this.coordinateOffset = coordinateOffset; -+ this.firstIndices = firstIndices; -+ this.secondIndices = secondIndices; -+ this.voxels = voxels; -+ } -+ -+ public DoubleList wrapCoords() { -+ if (this.coordinateOffset == 0.0) { -+ return DoubleArrayList.wrap(this.coordinates, this.voxels + 1); -+ } -+ return new OffsetDoubleList(DoubleArrayList.wrap(this.coordinates, this.voxels + 1), this.coordinateOffset); -+ } -+ -+ // assume coordinates.length > 1 -+ public static MergedVoxelCoordinateList getForSingle(final double[] coordinates, final double offset) { -+ final int voxels = coordinates.length - 1; -+ final int[] indices = voxels < SIMPLE_INDICES_CACHE.length ? SIMPLE_INDICES_CACHE[voxels] : getIndices(voxels); -+ -+ return new MergedVoxelCoordinateList(coordinates, offset, indices, indices, voxels); -+ } -+ -+ // assume coordinates.length > 1 -+ public static MergedVoxelCoordinateList merge(final double[] firstCoordinates, final double firstOffset, -+ final double[] secondCoordinates, final double secondOffset, -+ final boolean ft, final boolean tf) { -+ if (firstCoordinates == secondCoordinates && firstOffset == secondOffset) { -+ return getForSingle(firstCoordinates, firstOffset); -+ } -+ -+ final int firstCount = firstCoordinates.length; -+ final int secondCount = secondCoordinates.length; -+ -+ final int voxelsFirst = firstCount - 1; -+ final int voxelsSecond = secondCount - 1; -+ -+ final int maxCount = firstCount + secondCount; -+ -+ final double[] coordinates = new double[maxCount]; -+ final int[] firstIndices = new int[maxCount]; -+ final int[] secondIndices = new int[maxCount]; -+ -+ final boolean notTF = !tf; -+ final boolean notFT = !ft; -+ -+ int firstIndex = 0; -+ int secondIndex = 0; -+ int resultSize = 0; -+ -+ // note: operations on NaN are false -+ double last = Double.NaN; -+ -+ for (;;) { -+ final boolean noneLeftFirst = firstIndex >= firstCount; -+ final boolean noneLeftSecond = secondIndex >= secondCount; -+ -+ if ((noneLeftFirst & noneLeftSecond) | (noneLeftSecond & notTF) | (noneLeftFirst & notFT)) { -+ break; -+ } -+ -+ final boolean firstZero = firstIndex == 0; -+ final boolean secondZero = secondIndex == 0; -+ -+ final double select; -+ -+ if (noneLeftFirst) { -+ // noneLeftSecond -> false -+ // notFT -> false -+ select = secondCoordinates[secondIndex] + secondOffset; -+ ++secondIndex; -+ } else if (noneLeftSecond) { -+ // noneLeftFirst -> false -+ // notTF -> false -+ select = firstCoordinates[firstIndex] + firstOffset; -+ ++firstIndex; -+ } else { -+ // noneLeftFirst | noneLeftSecond -> false -+ // notTF -> ?? -+ // notFT -> ?? -+ final boolean breakFirst = notTF & secondZero; -+ final boolean breakSecond = notFT & firstZero; -+ -+ final double first = firstCoordinates[firstIndex] + firstOffset; -+ final double second = secondCoordinates[secondIndex] + secondOffset; -+ final boolean useFirst = first < (second + COLLISION_EPSILON); -+ final boolean cont = (useFirst & breakFirst) | (!useFirst & breakSecond); -+ -+ select = useFirst ? first : second; -+ firstIndex += useFirst ? 1 : 0; -+ secondIndex += 1 ^ (useFirst ? 1 : 0); -+ -+ if (cont) { -+ continue; -+ } -+ } -+ -+ int prevFirst = firstIndex - 1; -+ prevFirst = prevFirst >= voxelsFirst ? -1 : prevFirst; -+ int prevSecond = secondIndex - 1; -+ prevSecond = prevSecond >= voxelsSecond ? -1 : prevSecond; -+ -+ if (last >= (select - COLLISION_EPSILON)) { -+ // note: any operations on NaN is false -+ firstIndices[resultSize - 1] = prevFirst; -+ secondIndices[resultSize - 1] = prevSecond; -+ } else { -+ firstIndices[resultSize] = prevFirst; -+ secondIndices[resultSize] = prevSecond; -+ coordinates[resultSize] = select; -+ -+ ++resultSize; -+ last = select; -+ } -+ } -+ -+ return resultSize <= 1 ? EMPTY : new MergedVoxelCoordinateList(coordinates, 0.0, firstIndices, secondIndices, resultSize - 1); -+ } -+ } -+ -+ public static AABB offsetX(final AABB box, final double dx) { -+ return new AABB(box.minX + dx, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false); -+ } -+ -+ public static AABB offsetY(final AABB box, final double dy) { -+ return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false); -+ } -+ -+ public static AABB offsetZ(final AABB box, final double dz) { -+ return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.maxZ + dz, false); -+ } -+ -+ public static AABB expandRight(final AABB box, final double dx) { // dx > 0.0 -+ return new AABB(box.minX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false); -+ } -+ -+ public static AABB expandLeft(final AABB box, final double dx) { // dx < 0.0 -+ return new AABB(box.minX - dx, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ); -+ } -+ -+ public static AABB expandUpwards(final AABB box, final double dy) { // dy > 0.0 -+ return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false); -+ } -+ -+ public static AABB expandDownwards(final AABB box, final double dy) { // dy < 0.0 -+ return new AABB(box.minX, box.minY - dy, box.minZ, box.maxX, box.maxY, box.maxZ, false); -+ } -+ -+ public static AABB expandForwards(final AABB box, final double dz) { // dz > 0.0 -+ return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ + dz, false); -+ } -+ -+ public static AABB expandBackwards(final AABB box, final double dz) { // dz < 0.0 -+ return new AABB(box.minX, box.minY, box.minZ - dz, box.maxX, box.maxY, box.maxZ, false); -+ } -+ -+ public static AABB cutRight(final AABB box, final double dx) { // dx > 0.0 -+ return new AABB(box.maxX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false); -+ } -+ -+ public static AABB cutLeft(final AABB box, final double dx) { // dx < 0.0 -+ return new AABB(box.minX + dx, box.minY, box.minZ, box.minX, box.maxY, box.maxZ, false); -+ } -+ -+ public static AABB cutUpwards(final AABB box, final double dy) { // dy > 0.0 -+ return new AABB(box.minX, box.maxY, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false); -+ } -+ -+ public static AABB cutDownwards(final AABB box, final double dy) { // dy < 0.0 -+ return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.minY, box.maxZ, false); -+ } -+ -+ public static AABB cutForwards(final AABB box, final double dz) { // dz > 0.0 -+ return new AABB(box.minX, box.minY, box.maxZ, box.maxX, box.maxY, box.maxZ + dz, false); -+ } -+ -+ public static AABB cutBackwards(final AABB box, final double dz) { // dz < 0.0 -+ return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.minZ, false); -+ } -+ -+ public static double performAABBCollisionsX(final AABB currentBoundingBox, double value, final List potentialCollisions) { -+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { -+ if (Math.abs(value) < COLLISION_EPSILON) { -+ return 0.0; -+ } -+ final AABB target = potentialCollisions.get(i); -+ value = collideX(target, currentBoundingBox, value); -+ } -+ -+ return value; -+ } -+ -+ public static double performAABBCollisionsY(final AABB currentBoundingBox, double value, final List potentialCollisions) { -+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { -+ if (Math.abs(value) < COLLISION_EPSILON) { -+ return 0.0; -+ } -+ final AABB target = potentialCollisions.get(i); -+ value = collideY(target, currentBoundingBox, value); -+ } -+ -+ return value; -+ } -+ -+ public static double performAABBCollisionsZ(final AABB currentBoundingBox, double value, final List potentialCollisions) { -+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { -+ if (Math.abs(value) < COLLISION_EPSILON) { -+ return 0.0; -+ } -+ final AABB target = potentialCollisions.get(i); -+ value = collideZ(target, currentBoundingBox, value); -+ } -+ -+ return value; -+ } -+ -+ public static double performVoxelCollisionsX(final AABB currentBoundingBox, double value, final List potentialCollisions) { -+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { -+ if (Math.abs(value) < COLLISION_EPSILON) { -+ return 0.0; -+ } -+ final VoxelShape target = potentialCollisions.get(i); -+ value = collideX(target, currentBoundingBox, value); -+ } -+ -+ return value; -+ } -+ -+ public static double performVoxelCollisionsY(final AABB currentBoundingBox, double value, final List potentialCollisions) { -+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { -+ if (Math.abs(value) < COLLISION_EPSILON) { -+ return 0.0; -+ } -+ final VoxelShape target = potentialCollisions.get(i); -+ value = collideY(target, currentBoundingBox, value); -+ } -+ -+ return value; -+ } -+ -+ public static double performVoxelCollisionsZ(final AABB currentBoundingBox, double value, final List potentialCollisions) { -+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { -+ if (Math.abs(value) < COLLISION_EPSILON) { -+ return 0.0; -+ } -+ final VoxelShape target = potentialCollisions.get(i); -+ value = collideZ(target, currentBoundingBox, value); -+ } -+ -+ return value; -+ } -+ -+ public static Vec3 performVoxelCollisions(final Vec3 moveVector, AABB axisalignedbb, final List potentialCollisions) { -+ double x = moveVector.x; -+ double y = moveVector.y; -+ double z = moveVector.z; -+ -+ if (y != 0.0) { -+ y = performVoxelCollisionsY(axisalignedbb, y, potentialCollisions); -+ if (y != 0.0) { -+ axisalignedbb = offsetY(axisalignedbb, y); -+ } -+ } -+ -+ final boolean xSmaller = Math.abs(x) < Math.abs(z); -+ -+ if (xSmaller && z != 0.0) { -+ z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions); -+ if (z != 0.0) { -+ axisalignedbb = offsetZ(axisalignedbb, z); -+ } -+ } -+ -+ if (x != 0.0) { -+ x = performVoxelCollisionsX(axisalignedbb, x, potentialCollisions); -+ if (!xSmaller && x != 0.0) { -+ axisalignedbb = offsetX(axisalignedbb, x); -+ } -+ } -+ -+ if (!xSmaller && z != 0.0) { -+ z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions); -+ } -+ -+ return new Vec3(x, y, z); -+ } -+ -+ public static Vec3 performAABBCollisions(final Vec3 moveVector, AABB axisalignedbb, final List potentialCollisions) { -+ double x = moveVector.x; -+ double y = moveVector.y; -+ double z = moveVector.z; -+ -+ if (y != 0.0) { -+ y = performAABBCollisionsY(axisalignedbb, y, potentialCollisions); -+ if (y != 0.0) { -+ axisalignedbb = offsetY(axisalignedbb, y); -+ } -+ } -+ -+ final boolean xSmaller = Math.abs(x) < Math.abs(z); -+ -+ if (xSmaller && z != 0.0) { -+ z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions); -+ if (z != 0.0) { -+ axisalignedbb = offsetZ(axisalignedbb, z); -+ } -+ } -+ -+ if (x != 0.0) { -+ x = performAABBCollisionsX(axisalignedbb, x, potentialCollisions); -+ if (!xSmaller && x != 0.0) { -+ axisalignedbb = offsetX(axisalignedbb, x); -+ } -+ } -+ -+ if (!xSmaller && z != 0.0) { -+ z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions); -+ } -+ -+ return new Vec3(x, y, z); -+ } -+ -+ public static Vec3 performCollisions(final Vec3 moveVector, AABB axisalignedbb, -+ final List voxels, -+ final List aabbs) { -+ if (voxels.isEmpty()) { -+ // fast track only AABBs -+ return performAABBCollisions(moveVector, axisalignedbb, aabbs); -+ } -+ -+ double x = moveVector.x; -+ double y = moveVector.y; -+ double z = moveVector.z; -+ -+ if (y != 0.0) { -+ y = performAABBCollisionsY(axisalignedbb, y, aabbs); -+ y = performVoxelCollisionsY(axisalignedbb, y, voxels); -+ if (y != 0.0) { -+ axisalignedbb = offsetY(axisalignedbb, y); -+ } -+ } -+ -+ final boolean xSmaller = Math.abs(x) < Math.abs(z); -+ -+ if (xSmaller && z != 0.0) { -+ z = performAABBCollisionsZ(axisalignedbb, z, aabbs); -+ z = performVoxelCollisionsZ(axisalignedbb, z, voxels); -+ if (z != 0.0) { -+ axisalignedbb = offsetZ(axisalignedbb, z); -+ } -+ } -+ -+ if (x != 0.0) { -+ x = performAABBCollisionsX(axisalignedbb, x, aabbs); -+ x = performVoxelCollisionsX(axisalignedbb, x, voxels); -+ if (!xSmaller && x != 0.0) { -+ axisalignedbb = offsetX(axisalignedbb, x); -+ } -+ } -+ -+ if (!xSmaller && z != 0.0) { -+ z = performAABBCollisionsZ(axisalignedbb, z, aabbs); -+ z = performVoxelCollisionsZ(axisalignedbb, z, voxels); -+ } -+ -+ return new Vec3(x, y, z); -+ } -+ -+ public static boolean isCollidingWithBorder(final WorldBorder worldborder, final AABB boundingBox) { -+ return isCollidingWithBorder(worldborder, boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ); -+ } -+ -+ public static boolean isCollidingWithBorder(final WorldBorder worldborder, final double boxMinX, final double boxMaxX, -+ final double boxMinZ, final double boxMaxZ) { -+ // border size is rounded like the collide voxel shape of the border -+ final double borderMinX = Math.floor(worldborder.getMinX()); // -X -+ final double borderMaxX = Math.ceil(worldborder.getMaxX()); // +X -+ -+ final double borderMinZ = Math.floor(worldborder.getMinZ()); // -Z -+ final double borderMaxZ = Math.ceil(worldborder.getMaxZ()); // +Z -+ -+ // inverted check for world border enclosing the specified box expanded by -EPSILON -+ return (borderMinX - boxMinX) > CollisionUtil.COLLISION_EPSILON || (borderMaxX - boxMaxX) < -CollisionUtil.COLLISION_EPSILON || -+ (borderMinZ - boxMinZ) > CollisionUtil.COLLISION_EPSILON || (borderMaxZ - boxMaxZ) < -CollisionUtil.COLLISION_EPSILON; -+ } -+ -+ /* Math.max/min specify that any NaN argument results in a NaN return, unlike these functions */ -+ private static double min(final double x, final double y) { -+ return x < y ? x : y; -+ } -+ -+ private static double max(final double x, final double y) { -+ return x > y ? x : y; -+ } -+ -+ public static final int COLLISION_FLAG_LOAD_CHUNKS = 1 << 0; -+ public static final int COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS = 1 << 1; -+ public static final int COLLISION_FLAG_CHECK_BORDER = 1 << 2; -+ public static final int COLLISION_FLAG_CHECK_ONLY = 1 << 3; -+ -+ public static boolean getCollisionsForBlocksOrWorldBorder(final Level world, final Entity entity, final AABB aabb, -+ final List intoVoxel, final List intoAABB, -+ final int collisionFlags, final BiPredicate predicate) { -+ final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0; -+ boolean ret = false; -+ -+ if ((collisionFlags & COLLISION_FLAG_CHECK_BORDER) != 0) { -+ final WorldBorder worldBorder = world.getWorldBorder(); -+ if (CollisionUtil.isCollidingWithBorder(worldBorder, aabb) && entity != null && worldBorder.isInsideCloseToBorder(entity, aabb)) { -+ if (checkOnly) { -+ return true; -+ } else { -+ final VoxelShape borderShape = worldBorder.getCollisionShape(); -+ intoVoxel.add(borderShape); -+ ret = true; -+ } -+ } -+ } -+ -+ final int minSection = world.minSection; -+ -+ final int minBlockX = Mth.floor(aabb.minX - COLLISION_EPSILON) - 1; -+ final int maxBlockX = Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1; -+ -+ final int minBlockY = Math.max((minSection << 4) - 1, Mth.floor(aabb.minY - COLLISION_EPSILON) - 1); -+ final int maxBlockY = Math.min((world.maxSection << 4) + 16, Mth.floor(aabb.maxY + COLLISION_EPSILON) + 1); -+ -+ final int minBlockZ = Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1; -+ final int maxBlockZ = Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1; -+ -+ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); -+ final CollisionContext collisionShape = new LazyEntityCollisionContext(entity); -+ -+ // special cases: -+ if (minBlockY > maxBlockY) { -+ // no point in checking -+ return ret; -+ } -+ -+ final int minChunkX = minBlockX >> 4; -+ final int maxChunkX = maxBlockX >> 4; -+ -+ final int minChunkY = minBlockY >> 4; -+ final int maxChunkY = maxBlockY >> 4; -+ -+ final int minChunkZ = minBlockZ >> 4; -+ final int maxChunkZ = maxBlockZ >> 4; -+ -+ final boolean loadChunks = (collisionFlags & COLLISION_FLAG_LOAD_CHUNKS) != 0; -+ final ServerChunkCache chunkSource = (ServerChunkCache)world.getChunkSource(); -+ -+ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { -+ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { -+ final ChunkAccess chunk = loadChunks ? chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, true) : chunkSource.getChunkAtIfLoadedImmediately(currChunkX, currChunkZ); -+ -+ if (chunk == null) { -+ if ((collisionFlags & COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS) != 0) { -+ if (checkOnly) { -+ return true; -+ } else { -+ intoAABB.add(getBoxForChunk(currChunkX, currChunkZ)); -+ ret = true; -+ } -+ } -+ continue; -+ } -+ -+ final LevelChunkSection[] sections = chunk.getSections(); -+ -+ // bound y -+ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) { -+ final int sectionIdx = currChunkY - minSection; -+ if (sectionIdx < 0 || sectionIdx >= sections.length) { -+ continue; -+ } -+ final LevelChunkSection section = sections[sectionIdx]; -+ if (section == null || section.hasOnlyAir()) { -+ // empty -+ continue; -+ } -+ -+ final boolean hasSpecial = section.getSpecialCollidingBlocks() != 0; -+ final int sectionAdjust = !hasSpecial ? 1 : 0; -+ -+ final PalettedContainer blocks = section.states; -+ -+ final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) + sectionAdjust : 0; -+ final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) - sectionAdjust : 15; -+ final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) + sectionAdjust : 0; -+ final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) - sectionAdjust : 15; -+ final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) + sectionAdjust : 0; -+ final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) - sectionAdjust : 15; -+ -+ for (int currY = minYIterate; currY <= maxYIterate; ++currY) { -+ final int blockY = currY | (currChunkY << 4); -+ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) { -+ final int blockZ = currZ | (currChunkZ << 4); -+ for (int currX = minXIterate; currX <= maxXIterate; ++currX) { -+ final int localBlockIndex = (currX) | (currZ << 4) | ((currY) << 8); -+ final int blockX = currX | (currChunkX << 4); -+ -+ final int edgeCount = hasSpecial ? ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) + -+ ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) + -+ ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0) : 0; -+ if (edgeCount == 3) { -+ continue; -+ } -+ -+ final BlockState blockData = blocks.get(localBlockIndex); -+ -+ if (blockData.emptyCollisionShape()) { -+ continue; -+ } -+ -+ if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON))) { -+ VoxelShape blockCollision = blockData.getConstantCollisionShape(); -+ -+ if (blockCollision == null) { -+ mutablePos.set(blockX, blockY, blockZ); -+ blockCollision = blockData.getCollisionShape(world, mutablePos, collisionShape); -+ } -+ -+ AABB singleAABB = blockCollision.getSingleAABBRepresentation(); -+ if (singleAABB != null) { -+ singleAABB = singleAABB.move((double)blockX, (double)blockY, (double)blockZ); -+ if (!voxelShapeIntersect(aabb, singleAABB)) { -+ continue; -+ } -+ -+ if (predicate != null) { -+ mutablePos.set(blockX, blockY, blockZ); -+ if (!predicate.test(blockData, mutablePos)) { -+ continue; -+ } -+ } -+ -+ if (checkOnly) { -+ return true; -+ } else { -+ ret = true; -+ intoAABB.add(singleAABB); -+ continue; -+ } -+ } -+ -+ if (blockCollision.isEmpty()) { -+ continue; -+ } -+ -+ final VoxelShape blockCollisionOffset = blockCollision.move((double)blockX, (double)blockY, (double)blockZ); -+ -+ if (!voxelShapeIntersectNoEmpty(blockCollisionOffset, aabb)) { -+ continue; -+ } -+ -+ if (predicate != null) { -+ mutablePos.set(blockX, blockY, blockZ); -+ if (!predicate.test(blockData, mutablePos)) { -+ continue; -+ } -+ } -+ -+ if (checkOnly) { -+ return true; -+ } else { -+ ret = true; -+ intoVoxel.add(blockCollisionOffset); -+ continue; -+ } -+ } -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ return ret; -+ } -+ -+ public static boolean getEntityHardCollisions(final CollisionGetter getter, final Entity entity, AABB aabb, -+ final List into, final int collisionFlags, final Predicate predicate) { -+ final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0; -+ if (!(getter instanceof EntityGetter entityGetter)) { -+ return false; -+ } -+ -+ boolean ret = false; -+ -+ // to comply with vanilla intersection rules, expand by -epsilon so that we only get stuff we definitely collide with. -+ // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems -+ // specifically with boat collisions. -+ aabb = aabb.inflate(-COLLISION_EPSILON, -COLLISION_EPSILON, -COLLISION_EPSILON); -+ final List entities; -+ if (entity != null && entity.hardCollides()) { -+ entities = entityGetter.getEntities(entity, aabb, predicate); -+ } else { -+ entities = entityGetter.getHardCollidingEntities(entity, aabb, predicate); -+ } -+ -+ for (int i = 0, len = entities.size(); i < len; ++i) { -+ final Entity otherEntity = entities.get(i); -+ -+ if (otherEntity.isSpectator()) { -+ continue; -+ } -+ -+ if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) { -+ if (checkOnly) { -+ return true; -+ } else { -+ into.add(otherEntity.getBoundingBox()); -+ ret = true; -+ } -+ } -+ } -+ -+ return ret; -+ } -+ -+ public static boolean getCollisions(final Level world, final Entity entity, final AABB aabb, -+ final List intoVoxel, final List intoAABB, final int collisionFlags, -+ final BiPredicate blockPredicate, -+ final Predicate entityPredicate) { -+ if ((collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0) { -+ return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate) -+ || getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate); -+ } else { -+ return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate) -+ | getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate); -+ } -+ } -+ -+ public static final class LazyEntityCollisionContext extends EntityCollisionContext { -+ -+ private CollisionContext delegate; -+ private boolean delegated; -+ -+ public LazyEntityCollisionContext(final Entity entity) { -+ super(false, 0.0, null, null, entity); -+ } -+ -+ public boolean isDelegated() { -+ final boolean delegated = this.delegated; -+ this.delegated = false; -+ return delegated; -+ } -+ -+ public CollisionContext getDelegate() { -+ this.delegated = true; -+ final Entity entity = this.getEntity(); -+ return this.delegate == null ? this.delegate = (entity == null ? CollisionContext.empty() : CollisionContext.of(entity)) : this.delegate; -+ } -+ -+ @Override -+ public boolean isDescending() { -+ return this.getDelegate().isDescending(); -+ } -+ -+ @Override -+ public boolean isAbove(final VoxelShape shape, final BlockPos pos, final boolean defaultValue) { -+ return this.getDelegate().isAbove(shape, pos, defaultValue); -+ } -+ -+ @Override -+ public boolean isHoldingItem(final Item item) { -+ return this.getDelegate().isHoldingItem(item); -+ } -+ -+ @Override -+ public boolean canStandOnFluid(final FluidState state, final FluidState fluidState) { -+ return this.getDelegate().canStandOnFluid(state, fluidState); -+ } -+ } -+ -+ private CollisionUtil() { -+ throw new RuntimeException(); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/util/collisions/CachedShapeData.java b/src/main/java/io/papermc/paper/util/collisions/CachedShapeData.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1cb96b09375770f92f3e494ce2f28d9ff8699581 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/util/collisions/CachedShapeData.java -@@ -0,0 +1,10 @@ -+package io.papermc.paper.util.collisions; -+ -+public record CachedShapeData( -+ int sizeX, int sizeY, int sizeZ, -+ long[] voxelSet, -+ int minFullX, int minFullY, int minFullZ, -+ int maxFullX, int maxFullY, int maxFullZ, -+ boolean isEmpty, boolean hasSingleAABB -+) { -+} -diff --git a/src/main/java/io/papermc/paper/util/collisions/CachedToAABBs.java b/src/main/java/io/papermc/paper/util/collisions/CachedToAABBs.java -new file mode 100644 -index 0000000000000000000000000000000000000000..85c448a775f60ca4b4a4f2baf17487ef45bdd383 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/util/collisions/CachedToAABBs.java -@@ -0,0 +1,39 @@ -+package io.papermc.paper.util.collisions; -+ -+import net.minecraft.world.phys.AABB; -+import java.util.ArrayList; -+import java.util.List; -+ -+public record CachedToAABBs( -+ List aabbs, -+ boolean isOffset, -+ double offX, double offY, double offZ -+) { -+ -+ public CachedToAABBs removeOffset() { -+ final List toOffset = this.aabbs; -+ final double offX = this.offX; -+ final double offY = this.offY; -+ final double offZ = this.offZ; -+ -+ final List ret = new ArrayList<>(toOffset.size()); -+ -+ for (int i = 0, len = toOffset.size(); i < len; ++i) { -+ ret.add(toOffset.get(i).move(offX, offY, offZ)); -+ } -+ -+ return new CachedToAABBs(ret, false, 0.0, 0.0, 0.0); -+ } -+ -+ public static CachedToAABBs offset(final CachedToAABBs cache, final double offX, final double offY, final double offZ) { -+ if (offX == 0.0 && offY == 0.0 && offZ == 0.0) { -+ return cache; -+ } -+ -+ final double resX = cache.offX + offX; -+ final double resY = cache.offY + offY; -+ final double resZ = cache.offZ + offZ; -+ -+ return new CachedToAABBs(cache.aabbs, true, resX, resY, resZ); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/util/collisions/FlatBitsetUtil.java b/src/main/java/io/papermc/paper/util/collisions/FlatBitsetUtil.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ff9d2dad39dcc02b2371458b7b5f64c6090e8012 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/util/collisions/FlatBitsetUtil.java -@@ -0,0 +1,109 @@ -+package io.papermc.paper.util.collisions; -+ -+import java.util.Objects; -+ -+public final class FlatBitsetUtil { -+ -+ private static final int LOG2_LONG = 6; -+ private static final long ALL_SET = -1L; -+ private static final int BITS_PER_LONG = Long.SIZE; -+ -+ // from inclusive -+ // to exclusive -+ public static int firstSet(final long[] bitset, final int from, final int to) { -+ if ((from | to | (to - from)) < 0) { -+ throw new IndexOutOfBoundsException(); -+ } -+ -+ int bitsetIdx = from >>> LOG2_LONG; -+ int bitIdx = from & ~(BITS_PER_LONG - 1); -+ -+ long tmp = bitset[bitsetIdx] & (ALL_SET << from); -+ for (;;) { -+ if (tmp != 0L) { -+ final int ret = bitIdx | Long.numberOfTrailingZeros(tmp); -+ return ret >= to ? -1 : ret; -+ } -+ -+ bitIdx += BITS_PER_LONG; -+ -+ if (bitIdx >= to) { -+ return -1; -+ } -+ -+ tmp = bitset[++bitsetIdx]; -+ } -+ } -+ -+ // from inclusive -+ // to exclusive -+ public static int firstClear(final long[] bitset, final int from, final int to) { -+ if ((from | to | (to - from)) < 0) { -+ throw new IndexOutOfBoundsException(); -+ } -+ // like firstSet, but invert the bitset -+ -+ int bitsetIdx = from >>> LOG2_LONG; -+ int bitIdx = from & ~(BITS_PER_LONG - 1); -+ -+ long tmp = (~bitset[bitsetIdx]) & (ALL_SET << from); -+ for (;;) { -+ if (tmp != 0L) { -+ final int ret = bitIdx | Long.numberOfTrailingZeros(tmp); -+ return ret >= to ? -1 : ret; -+ } -+ -+ bitIdx += BITS_PER_LONG; -+ -+ if (bitIdx >= to) { -+ return -1; -+ } -+ -+ tmp = ~bitset[++bitsetIdx]; -+ } -+ } -+ -+ // from inclusive -+ // to exclusive -+ public static void clearRange(final long[] bitset, final int from, int to) { -+ if ((from | to | (to - from)) < 0) { -+ throw new IndexOutOfBoundsException(); -+ } -+ -+ if (from == to) { -+ return; -+ } -+ -+ --to; -+ -+ final int fromBitsetIdx = from >>> LOG2_LONG; -+ final int toBitsetIdx = to >>> LOG2_LONG; -+ -+ final long keepFirst = ~(ALL_SET << from); -+ final long keepLast = ~(ALL_SET >>> ((BITS_PER_LONG - 1) ^ to)); -+ -+ Objects.checkFromToIndex(fromBitsetIdx, toBitsetIdx, bitset.length); -+ -+ if (fromBitsetIdx == toBitsetIdx) { -+ // special case: need to keep both first and last -+ bitset[fromBitsetIdx] &= (keepFirst | keepLast); -+ } else { -+ bitset[fromBitsetIdx] &= keepFirst; -+ -+ for (int i = fromBitsetIdx + 1; i < toBitsetIdx; ++i) { -+ bitset[i] = 0L; -+ } -+ -+ bitset[toBitsetIdx] &= keepLast; -+ } -+ } -+ -+ // from inclusive -+ // to exclusive -+ public static boolean isRangeSet(final long[] bitset, final int from, final int to) { -+ return firstClear(bitset, from, to) == -1; -+ } -+ -+ -+ private FlatBitsetUtil() {} -+} -diff --git a/src/main/java/io/papermc/paper/util/collisions/MergedORCache.java b/src/main/java/io/papermc/paper/util/collisions/MergedORCache.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1f42bdfdb052056e62a939ab0d1944f8a064fe6c ---- /dev/null -+++ b/src/main/java/io/papermc/paper/util/collisions/MergedORCache.java -@@ -0,0 +1,10 @@ -+package io.papermc.paper.util.collisions; -+ -+import net.minecraft.world.phys.shapes.VoxelShape; -+ -+public record MergedORCache( -+ VoxelShape key, -+ VoxelShape result -+) { -+ -+} -diff --git a/src/main/java/net/minecraft/core/Direction.java b/src/main/java/net/minecraft/core/Direction.java -index 03c45ee77276462818a6f774b5945b25924aa3f0..ab289a6ca85459e03acb2089c6b9e931caa9c873 100644 ---- a/src/main/java/net/minecraft/core/Direction.java -+++ b/src/main/java/net/minecraft/core/Direction.java -@@ -60,6 +60,21 @@ public enum Direction implements StringRepresentable { - private final int adjY; - private final int adjZ; - // Paper end - Perf: Inline shift direction fields -+ // Paper start - optimise collisions -+ private static final int RANDOM_OFFSET = 2017601568; -+ private Direction opposite; -+ static { -+ for (final Direction direction : VALUES) { -+ direction.opposite = from3DDataValue(direction.oppositeIndex); -+ } -+ } -+ -+ private final int id = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(this.ordinal() + RANDOM_OFFSET) + RANDOM_OFFSET); -+ -+ public final int uniqueId() { -+ return this.id; -+ } -+ // Paper end - optimise collisions - - private Direction( - final int id, -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index e06b9b0c7b1c3a200b89819e0e521f1c88dc981b..ed42a78aebeda86b77f27cba4fcefbc6c17f86fc 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -512,7 +512,7 @@ public class ServerPlayer extends Player { - - if (blockposition1 != null) { - this.moveTo(blockposition1, world.getSharedSpawnAngle(), 0.0F); // Paper - MC-200092 - fix first spawn pos yaw being ignored -- if (world.noCollision((Entity) this)) { -+ if (world.noCollision(this, this.getBoundingBox(), true)) { // Paper - make sure this loads chunks, we default to NOT loading now - break; - } - } -@@ -520,7 +520,7 @@ public class ServerPlayer extends Player { - } else { - this.moveTo(blockposition, world.getSharedSpawnAngle(), 0.0F); // Paper - MC-200092 - fix first spawn pos yaw being ignored - -- while (!world.noCollision((Entity) this) && this.getY() < (double) (world.getMaxBuildHeight() - 1)) { -+ while (!world.noCollision(this, this.getBoundingBox(), true) && this.getY() < (double) (world.getMaxBuildHeight() - 1)) { // Paper - make sure this loads chunks, we default to NOT loading now - this.setPos(this.getX(), this.getY() + 1.0D, this.getZ()); - } - } -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 60ba289e724463129dfb27aa5e3b6daf3dd7386e..68446b7532dfbda303293aa9e756644c6fcdffca 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -935,7 +935,7 @@ public abstract class PlayerList { - entityplayer1.forceSetPositionRotation(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch()); - - worldserver1.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.POST_TELEPORT, new net.minecraft.world.level.ChunkPos(location.getBlockX() >> 4, location.getBlockZ() >> 4), 1, entityplayer.getId()); // Paper -- while (avoidSuffocation && !worldserver1.noCollision((Entity) entityplayer1) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) { -+ while (avoidSuffocation && !worldserver1.noCollision(entityplayer1, entityplayer1.getBoundingBox(), true) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) { // Paper - make sure this loads chunks, we default to NOT loading now - // CraftBukkit end - entityplayer1.setPos(entityplayer1.getX(), entityplayer1.getY() + 1.0D, entityplayer1.getZ()); - } -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 0fbcf60a994f67bdd81d40e4a8bf38f0cbb8993d..8ad2d17615ff489b2fbbb13480dd0b217a42d805 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -1261,9 +1261,44 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - float f = this.getBlockSpeedFactor(); - - this.setDeltaMovement(this.getDeltaMovement().multiply((double) f, 1.0D, (double) f)); -- if (this.level().getBlockStatesIfLoaded(this.getBoundingBox().deflate(1.0E-6D)).noneMatch((iblockdata2) -> { -- return iblockdata2.is(BlockTags.FIRE) || iblockdata2.is(Blocks.LAVA); -- })) { -+ // Paper start - remove expensive streams from here -+ boolean noneMatch = true; -+ AABB fireSearchBox = this.getBoundingBox().deflate(1.0E-6D); -+ { -+ int minX = Mth.floor(fireSearchBox.minX); -+ int minY = Mth.floor(fireSearchBox.minY); -+ int minZ = Mth.floor(fireSearchBox.minZ); -+ int maxX = Mth.floor(fireSearchBox.maxX); -+ int maxY = Mth.floor(fireSearchBox.maxY); -+ int maxZ = Mth.floor(fireSearchBox.maxZ); -+ fire_search_loop: -+ for (int fz = minZ; fz <= maxZ; ++fz) { -+ for (int fx = minX; fx <= maxX; ++fx) { -+ for (int fy = minY; fy <= maxY; ++fy) { -+ net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)this.level.getChunkIfLoadedImmediately(fx >> 4, fz >> 4); -+ if (chunk == null) { -+ // Vanilla rets an empty stream if all the chunks are not loaded, so noneMatch will be true -+ // even if we're in lava/fire -+ noneMatch = true; -+ break fire_search_loop; -+ } -+ if (!noneMatch) { -+ // don't do get type, we already know we're in fire - we just need to check the chunks -+ // loaded state -+ continue; -+ } -+ -+ BlockState type = chunk.getBlockStateFinal(fx, fy, fz); -+ if (type.is(BlockTags.FIRE) || type.is(Blocks.LAVA)) { -+ noneMatch = false; -+ // can't break, we need to retain vanilla behavior by ensuring ALL chunks are loaded -+ } -+ } -+ } -+ } -+ } -+ if (noneMatch) { -+ // Paper end - remove expensive streams from here - if (this.remainingFireTicks <= 0) { - this.setRemainingFireTicks(-this.getFireImmuneTicks()); - } -@@ -1443,32 +1478,82 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - - private Vec3 collide(Vec3 movement) { -- AABB axisalignedbb = this.getBoundingBox(); -- List list = this.level().getEntityCollisions(this, axisalignedbb.expandTowards(movement)); -- Vec3 vec3d1 = movement.lengthSqr() == 0.0D ? movement : Entity.collideBoundingBox(this, movement, axisalignedbb, this.level(), list); -- boolean flag = movement.x != vec3d1.x; -- boolean flag1 = movement.y != vec3d1.y; -- boolean flag2 = movement.z != vec3d1.z; -- boolean flag3 = this.onGround() || flag1 && movement.y < 0.0D; -+ // Paper start - optimise collisions -+ final boolean xZero = movement.x == 0.0; -+ final boolean yZero = movement.y == 0.0; -+ final boolean zZero = movement.z == 0.0; -+ if (xZero & yZero & zZero) { -+ return movement; -+ } -+ -+ final Level world = this.level; -+ final AABB currBoundingBox = this.getBoundingBox(); -+ -+ if (io.papermc.paper.util.CollisionUtil.isEmpty(currBoundingBox)) { -+ return movement; -+ } -+ -+ final List potentialCollisionsBB = new java.util.ArrayList<>(); -+ final List potentialCollisionsVoxel = new java.util.ArrayList<>(); -+ final double stepHeight = (double)this.maxUpStep(); -+ final AABB collisionBox; -+ final boolean onGround = this.onGround; -+ -+ if (xZero & zZero) { -+ if (movement.y > 0.0) { -+ collisionBox = io.papermc.paper.util.CollisionUtil.cutUpwards(currBoundingBox, movement.y); -+ } else { -+ collisionBox = io.papermc.paper.util.CollisionUtil.cutDownwards(currBoundingBox, movement.y); -+ } -+ } else { -+ // note: xZero == false or zZero == false -+ if (stepHeight > 0.0 && (onGround || (movement.y < 0.0))) { -+ // don't bother getting the collisions if we don't need them. -+ if (movement.y <= 0.0) { -+ collisionBox = io.papermc.paper.util.CollisionUtil.expandUpwards(currBoundingBox.expandTowards(movement.x, movement.y, movement.z), stepHeight); -+ } else { -+ collisionBox = currBoundingBox.expandTowards(movement.x, Math.max(stepHeight, movement.y), movement.z); -+ } -+ } else { -+ collisionBox = currBoundingBox.expandTowards(movement.x, movement.y, movement.z); -+ } -+ } -+ -+ io.papermc.paper.util.CollisionUtil.getCollisions( -+ world, this, collisionBox, potentialCollisionsVoxel, potentialCollisionsBB, -+ io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, -+ null, null -+ ); -+ -+ if (potentialCollisionsVoxel.isEmpty() && potentialCollisionsBB.isEmpty()) { -+ return movement; -+ } - -- if (this.maxUpStep() > 0.0F && flag3 && (flag || flag2)) { -- Vec3 vec3d2 = Entity.collideBoundingBox(this, new Vec3(movement.x, (double) this.maxUpStep(), movement.z), axisalignedbb, this.level(), list); -- Vec3 vec3d3 = Entity.collideBoundingBox(this, new Vec3(0.0D, (double) this.maxUpStep(), 0.0D), axisalignedbb.expandTowards(movement.x, 0.0D, movement.z), this.level(), list); -+ final Vec3 limitedMoveVector = io.papermc.paper.util.CollisionUtil.performCollisions(movement, currBoundingBox, potentialCollisionsVoxel, potentialCollisionsBB); - -- if (vec3d3.y < (double) this.maxUpStep()) { -- Vec3 vec3d4 = Entity.collideBoundingBox(this, new Vec3(movement.x, 0.0D, movement.z), axisalignedbb.move(vec3d3), this.level(), list).add(vec3d3); -+ if (stepHeight > 0.0 -+ && (onGround || (limitedMoveVector.y != movement.y && movement.y < 0.0)) -+ && (limitedMoveVector.x != movement.x || limitedMoveVector.z != movement.z)) { -+ Vec3 vec3d2 = io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(movement.x, stepHeight, movement.z), currBoundingBox, potentialCollisionsVoxel, potentialCollisionsBB); -+ final Vec3 vec3d3 = io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(0.0, stepHeight, 0.0), currBoundingBox.expandTowards(movement.x, 0.0, movement.z), potentialCollisionsVoxel, potentialCollisionsBB); -+ -+ if (vec3d3.y < stepHeight) { -+ final Vec3 vec3d4 = io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(movement.x, 0.0D, movement.z), currBoundingBox.move(vec3d3), potentialCollisionsVoxel, potentialCollisionsBB).add(vec3d3); - - if (vec3d4.horizontalDistanceSqr() > vec3d2.horizontalDistanceSqr()) { - vec3d2 = vec3d4; - } - } - -- if (vec3d2.horizontalDistanceSqr() > vec3d1.horizontalDistanceSqr()) { -- return vec3d2.add(Entity.collideBoundingBox(this, new Vec3(0.0D, -vec3d2.y + movement.y, 0.0D), axisalignedbb.move(vec3d2), this.level(), list)); -+ if (vec3d2.horizontalDistanceSqr() > limitedMoveVector.horizontalDistanceSqr()) { -+ return vec3d2.add(io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(0.0D, -vec3d2.y + movement.y, 0.0D), currBoundingBox.move(vec3d2), potentialCollisionsVoxel, potentialCollisionsBB)); - } -- } - -- return vec3d1; -+ return limitedMoveVector; -+ } else { -+ return limitedMoveVector; -+ } -+ // Paper end - optimise collisions - } - - public static Vec3 collideBoundingBox(@Nullable Entity entity, Vec3 movement, AABB entityBoundingBox, Level world, List collisions) { -@@ -2735,11 +2820,70 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - float f = this.dimensions.width() * 0.8F; - AABB axisalignedbb = AABB.ofSize(this.getEyePosition(), (double) f, 1.0E-6D, (double) f); - -- return BlockPos.betweenClosedStream(axisalignedbb).anyMatch((blockposition) -> { -- BlockState iblockdata = this.level().getBlockState(blockposition); -+ // Paper start - optimise collisions -+ if (io.papermc.paper.util.CollisionUtil.isEmpty(axisalignedbb)) { -+ return false; -+ } - -- return !iblockdata.isAir() && iblockdata.isSuffocating(this.level(), blockposition) && Shapes.joinIsNotEmpty(iblockdata.getCollisionShape(this.level(), blockposition).move((double) blockposition.getX(), (double) blockposition.getY(), (double) blockposition.getZ()), Shapes.create(axisalignedbb), BooleanOp.AND); -- }); -+ final BlockPos.MutableBlockPos tempPos = new BlockPos.MutableBlockPos(); -+ -+ final int minX = Mth.floor(axisalignedbb.minX); -+ final int minY = Mth.floor(axisalignedbb.minY); -+ final int minZ = Mth.floor(axisalignedbb.minZ); -+ final int maxX = Mth.floor(axisalignedbb.maxX); -+ final int maxY = Mth.floor(axisalignedbb.maxY); -+ final int maxZ = Mth.floor(axisalignedbb.maxZ); -+ -+ final net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.level.getChunkSource(); -+ -+ long lastChunkKey = ChunkPos.INVALID_CHUNK_POS; -+ net.minecraft.world.level.chunk.LevelChunk lastChunk = null; -+ for (int fz = minZ; fz <= maxZ; ++fz) { -+ tempPos.setZ(fz); -+ for (int fx = minX; fx <= maxX; ++fx) { -+ final int newChunkX = fx >> 4; -+ final int newChunkZ = fz >> 4; -+ final net.minecraft.world.level.chunk.LevelChunk chunk = lastChunkKey == (lastChunkKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(newChunkX, newChunkZ)) ? -+ lastChunk : (lastChunk = chunkProvider.getChunkAtIfLoadedImmediately(newChunkX, newChunkZ)); -+ tempPos.setX(fx); -+ if (chunk == null) { -+ continue; -+ } -+ for (int fy = minY; fy <= maxY; ++fy) { -+ tempPos.setY(fy); -+ -+ final BlockState state = chunk.getBlockState(tempPos); -+ -+ if (state.emptyCollisionShape() || !state.isSuffocating(this.level, tempPos)) { -+ continue; -+ } -+ -+ // Yes, it does not use the Entity context stuff. -+ final VoxelShape collisionShape = state.getCollisionShape(this.level, tempPos); -+ -+ if (collisionShape.isEmpty()) { -+ continue; -+ } -+ -+ final AABB toCollide = axisalignedbb.move(-(double)fx, -(double)fy, -(double)fz); -+ -+ final AABB singleAABB = collisionShape.getSingleAABBRepresentation(); -+ if (singleAABB != null) { -+ if (io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(singleAABB, toCollide)) { -+ return true; -+ } -+ continue; -+ } -+ -+ if (io.papermc.paper.util.CollisionUtil.voxelShapeIntersectNoEmpty(collisionShape, toCollide)) { -+ return true; -+ } -+ continue; -+ } -+ } -+ } -+ // Paper end - optimise collisions -+ return false; - } - } - -diff --git a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java -index 2ed6845f16fab175e2e9e96e76391e63ab4a43e2..c2bd2e303f956d390319f6bbbe9a6492ebec5154 100644 ---- a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java -+++ b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java -@@ -360,7 +360,7 @@ public class ArmorStand extends LivingEntity { - @Override - protected void pushEntities() { - if (!this.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return; // Paper - Option to prevent armor stands from doing entity lookups -- List list = this.level().getEntities((Entity) this, this.getBoundingBox(), ArmorStand.RIDABLE_MINECARTS); -+ List list = this.level().getEntitiesOfClass(AbstractMinecart.class, this.getBoundingBox(), ArmorStand.RIDABLE_MINECARTS); // Paper - optimise collisions - Iterator iterator = list.iterator(); - - while (iterator.hasNext()) { -diff --git a/src/main/java/net/minecraft/world/entity/monster/Spider.java b/src/main/java/net/minecraft/world/entity/monster/Spider.java -index f0127f1b55999aa4a841341ad02cbcde45702b50..ef8911f7bcf6a97496675abb4689bb09cf322e85 100644 ---- a/src/main/java/net/minecraft/world/entity/monster/Spider.java -+++ b/src/main/java/net/minecraft/world/entity/monster/Spider.java -@@ -82,7 +82,7 @@ public class Spider extends Monster { - public void tick() { - super.tick(); - if (!this.level().isClientSide) { -- this.setClimbing(this.horizontalCollision && (this.level().paperConfig().entities.behavior.allowSpiderWorldBorderClimbing)); // Paper - Add config option for spider worldborder climbing -+ this.setClimbing(this.horizontalCollision && (this.level().paperConfig().entities.behavior.allowSpiderWorldBorderClimbing || !io.papermc.paper.util.CollisionUtil.isCollidingWithBorder(this.level().getWorldBorder(), this.getBoundingBox().inflate(io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)))); // Paper - Add config option for spider worldborder climbing & Inflate by +EPSILON as collision will just barely place us outside border - } - - } -diff --git a/src/main/java/net/minecraft/world/level/BlockCollisions.java b/src/main/java/net/minecraft/world/level/BlockCollisions.java -index cd89623a44f02d7db77f0d0f87545cf80841f403..48710a60561824a3670ebef3601f284dd7089481 100644 ---- a/src/main/java/net/minecraft/world/level/BlockCollisions.java -+++ b/src/main/java/net/minecraft/world/level/BlockCollisions.java -@@ -99,7 +99,7 @@ public class BlockCollisions extends AbstractIterator { - // Paper end - VoxelShape voxelShape = blockState.getCollisionShape(this.collisionGetter, this.pos, this.context); - if (voxelShape == Shapes.block()) { -- if (this.box.intersects((double)i, (double)j, (double)k, (double)i + 1.0, (double)j + 1.0, (double)k + 1.0)) { -+ if (io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(this.box, i, j, k, i + 1.0, j + 1.0, k + 1.0)) { // Paper - keep vanilla behavior for voxelshape intersection - See comment in CollisionUtil - return this.resultProvider.apply(this.pos, voxelShape.move((double)i, (double)j, (double)k)); - } - } else { -diff --git a/src/main/java/net/minecraft/world/level/ClipContext.java b/src/main/java/net/minecraft/world/level/ClipContext.java -index 3fa2964b979053ecbefc946c7fe76828de86d8f1..2634ba06147c936b0fae08a190c7820c832e6b23 100644 ---- a/src/main/java/net/minecraft/world/level/ClipContext.java -+++ b/src/main/java/net/minecraft/world/level/ClipContext.java -@@ -17,8 +17,8 @@ public class ClipContext { - - private final Vec3 from; - private final Vec3 to; -- private final ClipContext.Block block; -- private final ClipContext.Fluid fluid; -+ public final ClipContext.Block block; // Paper - optimise collisions - public -+ public final ClipContext.Fluid fluid; // Paper - optimise collisions - public - private final CollisionContext collisionContext; - - public ClipContext(Vec3 start, Vec3 end, ClipContext.Block shapeType, ClipContext.Fluid fluidHandling, Entity entity) { -diff --git a/src/main/java/net/minecraft/world/level/CollisionGetter.java b/src/main/java/net/minecraft/world/level/CollisionGetter.java -index 1ad0c976c6e2d6d31397dff850a9de7c16d16fba..dc877fe2e3c53b353baa59c125232e425fee67d7 100644 ---- a/src/main/java/net/minecraft/world/level/CollisionGetter.java -+++ b/src/main/java/net/minecraft/world/level/CollisionGetter.java -@@ -35,6 +35,12 @@ public interface CollisionGetter extends BlockGetter { - return this.isUnobstructed(entity, Shapes.create(entity.getBoundingBox())); - } - -+ // Paper start - optimise collisions -+ default boolean noCollision(Entity entity, AABB box, boolean loadChunks) { -+ return this.noCollision(entity, box); -+ } -+ // Paper end - optimise collisions -+ - default boolean noCollision(AABB box) { - return this.noCollision(null, box); - } -diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java -index 9a28912f52824acdc80a62243b136e6f365bf567..21843501355a0c0c8d594e3e5312e97861c9a777 100644 ---- a/src/main/java/net/minecraft/world/level/EntityGetter.java -+++ b/src/main/java/net/minecraft/world/level/EntityGetter.java -@@ -46,20 +46,36 @@ public interface EntityGetter { - } - - default boolean isUnobstructed(@Nullable Entity except, VoxelShape shape) { -+ // Paper start - optimise collisions - if (shape.isEmpty()) { -- return true; -- } else { -- for (Entity entity : this.getEntities(except, shape.bounds())) { -- if (!entity.isRemoved() -- && entity.blocksBuilding -- && (except == null || !entity.isPassengerOfSameVehicle(except)) -- && Shapes.joinIsNotEmpty(shape, Shapes.create(entity.getBoundingBox()), BooleanOp.AND)) { -- return false; -+ return false; -+ } -+ -+ final AABB singleAABB = shape.getSingleAABBRepresentation(); -+ final List entities = this.getEntities( -+ except, -+ singleAABB == null ? shape.bounds() : singleAABB.inflate(-io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) -+ ); -+ -+ for (int i = 0, len = entities.size(); i < len; ++i) { -+ final Entity otherEntity = entities.get(i); -+ -+ if (otherEntity.isRemoved() || !otherEntity.blocksBuilding || (except != null && otherEntity.isPassengerOfSameVehicle(except))) { -+ continue; -+ } -+ -+ if (singleAABB == null) { -+ final AABB entityBB = otherEntity.getBoundingBox(); -+ if (io.papermc.paper.util.CollisionUtil.isEmpty(entityBB) || !io.papermc.paper.util.CollisionUtil.voxelShapeIntersectNoEmpty(shape, entityBB)) { -+ continue; - } - } - -- return true; -+ return false; - } -+ -+ return true; -+ // Paper end - optimise collisions - } - - default List getEntitiesOfClass(Class entityClass, AABB box) { -@@ -67,23 +83,41 @@ public interface EntityGetter { - } - - default List getEntityCollisions(@Nullable Entity entity, AABB box) { -- if (box.getSize() < 1.0E-7) { -- return List.of(); -+ // Paper start - optimise collisions -+ // first behavior change is to correctly check for empty AABB -+ if (io.papermc.paper.util.CollisionUtil.isEmpty(box)) { -+ // reduce indirection by always returning type with same class -+ return new java.util.ArrayList<>(); -+ } -+ -+ // to comply with vanilla intersection rules, expand by -epsilon so that we only get stuff we definitely collide with. -+ // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems -+ // specifically with boat collisions. -+ box = box.inflate(-io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON); -+ -+ final List entities; -+ if (entity != null && entity.hardCollides()) { -+ entities = this.getEntities(entity, box, null); - } else { -- Predicate predicate = entity == null ? EntitySelector.CAN_BE_COLLIDED_WITH : EntitySelector.NO_SPECTATORS.and(entity::canCollideWith); -- List list = this.getEntities(entity, box.inflate(1.0E-7), predicate); -- if (list.isEmpty()) { -- return List.of(); -- } else { -- Builder builder = ImmutableList.builderWithExpectedSize(list.size()); -- -- for (Entity entity2 : list) { -- builder.add(Shapes.create(entity2.getBoundingBox())); -- } -+ entities = this.getHardCollidingEntities(entity, box, null); -+ } -+ -+ final List ret = new java.util.ArrayList<>(Math.min(25, entities.size())); - -- return builder.build(); -+ for (int i = 0, len = entities.size(); i < len; ++i) { -+ final Entity otherEntity = entities.get(i); -+ -+ if (otherEntity.isSpectator()) { -+ continue; -+ } -+ -+ if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) { -+ ret.add(Shapes.create(otherEntity.getBoundingBox())); - } - } -+ -+ return ret; -+ // Paper end - optimise collisions - } - - // Paper start - Affects Spawning API -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index 4001bec504b8fcda1eec03f49abd4e4d82cf6336..07281d73fcbca2ea5f8bce25f6bf961d258bf8a0 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -287,6 +287,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - this.entityLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.entityMaxTickTime); - this.tileLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.tileMaxTickTime); - this.chunkPacketBlockController = this.paperConfig().anticheat.antiXray.enabled ? new com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor) : com.destroystokyo.paper.antixray.ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray -+ // Paper start - optimise collisions -+ this.minSection = io.papermc.paper.util.WorldUtil.getMinSection(this); -+ this.maxSection = io.papermc.paper.util.WorldUtil.getMaxSection(this); -+ // Paper end - optimise collisions - } - - // Paper start - Cancel hit for vanished players -@@ -328,6 +332,366 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - return true; - } - // Paper end - Cancel hit for vanished players -+ // Paper start - optimise collisions -+ public final int minSection; -+ public final int maxSection; -+ -+ @Override -+ public final boolean isUnobstructed(final Entity entity) { -+ final AABB boundingBox = entity.getBoundingBox(); -+ if (io.papermc.paper.util.CollisionUtil.isEmpty(boundingBox)) { -+ return false; -+ } -+ -+ final List entities = this.getEntities( -+ entity, -+ boundingBox.inflate(-io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON), -+ null -+ ); -+ -+ for (int i = 0, len = entities.size(); i < len; ++i) { -+ final Entity otherEntity = entities.get(i); -+ -+ if (otherEntity.isSpectator() || otherEntity.isRemoved() || !otherEntity.blocksBuilding || otherEntity.isPassengerOfSameVehicle(entity)) { -+ continue; -+ } -+ -+ return false; -+ } -+ -+ return true; -+ } -+ -+ private static net.minecraft.world.phys.BlockHitResult miss(final ClipContext clipContext) { -+ final Vec3 to = clipContext.getTo(); -+ final Vec3 from = clipContext.getFrom(); -+ -+ return net.minecraft.world.phys.BlockHitResult.miss(to, Direction.getNearest(from.x - to.x, from.y - to.y, from.z - to.z), BlockPos.containing(to.x, to.y, to.z)); -+ } -+ -+ private static final FluidState AIR_FLUIDSTATE = Fluids.EMPTY.defaultFluidState(); -+ -+ private static net.minecraft.world.phys.BlockHitResult fastClip(final Vec3 from, final Vec3 to, final Level level, -+ final ClipContext clipContext) { -+ final double adjX = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.x - to.x); -+ final double adjY = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.y - to.y); -+ final double adjZ = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.z - to.z); -+ -+ if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) { -+ return miss(clipContext); -+ } -+ -+ final double toXAdj = to.x - adjX; -+ final double toYAdj = to.y - adjY; -+ final double toZAdj = to.z - adjZ; -+ final double fromXAdj = from.x + adjX; -+ final double fromYAdj = from.y + adjY; -+ final double fromZAdj = from.z + adjZ; -+ -+ int currX = Mth.floor(fromXAdj); -+ int currY = Mth.floor(fromYAdj); -+ int currZ = Mth.floor(fromZAdj); -+ -+ final BlockPos.MutableBlockPos currPos = new BlockPos.MutableBlockPos(); -+ -+ final double diffX = toXAdj - fromXAdj; -+ final double diffY = toYAdj - fromYAdj; -+ final double diffZ = toZAdj - fromZAdj; -+ -+ final double dxDouble = Math.signum(diffX); -+ final double dyDouble = Math.signum(diffY); -+ final double dzDouble = Math.signum(diffZ); -+ -+ final int dx = (int)dxDouble; -+ final int dy = (int)dyDouble; -+ final int dz = (int)dzDouble; -+ -+ final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX; -+ final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY; -+ final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ; -+ -+ double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj)); -+ double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj)); -+ double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj)); -+ -+ net.minecraft.world.level.chunk.LevelChunkSection[] lastChunk = null; -+ net.minecraft.world.level.chunk.PalettedContainer lastSection = null; -+ int lastChunkX = Integer.MIN_VALUE; -+ int lastChunkY = Integer.MIN_VALUE; -+ int lastChunkZ = Integer.MIN_VALUE; -+ -+ final int minSection = level.minSection; -+ final net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)level.getChunkSource(); -+ -+ for (;;) { -+ currPos.set(currX, currY, currZ); -+ -+ final int newChunkX = currX >> 4; -+ final int newChunkY = currY >> 4; -+ final int newChunkZ = currZ >> 4; -+ -+ final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)); -+ final int chunkYDiff = newChunkY ^ lastChunkY; -+ -+ if ((chunkDiff | chunkYDiff) != 0) { -+ if (chunkDiff != 0) { -+ LevelChunk chunk = chunkProvider.getChunkAtIfLoadedImmediately(newChunkX, newChunkZ); -+ lastChunk = chunk == null ? null : chunk.getSections(); // diff: don't load chunks for this -+ } -+ final int sectionY = newChunkY - minSection; -+ lastSection = lastChunk != null && sectionY >= 0 && sectionY < lastChunk.length ? lastChunk[sectionY].states : null; -+ -+ lastChunkX = newChunkX; -+ lastChunkY = newChunkY; -+ lastChunkZ = newChunkZ; -+ } -+ -+ final BlockState blockState; -+ if (lastSection != null && !(blockState = lastSection.get((currX & 15) | ((currZ & 15) << 4) | ((currY & 15) << (4+4)))).isAir()) { -+ final net.minecraft.world.phys.shapes.VoxelShape blockCollision = clipContext.getBlockShape(blockState, level, currPos); -+ -+ final net.minecraft.world.phys.BlockHitResult blockHit = blockCollision.isEmpty() ? null : level.clipWithInteractionOverride(from, to, currPos, blockCollision, blockState); -+ -+ final net.minecraft.world.phys.shapes.VoxelShape fluidCollision; -+ final FluidState fluidState; -+ if (clipContext.fluid != ClipContext.Fluid.NONE && (fluidState = blockState.getFluidState()) != AIR_FLUIDSTATE) { -+ fluidCollision = clipContext.getFluidShape(fluidState, level, currPos); -+ -+ final net.minecraft.world.phys.BlockHitResult fluidHit = fluidCollision.clip(from, to, currPos); -+ -+ if (fluidHit != null) { -+ if (blockHit == null) { -+ return fluidHit; -+ } -+ -+ return from.distanceToSqr(blockHit.getLocation()) <= from.distanceToSqr(fluidHit.getLocation()) ? blockHit : fluidHit; -+ } -+ } -+ -+ if (blockHit != null) { -+ return blockHit; -+ } -+ } // else: usually fall here -+ -+ if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) { -+ return miss(clipContext); -+ } -+ -+ // inc the smallest normalized coordinate -+ -+ if (normalizedCurrX < normalizedCurrY) { -+ if (normalizedCurrX < normalizedCurrZ) { -+ currX += dx; -+ normalizedCurrX += normalizedDiffX; -+ } else { -+ // x < y && x >= z <--> z < y && z <= x -+ currZ += dz; -+ normalizedCurrZ += normalizedDiffZ; -+ } -+ } else if (normalizedCurrY < normalizedCurrZ) { -+ // y <= x && y < z -+ currY += dy; -+ normalizedCurrY += normalizedDiffY; -+ } else { -+ // y <= x && z <= y <--> z <= y && z <= x -+ currZ += dz; -+ normalizedCurrZ += normalizedDiffZ; -+ } -+ } -+ } -+ -+ @Override -+ public final net.minecraft.world.phys.BlockHitResult clip(final ClipContext clipContext) { -+ // can only do this in this class, as not everything that implements BlockGetter can retrieve chunks -+ return fastClip(clipContext.getFrom(), clipContext.getTo(), this, clipContext); -+ } -+ -+ @Override -+ public final boolean noCollision(final Entity entity, final AABB box, final boolean loadChunks) { -+ int flags = io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_ONLY; -+ if (entity != null) { -+ flags |= io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER; -+ } -+ if (loadChunks) { -+ flags |= io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_LOAD_CHUNKS; -+ } -+ if (io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, null, flags, null)) { -+ return false; -+ } -+ -+ return !io.papermc.paper.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, flags, null); -+ } -+ -+ @Override -+ public final boolean collidesWithSuffocatingBlock(final Entity entity, final AABB box) { -+ return io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, null, -+ io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_ONLY, -+ (final BlockState state, final BlockPos pos) -> { -+ return state.isSuffocating(Level.this, pos); -+ } -+ ); -+ } -+ -+ private static net.minecraft.world.phys.shapes.VoxelShape inflateAABBToVoxel(final AABB aabb, final double x, final double y, final double z) { -+ return net.minecraft.world.phys.shapes.Shapes.create( -+ aabb.minX - x, -+ aabb.minY - y, -+ aabb.minZ - z, -+ -+ aabb.maxX + x, -+ aabb.maxY + y, -+ aabb.maxZ + z -+ ); -+ } -+ -+ @Override -+ public final java.util.Optional findFreePosition(final Entity entity, final net.minecraft.world.phys.shapes.VoxelShape boundsShape, final Vec3 fromPosition, -+ final double rangeX, final double rangeY, final double rangeZ) { -+ if (boundsShape.isEmpty()) { -+ return java.util.Optional.empty(); -+ } -+ -+ final double expandByX = rangeX * 0.5; -+ final double expandByY = rangeY * 0.5; -+ final double expandByZ = rangeZ * 0.5; -+ -+ // note: it is useless to look at shapes outside of range / 2.0 -+ final AABB collectionVolume = boundsShape.bounds().inflate(expandByX, expandByY, expandByZ); -+ -+ final List aabbs = new java.util.ArrayList<>(); -+ final List voxels = new java.util.ArrayList<>(); -+ -+ io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder( -+ this, entity, collectionVolume, voxels, aabbs, -+ io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, -+ null -+ ); -+ -+ // push voxels into aabbs -+ for (int i = 0, len = voxels.size(); i < len; ++i) { -+ aabbs.addAll(voxels.get(i).toAabbs()); -+ } -+ -+ // expand AABBs -+ final net.minecraft.world.phys.shapes.VoxelShape first = aabbs.isEmpty() ? net.minecraft.world.phys.shapes.Shapes.empty() : inflateAABBToVoxel(aabbs.get(0), expandByX, expandByY, expandByZ); -+ final net.minecraft.world.phys.shapes.VoxelShape[] rest = new net.minecraft.world.phys.shapes.VoxelShape[Math.max(0, aabbs.size() - 1)]; -+ -+ for (int i = 1, len = aabbs.size(); i < len; ++i) { -+ rest[i - 1] = inflateAABBToVoxel(aabbs.get(i), expandByX, expandByY, expandByZ); -+ } -+ -+ // use optimized implementation of ORing the shapes together -+ final net.minecraft.world.phys.shapes.VoxelShape joined = net.minecraft.world.phys.shapes.Shapes.or(first, rest); -+ -+ // find free space -+ // can use unoptimized join here (instead of join()), as closestPointTo uses toAabbs() -+ final net.minecraft.world.phys.shapes.VoxelShape freeSpace = net.minecraft.world.phys.shapes.Shapes.joinUnoptimized( -+ boundsShape, joined, net.minecraft.world.phys.shapes.BooleanOp.ONLY_FIRST -+ ); -+ -+ return freeSpace.closestPointTo(fromPosition); -+ } -+ -+ @Override -+ public final java.util.Optional findSupportingBlock(final Entity entity, final AABB aabb) { -+ final int minBlockX = Mth.floor(aabb.minX - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) - 1; -+ final int maxBlockX = Mth.floor(aabb.maxX + io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) + 1; -+ -+ final int minBlockY = Mth.floor(aabb.minY - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) - 1; -+ final int maxBlockY = Mth.floor(aabb.maxY + io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) + 1; -+ -+ final int minBlockZ = Mth.floor(aabb.minZ - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) - 1; -+ final int maxBlockZ = Mth.floor(aabb.maxZ + io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) + 1; -+ -+ io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext collisionContext = null; -+ -+ final BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); -+ BlockPos selected = null; -+ double selectedDistance = Double.MAX_VALUE; -+ -+ final Vec3 entityPos = entity.position(); -+ -+ LevelChunk lastChunk = null; -+ int lastChunkX = Integer.MIN_VALUE; -+ int lastChunkZ = Integer.MIN_VALUE; -+ -+ final net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.getChunkSource(); -+ -+ for (int currZ = minBlockZ; currZ <= maxBlockZ; ++currZ) { -+ pos.setZ(currZ); -+ for (int currX = minBlockX; currX <= maxBlockX; ++currX) { -+ pos.setX(currX); -+ -+ final int newChunkX = currX >> 4; -+ final int newChunkZ = currZ >> 4; -+ -+ final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)); -+ -+ if (chunkDiff != 0) { -+ lastChunk = chunkProvider.getChunkAtIfLoadedImmediately(newChunkX, newChunkZ); -+ } -+ -+ if (lastChunk == null) { -+ continue; -+ } -+ for (int currY = minBlockY; currY <= maxBlockY; ++currY) { -+ int edgeCount = ((currX == minBlockX || currX == maxBlockX) ? 1 : 0) + -+ ((currY == minBlockY || currY == maxBlockY) ? 1 : 0) + -+ ((currZ == minBlockZ || currZ == maxBlockZ) ? 1 : 0); -+ if (edgeCount == 3) { -+ continue; -+ } -+ -+ pos.setY(currY); -+ -+ final double distance = pos.distToCenterSqr(entityPos); -+ if (distance > selectedDistance || (distance == selectedDistance && selected.compareTo(pos) >= 0)) { -+ continue; -+ } -+ -+ final BlockState state = lastChunk.getBlockState(currX, currY, currZ); -+ if (state.emptyCollisionShape()) { -+ continue; -+ } -+ -+ if ((edgeCount != 1 || state.hasLargeCollisionShape()) && (edgeCount != 2 || state.getBlock() == Blocks.MOVING_PISTON)) { -+ if (collisionContext == null) { -+ collisionContext = new io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext(entity); -+ } -+ final net.minecraft.world.phys.shapes.VoxelShape blockCollision = state.getCollisionShape(lastChunk, pos, collisionContext); -+ if (blockCollision.isEmpty()) { -+ continue; -+ } -+ -+ // avoid VoxelShape#move by shifting the entity collision shape instead -+ final AABB shiftedAABB = aabb.move(-(double)currX, -(double)currY, -(double)currZ); -+ -+ final AABB singleAABB = blockCollision.getSingleAABBRepresentation(); -+ if (singleAABB != null) { -+ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(singleAABB, shiftedAABB)) { -+ continue; -+ } -+ -+ selected = pos.immutable(); -+ selectedDistance = distance; -+ continue; -+ } -+ -+ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersectNoEmpty(blockCollision, shiftedAABB)) { -+ continue; -+ } -+ -+ selected = pos.immutable(); -+ selectedDistance = distance; -+ continue; -+ } -+ } -+ } -+ } -+ -+ return java.util.Optional.ofNullable(selected); -+ } -+ // Paper end - optimise collisions - @Override - public boolean isClientSide() { - return this.isClientSide; -@@ -949,7 +1313,17 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - @Override - public boolean noCollision(@Nullable Entity entity, AABB box) { - if (entity instanceof net.minecraft.world.entity.decoration.ArmorStand && !entity.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return false; -- return LevelAccessor.super.noCollision(entity, box); -+ // Paper start - optimise collisions -+ int flags = io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_ONLY; -+ if (entity != null) { -+ flags |= io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER; -+ } -+ if (io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, null, flags, null)) { -+ return false; -+ } -+ -+ return !io.papermc.paper.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, flags, null); -+ // Paper end - optimise collisions - } - // Paper end - Option to prevent armor stands from doing entity lookups - -diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java -index 054593fc0b8d13f6bf449cc20a1f7ddfd5f1d1f0..cf8b8c8efd1c9c81eb5f02d75bd75875eb66771f 100644 ---- a/src/main/java/net/minecraft/world/level/block/Block.java -+++ b/src/main/java/net/minecraft/world/level/block/Block.java -@@ -280,7 +280,7 @@ public class Block extends BlockBehaviour implements ItemLike { - } - - public static boolean isShapeFullBlock(VoxelShape shape) { -- return (Boolean) Block.SHAPE_FULL_BLOCK_CACHE.getUnchecked(shape); -+ return shape.isFullBlock(); // Paper - optimise collisions - } - - public void animateTick(BlockState state, Level world, BlockPos pos, RandomSource random) {} -diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -index 55efd0d379bac79935f62446cd3479d1e59361a4..2034ca2edd3aff61d94416266e75402babd3e741 100644 ---- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -+++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -@@ -813,6 +813,10 @@ public abstract class BlockBehaviour implements FeatureElement { - this.instrument = blockbase_info.instrument; - this.replaceable = blockbase_info.replaceable; - this.conditionallyFullOpaque = this.canOcclude & this.useShapeForLightOcclusion; // Paper -+ // Paper start - optimise collisions -+ this.id1 = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(ID_GENERATOR.getAndIncrement() + RANDOM_OFFSET) + RANDOM_OFFSET); -+ this.id2 = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(ID_GENERATOR.getAndIncrement() + RANDOM_OFFSET) + RANDOM_OFFSET); -+ // Paper end - optimise collisions - } - // Paper start - Perf: impl cached craft block data, lazy load to fix issue with loading at the wrong time - private org.bukkit.craftbukkit.block.data.CraftBlockData cachedCraftBlockData; -@@ -861,6 +865,52 @@ public abstract class BlockBehaviour implements FeatureElement { - return this.conditionallyFullOpaque; - } - // Paper end - starlight -+ // Paper start - optimise collisions -+ private static final int RANDOM_OFFSET = 704237939; -+ private static final Direction[] DIRECTIONS_CACHED = Direction.values(); -+ private static final java.util.concurrent.atomic.AtomicInteger ID_GENERATOR = new java.util.concurrent.atomic.AtomicInteger(); -+ private final int id1, id2; -+ private boolean occludesFullBlock; -+ private boolean emptyCollisionShape; -+ private VoxelShape constantCollisionShape; -+ private AABB constantAABBCollision; -+ private static void initCaches(final VoxelShape shape) { -+ shape.isFullBlock(); -+ shape.occludesFullBlock(); -+ shape.toAabbs(); -+ if (!shape.isEmpty()) { -+ shape.bounds(); -+ } -+ } -+ -+ public final boolean hasCache() { -+ return this.cache != null; -+ } -+ -+ public final boolean occludesFullBlock() { -+ return this.occludesFullBlock; -+ } -+ -+ public final boolean emptyCollisionShape() { -+ return this.emptyCollisionShape; -+ } -+ -+ public final int uniqueId1() { -+ return this.id1; -+ } -+ -+ public final int uniqueId2() { -+ return this.id2; -+ } -+ -+ public final VoxelShape getConstantCollisionShape() { -+ return this.constantCollisionShape; -+ } -+ -+ public final AABB getConstantCollisionAABB() { -+ return this.constantAABBCollision; -+ } -+ // Paper end - optimise collisions - - public void initCache() { - this.fluidState = ((Block) this.owner).getFluidState(this.asState()); -@@ -872,6 +922,39 @@ public abstract class BlockBehaviour implements FeatureElement { - this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque() ? -1 : this.cache.lightBlock; // Paper - starlight - cache opacity for light - - this.legacySolid = this.calculateSolid(); -+ // Paper start - optimise collisions -+ if (this.cache != null) { -+ final VoxelShape collisionShape = this.cache.collisionShape; -+ try { -+ this.constantCollisionShape = this.getCollisionShape(null, null, null); -+ this.constantAABBCollision = this.constantCollisionShape == null ? null : this.constantCollisionShape.getSingleAABBRepresentation(); -+ } catch (final Throwable throwable) { -+ this.constantCollisionShape = null; -+ this.constantAABBCollision = null; -+ } -+ this.occludesFullBlock = collisionShape.occludesFullBlock(); -+ this.emptyCollisionShape = collisionShape.isEmpty(); -+ // init caches -+ initCaches(collisionShape); -+ if (collisionShape != Shapes.empty() && collisionShape != Shapes.block()) { -+ for (final Direction direction : DIRECTIONS_CACHED) { -+ // initialise the directional face shape cache as well -+ final VoxelShape shape = Shapes.getFaceShape(collisionShape, direction); -+ initCaches(shape); -+ } -+ } -+ if (this.cache.occlusionShapes != null) { -+ for (final VoxelShape shape : this.cache.occlusionShapes) { -+ initCaches(shape); -+ } -+ } -+ } else { -+ this.occludesFullBlock = false; -+ this.emptyCollisionShape = false; -+ this.constantCollisionShape = null; -+ this.constantAABBCollision = null; -+ } -+ // Paper end - optimise collisions - } - - public Block getBlock() { -diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java -index 8cd6c1d838e0332125fde3fc36475034aa4effa0..a2a5aef769ee8bb638a5a9f3da9812fa4a85dda5 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java -+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java -@@ -26,6 +26,22 @@ public class LevelChunkSection { - // CraftBukkit start - read/write - private PalettedContainer> biomes; - public final com.destroystokyo.paper.util.maplist.IBlockDataList tickingList = new com.destroystokyo.paper.util.maplist.IBlockDataList(); // Paper -+ // Paper start - optimise collisions -+ private int specialCollidingBlocks; -+ -+ private void updateBlockCallback(final int x, final int y, final int z, final BlockState oldState, final BlockState newState) { -+ if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(newState)) { -+ ++this.specialCollidingBlocks; -+ } -+ if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(oldState)) { -+ --this.specialCollidingBlocks; -+ } -+ } -+ -+ public final int getSpecialCollidingBlocks() { -+ return this.specialCollidingBlocks; -+ } -+ // Paper end - optimise collisions - - public LevelChunkSection(PalettedContainer datapaletteblock, PalettedContainer> palettedcontainerro) { - // CraftBukkit end -@@ -62,8 +78,8 @@ public class LevelChunkSection { - return this.setBlockState(x, y, z, state, true); - } - -- public BlockState setBlockState(int x, int y, int z, BlockState state, boolean lock) { -- BlockState iblockdata1; -+ public BlockState setBlockState(int x, int y, int z, BlockState state, boolean lock) { // Paper - state -> new state -+ BlockState iblockdata1; // Paper - iblockdata1 -> oldState - - if (lock) { - iblockdata1 = (BlockState) this.states.getAndSet(x, y, z, state); -@@ -102,6 +118,7 @@ public class LevelChunkSection { - ++this.tickingFluidCount; - } - -+ this.updateBlockCallback(x, y, z, iblockdata1, state); // Paper - optimise collisions - return iblockdata1; - } - -@@ -147,6 +164,11 @@ public class LevelChunkSection { - } - } - -+ // Paper start - optimise collisions -+ if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(iblockdata)) { -+ ++this.specialCollidingBlocks; -+ } -+ // Paper end - optimise collisions - }); - } - // Paper end - unfuck this -diff --git a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java -index 1c0712295695727ee9c4d430d4157b8e17cbd71f..c2943d892b067b3f1fb3b93301a092e912d71f08 100644 ---- a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java -+++ b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java -@@ -240,6 +240,17 @@ public abstract class FlowingFluid extends Fluid { - } - - private boolean canPassThroughWall(Direction face, BlockGetter world, BlockPos pos, BlockState state, BlockPos fromPos, BlockState fromState) { -+ // Paper start - optimise collisions -+ if (state.emptyCollisionShape() & fromState.emptyCollisionShape()) { -+ // don't even try to cache simple cases -+ return true; -+ } -+ -+ if (state.occludesFullBlock() | fromState.occludesFullBlock()) { -+ // don't even try to cache simple cases -+ return false; -+ } -+ // Paper end - optimise collisions - Object2ByteLinkedOpenHashMap object2bytelinkedopenhashmap; - - if (!state.getBlock().hasDynamicShape() && !fromState.getBlock().hasDynamicShape()) { -diff --git a/src/main/java/net/minecraft/world/phys/AABB.java b/src/main/java/net/minecraft/world/phys/AABB.java -index 62752e28a68400f0e1a44f0196f0e51e3dd702b8..92394960fc76886f393cba02ac33c57739a4b383 100644 ---- a/src/main/java/net/minecraft/world/phys/AABB.java -+++ b/src/main/java/net/minecraft/world/phys/AABB.java -@@ -25,6 +25,17 @@ public class AABB { - this.maxZ = Math.max(z1, z2); - } - -+ // Paper start -+ public AABB(double minX, double minY, double minZ, double maxX, double maxY, double maxZ, boolean dummy) { -+ this.minX = minX; -+ this.minY = minY; -+ this.minZ = minZ; -+ this.maxX = maxX; -+ this.maxY = maxY; -+ this.maxZ = maxZ; -+ } -+ // Paper end -+ - public AABB(BlockPos pos) { - this((double)pos.getX(), (double)pos.getY(), (double)pos.getZ(), (double)(pos.getX() + 1), (double)(pos.getY() + 1), (double)(pos.getZ() + 1)); - } -@@ -321,7 +332,7 @@ public class AABB { - } - - @Nullable -- private static Direction getDirection( -+ public static Direction getDirection( // Paper - optimise collisions - public - AABB box, Vec3 intersectingVector, double[] traceDistanceResult, @Nullable Direction approachDirection, double deltaX, double deltaY, double deltaZ - ) { - if (deltaX > 1.0E-7) { -diff --git a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java -index fc7f986812bdf74e0aea3bd09a1d53ba6def697f..0583d40a235aaecd9d6081486bbfb7355709a5ac 100644 ---- a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java -+++ b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java -@@ -20,7 +20,7 @@ public class ArrayVoxelShape extends VoxelShape { - ); - } - -- ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints) { -+ public ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints) { // Paper - optimise collisions - public - super(shape); - int i = shape.getXSize() + 1; - int j = shape.getYSize() + 1; -@@ -34,6 +34,7 @@ public class ArrayVoxelShape extends VoxelShape { - new IllegalArgumentException("Lengths of point arrays must be consistent with the size of the VoxelShape.") - ); - } -+ this.initCache(); // Paper - optimise collisions - } - - @Override -@@ -49,4 +50,5 @@ public class ArrayVoxelShape extends VoxelShape { - throw new IllegalArgumentException(); - } - } -+ - } -diff --git a/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java -index e8f3307727e7e3da9a7629cafc6e1ee53790b75d..a71421947b161697d43bd8602e2af95aa272ed3f 100644 ---- a/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java -+++ b/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java -@@ -4,13 +4,13 @@ import java.util.BitSet; - import net.minecraft.core.Direction; - - public final class BitSetDiscreteVoxelShape extends DiscreteVoxelShape { -- private final BitSet storage; -- private int xMin; -- private int yMin; -- private int zMin; -- private int xMax; -- private int yMax; -- private int zMax; -+ public final BitSet storage; // Paper - optimise collisions - public -+ public int xMin; // Paper - optimise collisions - public -+ public int yMin; // Paper - optimise collisions - public -+ public int zMin; // Paper - optimise collisions - public -+ public int xMax; // Paper - optimise collisions - public -+ public int yMax; // Paper - optimise collisions - public -+ public int zMax; // Paper - optimise collisions - public - - public BitSetDiscreteVoxelShape(int sizeX, int sizeY, int sizeZ) { - super(sizeX, sizeY, sizeZ); -@@ -151,45 +151,106 @@ public final class BitSetDiscreteVoxelShape extends DiscreteVoxelShape { - } - - protected static void forAllBoxes(DiscreteVoxelShape voxelSet, DiscreteVoxelShape.IntLineConsumer callback, boolean coalesce) { -- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = new BitSetDiscreteVoxelShape(voxelSet); -+ // Paper start - optimise collisions -+ // called with the shape of a VoxelShape, so we can expect the cache to exist -+ final io.papermc.paper.util.collisions.CachedShapeData cache = voxelSet.getOrCreateCachedShapeData(); -+ -+ final int sizeX = cache.sizeX(); -+ final int sizeY = cache.sizeY(); -+ final int sizeZ = cache.sizeZ(); -+ -+ int indexX; -+ int indexY = 0; -+ int indexZ; -+ -+ int incY = sizeZ; -+ int incX = sizeZ*sizeY; -+ -+ long[] bitset = cache.voxelSet(); -+ -+ // index = z + y*size_z + x*(size_z*size_y) -+ -+ if (!coalesce) { -+ // due to the odd selection of loop order (which does affect behavior, unfortunately) we can't simply -+ // increment an index in the Z loop, and have to perform this trash (keeping track of 3 counters) to avoid -+ // the multiplication -+ for (int y = 0; y < sizeY; ++y, indexY += incY) { -+ indexX = indexY; -+ for (int x = 0; x < sizeX; ++x, indexX += incX) { -+ indexZ = indexX; -+ for (int z = 0; z < sizeZ; ++z, ++indexZ) { -+ if ((bitset[indexZ >>> 6] & (1L << indexZ)) != 0L) { -+ callback.consume(x, y, z, x + 1, y + 1, z + 1); -+ } -+ } -+ } -+ } -+ } else { -+ // same notes about loop order as the above -+ // this branch is actually important to optimise, as it affects uncached toAabbs() (which affects optimize()) - -- for (int i = 0; i < bitSetDiscreteVoxelShape.ySize; i++) { -- for (int j = 0; j < bitSetDiscreteVoxelShape.xSize; j++) { -- int k = -1; -+ // only clone when we may write to it -+ bitset = bitset.clone(); - -- for (int l = 0; l <= bitSetDiscreteVoxelShape.zSize; l++) { -- if (bitSetDiscreteVoxelShape.isFullWide(j, i, l)) { -- if (coalesce) { -- if (k == -1) { -- k = l; -- } -- } else { -- callback.consume(j, i, l, j + 1, i + 1, l + 1); -+ for (int y = 0; y < sizeY; ++y, indexY += incY) { -+ indexX = indexY; -+ for (int x = 0; x < sizeX; ++x, indexX += incX) { -+ for (int zIdx = indexX, endIndex = indexX + sizeZ; zIdx < endIndex;) { -+ final int firstSetZ = io.papermc.paper.util.collisions.FlatBitsetUtil.firstSet(bitset, zIdx, endIndex); -+ -+ if (firstSetZ == -1) { -+ break; -+ } -+ -+ int lastSetZ = io.papermc.paper.util.collisions.FlatBitsetUtil.firstClear(bitset, firstSetZ, endIndex); -+ if (lastSetZ == -1) { -+ lastSetZ = endIndex; - } -- } else if (k != -1) { -- int m = j; -- int n = i; -- bitSetDiscreteVoxelShape.clearZStrip(k, l, j, i); -- -- while (bitSetDiscreteVoxelShape.isZStripFull(k, l, m + 1, i)) { -- bitSetDiscreteVoxelShape.clearZStrip(k, l, m + 1, i); -- m++; -+ -+ io.papermc.paper.util.collisions.FlatBitsetUtil.clearRange(bitset, firstSetZ, lastSetZ); -+ -+ // try to merge neighbouring on the X axis -+ int endX = x + 1; // exclusive -+ for (int neighbourIdxStart = firstSetZ + incX, neighbourIdxEnd = lastSetZ + incX; -+ endX < sizeX && io.papermc.paper.util.collisions.FlatBitsetUtil.isRangeSet(bitset, neighbourIdxStart, neighbourIdxEnd); -+ neighbourIdxStart += incX, neighbourIdxEnd += incX) { -+ -+ ++endX; -+ io.papermc.paper.util.collisions.FlatBitsetUtil.clearRange(bitset, neighbourIdxStart, neighbourIdxEnd); - } - -- while (bitSetDiscreteVoxelShape.isXZRectangleFull(j, m + 1, k, l, n + 1)) { -- for (int o = j; o <= m; o++) { -- bitSetDiscreteVoxelShape.clearZStrip(k, l, o, n + 1); -+ // try to merge neighbouring on the Y axis -+ -+ int endY; // exclusive -+ int firstSetZY, lastSetZY; -+ y_merge: -+ for (endY = y + 1, firstSetZY = firstSetZ + incY, lastSetZY = lastSetZ + incY; endY < sizeY; -+ firstSetZY += incY, lastSetZY += incY) { -+ -+ // test the whole XZ range -+ for (int testX = x, start = firstSetZY, end = lastSetZY; testX < endX; -+ ++testX, start += incX, end += incX) { -+ if (!io.papermc.paper.util.collisions.FlatBitsetUtil.isRangeSet(bitset, start, end)) { -+ break y_merge; -+ } - } - -- n++; -+ ++endY; -+ -+ // passed, so we can clear it -+ for (int testX = x, start = firstSetZY, end = lastSetZY; testX < endX; -+ ++testX, start += incX, end += incX) { -+ io.papermc.paper.util.collisions.FlatBitsetUtil.clearRange(bitset, start, end); -+ } - } - -- callback.consume(j, i, k, m + 1, n + 1, l); -- k = -1; -+ callback.consume(x, y, firstSetZ - indexX, endX, endY, lastSetZ - indexX); -+ zIdx = lastSetZ; - } - } - } - } -+ // Paper end - optimise collisions - } - - private boolean isZStripFull(int z1, int z2, int x, int y) { -diff --git a/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java -index 32632368f06b79f53342fde060bbcd1b7c64767a..b9af1d14c7815c99273bce8165cf384d669c1a75 100644 ---- a/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java -+++ b/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java -@@ -7,6 +7,7 @@ import net.minecraft.util.Mth; - public final class CubeVoxelShape extends VoxelShape { - protected CubeVoxelShape(DiscreteVoxelShape voxels) { - super(voxels); -+ this.initCache(); // Paper - optimise collisions - } - - @Override -diff --git a/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java -index 01693ba050b12b9debcdaefceeff9cbcd503b369..a7af766f1c28f2c9ca2a430808ac4c07fe24df68 100644 ---- a/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java -+++ b/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java -@@ -9,6 +9,71 @@ public abstract class DiscreteVoxelShape { - protected final int ySize; - protected final int zSize; - -+ // Paper start - optimise collisions -+ private io.papermc.paper.util.collisions.CachedShapeData cachedShapeData; -+ -+ public final io.papermc.paper.util.collisions.CachedShapeData getOrCreateCachedShapeData() { -+ if (this.cachedShapeData != null) { -+ return this.cachedShapeData; -+ } -+ -+ final DiscreteVoxelShape discreteVoxelShape = (DiscreteVoxelShape)(Object)this; -+ -+ final int sizeX = discreteVoxelShape.getXSize(); -+ final int sizeY = discreteVoxelShape.getYSize(); -+ final int sizeZ = discreteVoxelShape.getZSize(); -+ -+ final int maxIndex = sizeX * sizeY * sizeZ; // exclusive -+ -+ final int longsRequired = (maxIndex + (Long.SIZE - 1)) >>> 6; -+ long[] voxelSet; -+ -+ final boolean isEmpty = discreteVoxelShape.isEmpty(); -+ -+ if (discreteVoxelShape instanceof BitSetDiscreteVoxelShape bitsetShape) { -+ voxelSet = bitsetShape.storage.toLongArray(); -+ if (voxelSet.length < longsRequired) { -+ // happens when the later long values are 0L, so we need to resize -+ voxelSet = java.util.Arrays.copyOf(voxelSet, longsRequired); -+ } -+ } else { -+ voxelSet = new long[longsRequired]; -+ if (!isEmpty) { -+ final int mulX = sizeZ * sizeY; -+ for (int x = 0; x < sizeX; ++x) { -+ for (int y = 0; y < sizeY; ++y) { -+ for (int z = 0; z < sizeZ; ++z) { -+ if (discreteVoxelShape.isFull(x, y, z)) { -+ // index = z + y*size_z + x*(size_z*size_y) -+ final int index = z + y * sizeZ + x * mulX; -+ -+ voxelSet[index >>> 6] |= 1L << index; -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ final boolean hasSingleAABB = sizeX == 1 && sizeY == 1 && sizeZ == 1 && !isEmpty && discreteVoxelShape.isFull(0, 0, 0); -+ -+ final int minFullX = discreteVoxelShape.firstFull(Direction.Axis.X); -+ final int minFullY = discreteVoxelShape.firstFull(Direction.Axis.Y); -+ final int minFullZ = discreteVoxelShape.firstFull(Direction.Axis.Z); -+ -+ final int maxFullX = discreteVoxelShape.lastFull(Direction.Axis.X); -+ final int maxFullY = discreteVoxelShape.lastFull(Direction.Axis.Y); -+ final int maxFullZ = discreteVoxelShape.lastFull(Direction.Axis.Z); -+ -+ return this.cachedShapeData = new io.papermc.paper.util.collisions.CachedShapeData( -+ sizeX, sizeY, sizeZ, voxelSet, -+ minFullX, minFullY, minFullZ, -+ maxFullX, maxFullY, maxFullZ, -+ isEmpty, hasSingleAABB -+ ); -+ } -+ // Paper end - optimise collisions -+ - protected DiscreteVoxelShape(int sizeX, int sizeY, int sizeZ) { - if (sizeX >= 0 && sizeY >= 0 && sizeZ >= 0) { - this.xSize = sizeX; -diff --git a/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java b/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java -index 7ec02a7849437a18860aa0df7d9ddd71b2447d4c..5e45e49ab09344cb95736f4124b1c6e002ef5b82 100644 ---- a/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java -+++ b/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java -@@ -4,8 +4,8 @@ import it.unimi.dsi.fastutil.doubles.AbstractDoubleList; - import it.unimi.dsi.fastutil.doubles.DoubleList; - - public class OffsetDoubleList extends AbstractDoubleList { -- private final DoubleList delegate; -- private final double offset; -+ public final DoubleList delegate; // Paper - optimise collisions - public -+ public final double offset; // Paper - optimise collisions - public - - public OffsetDoubleList(DoubleList oldList, double offset) { - this.delegate = oldList; -diff --git a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java -index 86df4ef44d0a5107ee929dfd40d8ccb0779e8bfc..fbf1a559aefe444410b63a773374e011e4964e16 100644 ---- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java -+++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java -@@ -16,9 +16,15 @@ public final class Shapes { - public static final double EPSILON = 1.0E-7; - public static final double BIG_EPSILON = 1.0E-6; - private static final VoxelShape BLOCK = Util.make(() -> { -- DiscreteVoxelShape discreteVoxelShape = new BitSetDiscreteVoxelShape(1, 1, 1); -- discreteVoxelShape.fill(0, 0, 0); -- return new CubeVoxelShape(discreteVoxelShape); -+ // Paper start - optimise collisions - force arrayvoxelshape -+ final DiscreteVoxelShape shape = new BitSetDiscreteVoxelShape(1, 1, 1); -+ shape.fill(0, 0, 0); -+ -+ return new ArrayVoxelShape( -+ shape, -+ io.papermc.paper.util.CollisionUtil.ZERO_ONE, io.papermc.paper.util.CollisionUtil.ZERO_ONE, io.papermc.paper.util.CollisionUtil.ZERO_ONE -+ ); -+ // Paper end - optimise collisions - force arrayvoxelshape - }); - public static final VoxelShape INFINITY = box( - Double.NEGATIVE_INFINITY, -@@ -35,6 +41,30 @@ public final class Shapes { - new DoubleArrayList(new double[]{0.0}) - ); - -+ // Paper start - optimise collisions - force arrayvoxelshape -+ private static final DoubleArrayList[] PARTS_BY_BITS = new DoubleArrayList[] { -+ DoubleArrayList.wrap(generateCubeParts(1 << 0)), -+ DoubleArrayList.wrap(generateCubeParts(1 << 1)), -+ DoubleArrayList.wrap(generateCubeParts(1 << 2)), -+ DoubleArrayList.wrap(generateCubeParts(1 << 3)) -+ }; -+ -+ private static double[] generateCubeParts(final int parts) { -+ // note: parts is a power of two, so we do not need to worry about loss of precision here -+ // note: parts is from [2^0, 2^3] -+ final double inc = 1.0 / (double)parts; -+ -+ final double[] ret = new double[parts + 1]; -+ double val = 0.0; -+ for (int i = 0; i <= parts; ++i) { -+ ret[i] = val; -+ val += inc; -+ } -+ -+ return ret; -+ } -+ // Paper end - optimise collisions - force arrayvoxelshape -+ - public static VoxelShape empty() { - return EMPTY; - } -@@ -53,35 +83,39 @@ public final class Shapes { - - public static VoxelShape create(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { - if (!(maxX - minX < 1.0E-7) && !(maxY - minY < 1.0E-7) && !(maxZ - minZ < 1.0E-7)) { -- int i = findBits(minX, maxX); -- int j = findBits(minY, maxY); -- int k = findBits(minZ, maxZ); -- if (i < 0 || j < 0 || k < 0) { -- return new ArrayVoxelShape( -- BLOCK.shape, -- DoubleArrayList.wrap(new double[]{minX, maxX}), -- DoubleArrayList.wrap(new double[]{minY, maxY}), -- DoubleArrayList.wrap(new double[]{minZ, maxZ}) -- ); -- } else if (i == 0 && j == 0 && k == 0) { -- return block(); -+ // Paper start - optimise collisions -+ // force ArrayVoxelShape in every case -+ final int bitsX = findBits(minX, maxX); -+ final int bitsY = findBits(minY, maxY); -+ final int bitsZ = findBits(minZ, maxZ); -+ if (bitsX >= 0 && bitsY >= 0 && bitsZ >= 0) { -+ if (bitsX == 0 && bitsY == 0 && bitsZ == 0) { -+ return BLOCK; -+ } else { -+ final int sizeX = 1 << bitsX; -+ final int sizeY = 1 << bitsY; -+ final int sizeZ = 1 << bitsZ; -+ final BitSetDiscreteVoxelShape shape = BitSetDiscreteVoxelShape.withFilledBounds( -+ sizeX, sizeY, sizeZ, -+ (int)Math.round(minX * (double)sizeX), (int)Math.round(minY * (double)sizeY), (int)Math.round(minZ * (double)sizeZ), -+ (int)Math.round(maxX * (double)sizeX), (int)Math.round(maxY * (double)sizeY), (int)Math.round(maxZ * (double)sizeZ) -+ ); -+ return new ArrayVoxelShape( -+ shape, -+ PARTS_BY_BITS[bitsX], -+ PARTS_BY_BITS[bitsY], -+ PARTS_BY_BITS[bitsZ] -+ ); -+ } - } else { -- int l = 1 << i; -- int m = 1 << j; -- int n = 1 << k; -- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.withFilledBounds( -- l, -- m, -- n, -- (int)Math.round(minX * (double)l), -- (int)Math.round(minY * (double)m), -- (int)Math.round(minZ * (double)n), -- (int)Math.round(maxX * (double)l), -- (int)Math.round(maxY * (double)m), -- (int)Math.round(maxZ * (double)n) -+ return new ArrayVoxelShape( -+ BLOCK.shape, -+ minX == 0.0 && maxX == 1.0 ? io.papermc.paper.util.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minX, maxX }), -+ minY == 0.0 && maxY == 1.0 ? io.papermc.paper.util.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minY, maxY }), -+ minZ == 0.0 && maxZ == 1.0 ? io.papermc.paper.util.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minZ, maxZ }) - ); -- return new CubeVoxelShape(bitSetDiscreteVoxelShape); - } -+ // Paper end - optimise collisions - } else { - return empty(); - } -@@ -120,79 +154,53 @@ public final class Shapes { - } - - public static VoxelShape or(VoxelShape first, VoxelShape... others) { -- return Arrays.stream(others).reduce(first, Shapes::or); -+ // Paper start - optimise collisions -+ int size = others.length; -+ if (size == 0) { -+ return first; -+ } -+ -+ // reduce complexity of joins by splitting the merges -+ -+ // add extra slot for first shape -+ ++size; -+ final VoxelShape[] tmp = Arrays.copyOf(others, size); -+ // insert first shape -+ tmp[size - 1] = first; -+ -+ while (size > 1) { -+ int newSize = 0; -+ for (int i = 0; i < size; i += 2) { -+ final int next = i + 1; -+ if (next >= size) { -+ // nothing to merge with, so leave it for next iteration -+ tmp[newSize++] = tmp[i]; -+ break; -+ } else { -+ // merge with adjacent -+ final VoxelShape one = tmp[i]; -+ final VoxelShape second = tmp[next]; -+ -+ tmp[newSize++] = Shapes.or(one, second); -+ } -+ } -+ size = newSize; -+ } -+ -+ return tmp[0]; -+ // Paper end - optimise collisions - } - - public static VoxelShape join(VoxelShape first, VoxelShape second, BooleanOp function) { -- return joinUnoptimized(first, second, function).optimize(); -+ return io.papermc.paper.util.CollisionUtil.joinOptimized(first, second, function); // Paper - optimise collisions - } - - public static VoxelShape joinUnoptimized(VoxelShape one, VoxelShape two, BooleanOp function) { -- if (function.apply(false, false)) { -- throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException()); -- } else if (one == two) { -- return function.apply(true, true) ? one : empty(); -- } else { -- boolean bl = function.apply(true, false); -- boolean bl2 = function.apply(false, true); -- if (one.isEmpty()) { -- return bl2 ? two : empty(); -- } else if (two.isEmpty()) { -- return bl ? one : empty(); -- } else { -- IndexMerger indexMerger = createIndexMerger(1, one.getCoords(Direction.Axis.X), two.getCoords(Direction.Axis.X), bl, bl2); -- IndexMerger indexMerger2 = createIndexMerger(indexMerger.size() - 1, one.getCoords(Direction.Axis.Y), two.getCoords(Direction.Axis.Y), bl, bl2); -- IndexMerger indexMerger3 = createIndexMerger( -- (indexMerger.size() - 1) * (indexMerger2.size() - 1), one.getCoords(Direction.Axis.Z), two.getCoords(Direction.Axis.Z), bl, bl2 -- ); -- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.join( -- one.shape, two.shape, indexMerger, indexMerger2, indexMerger3, function -- ); -- return (VoxelShape)(indexMerger instanceof DiscreteCubeMerger -- && indexMerger2 instanceof DiscreteCubeMerger -- && indexMerger3 instanceof DiscreteCubeMerger -- ? new CubeVoxelShape(bitSetDiscreteVoxelShape) -- : new ArrayVoxelShape(bitSetDiscreteVoxelShape, indexMerger.getList(), indexMerger2.getList(), indexMerger3.getList())); -- } -- } -+ return io.papermc.paper.util.CollisionUtil.joinUnoptimized(one, two, function); // Paper - optimise collisions - } - - public static boolean joinIsNotEmpty(VoxelShape shape1, VoxelShape shape2, BooleanOp predicate) { -- if (predicate.apply(false, false)) { -- throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException()); -- } else { -- boolean bl = shape1.isEmpty(); -- boolean bl2 = shape2.isEmpty(); -- if (!bl && !bl2) { -- if (shape1 == shape2) { -- return predicate.apply(true, true); -- } else { -- boolean bl3 = predicate.apply(true, false); -- boolean bl4 = predicate.apply(false, true); -- -- for (Direction.Axis axis : AxisCycle.AXIS_VALUES) { -- if (shape1.max(axis) < shape2.min(axis) - 1.0E-7) { -- return bl3 || bl4; -- } -- -- if (shape2.max(axis) < shape1.min(axis) - 1.0E-7) { -- return bl3 || bl4; -- } -- } -- -- IndexMerger indexMerger = createIndexMerger(1, shape1.getCoords(Direction.Axis.X), shape2.getCoords(Direction.Axis.X), bl3, bl4); -- IndexMerger indexMerger2 = createIndexMerger( -- indexMerger.size() - 1, shape1.getCoords(Direction.Axis.Y), shape2.getCoords(Direction.Axis.Y), bl3, bl4 -- ); -- IndexMerger indexMerger3 = createIndexMerger( -- (indexMerger.size() - 1) * (indexMerger2.size() - 1), shape1.getCoords(Direction.Axis.Z), shape2.getCoords(Direction.Axis.Z), bl3, bl4 -- ); -- return joinIsNotEmpty(indexMerger, indexMerger2, indexMerger3, shape1.shape, shape2.shape, predicate); -- } -- } else { -- return predicate.apply(!bl, !bl2); -- } -- } -+ return io.papermc.paper.util.CollisionUtil.isJoinNonEmpty(shape1, shape2, predicate); // Paper - optimise collisions - } - - private static boolean joinIsNotEmpty( -@@ -220,69 +228,119 @@ public final class Shapes { - } - - public static boolean blockOccudes(VoxelShape shape, VoxelShape neighbor, Direction direction) { -- if (shape == block() && neighbor == block()) { -+ // Paper start - optimise collisions -+ final boolean firstBlock = shape == BLOCK; -+ final boolean secondBlock = neighbor == BLOCK; -+ -+ if (firstBlock & secondBlock) { - return true; -- } else if (neighbor.isEmpty()) { -+ } -+ -+ if (shape.isEmpty() | neighbor.isEmpty()) { -+ return false; -+ } -+ -+ // we optimise getOpposite, so we can use it -+ // secondly, use our cache to retrieve sliced shape -+ final VoxelShape newFirst = shape.getFaceShapeClamped(direction); -+ if (newFirst.isEmpty()) { - return false; -- } else { -- Direction.Axis axis = direction.getAxis(); -- Direction.AxisDirection axisDirection = direction.getAxisDirection(); -- VoxelShape voxelShape = axisDirection == Direction.AxisDirection.POSITIVE ? shape : neighbor; -- VoxelShape voxelShape2 = axisDirection == Direction.AxisDirection.POSITIVE ? neighbor : shape; -- BooleanOp booleanOp = axisDirection == Direction.AxisDirection.POSITIVE ? BooleanOp.ONLY_FIRST : BooleanOp.ONLY_SECOND; -- return DoubleMath.fuzzyEquals(voxelShape.max(axis), 1.0, 1.0E-7) -- && DoubleMath.fuzzyEquals(voxelShape2.min(axis), 0.0, 1.0E-7) -- && !joinIsNotEmpty(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape2, axis, 0), booleanOp); - } -+ final VoxelShape newSecond = neighbor.getFaceShapeClamped(direction.getOpposite()); -+ if (newSecond.isEmpty()) { -+ return false; -+ } -+ -+ return !joinIsNotEmpty(newFirst, newSecond, BooleanOp.ONLY_FIRST); -+ // Paper end - optimise collisions - } - - public static VoxelShape getFaceShape(VoxelShape shape, Direction direction) { -- if (shape == block()) { -- return block(); -- } else { -- Direction.Axis axis = direction.getAxis(); -- boolean bl; -- int i; -- if (direction.getAxisDirection() == Direction.AxisDirection.POSITIVE) { -- bl = DoubleMath.fuzzyEquals(shape.max(axis), 1.0, 1.0E-7); -- i = shape.shape.getSize(axis) - 1; -- } else { -- bl = DoubleMath.fuzzyEquals(shape.min(axis), 0.0, 1.0E-7); -- i = 0; -- } -+ return shape.getFaceShapeClamped(direction); // Paper - optimise collisions -+ } - -- return (VoxelShape)(!bl ? empty() : new SliceShape(shape, axis, i)); -- } -+ // Paper start - optimise collisions -+ private static boolean mergedMayOccludeBlock(final VoxelShape shape1, final VoxelShape shape2) { -+ // if the combined bounds of the two shapes cannot occlude, then neither can the merged -+ final AABB bounds1 = shape1.bounds(); -+ final AABB bounds2 = shape2.bounds(); -+ -+ final double minX = Math.min(bounds1.minX, bounds2.minX); -+ final double minY = Math.min(bounds1.minY, bounds2.minY); -+ final double minZ = Math.min(bounds1.minZ, bounds2.minZ); -+ -+ final double maxX = Math.max(bounds1.maxX, bounds2.maxX); -+ final double maxY = Math.max(bounds1.maxY, bounds2.maxY); -+ final double maxZ = Math.max(bounds1.maxZ, bounds2.maxZ); -+ -+ return (minX <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && maxX >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) && -+ (minY <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && maxY >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) && -+ (minZ <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && maxZ >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)); - } -+ // Paper end - optimise collisions - - public static boolean mergedFaceOccludes(VoxelShape one, VoxelShape two, Direction direction) { -- if (one != block() && two != block()) { -- Direction.Axis axis = direction.getAxis(); -- Direction.AxisDirection axisDirection = direction.getAxisDirection(); -- VoxelShape voxelShape = axisDirection == Direction.AxisDirection.POSITIVE ? one : two; -- VoxelShape voxelShape2 = axisDirection == Direction.AxisDirection.POSITIVE ? two : one; -- if (!DoubleMath.fuzzyEquals(voxelShape.max(axis), 1.0, 1.0E-7)) { -- voxelShape = empty(); -- } -+ // Paper start - optimise collisions -+ // see if any of the shapes on their own occludes, only if cached -+ if (one.occludesFullBlockIfCached() || two.occludesFullBlockIfCached()) { -+ return true; -+ } - -- if (!DoubleMath.fuzzyEquals(voxelShape2.min(axis), 0.0, 1.0E-7)) { -- voxelShape2 = empty(); -- } -+ if (one.isEmpty() & two.isEmpty()) { -+ return false; -+ } - -- return !joinIsNotEmpty( -- block(), -- joinUnoptimized(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape2, axis, 0), BooleanOp.OR), -- BooleanOp.ONLY_FIRST -- ); -- } else { -+ // we optimise getOpposite, so we can use it -+ // secondly, use our cache to retrieve sliced shape -+ final VoxelShape newFirst = one.getFaceShapeClamped(direction); -+ final VoxelShape newSecond = two.getFaceShapeClamped(direction.getOpposite()); -+ -+ // see if any of the shapes on their own occludes, only if cached -+ if (newFirst.occludesFullBlockIfCached() || newSecond.occludesFullBlockIfCached()) { - return true; - } -+ -+ final boolean firstEmpty = newFirst.isEmpty(); -+ final boolean secondEmpty = newSecond.isEmpty(); -+ -+ if (firstEmpty & secondEmpty) { -+ return false; -+ } -+ -+ if (firstEmpty | secondEmpty) { -+ return secondEmpty ? newFirst.occludesFullBlock() : newSecond.occludesFullBlock(); -+ } -+ -+ if (newFirst == newSecond) { -+ return newFirst.occludesFullBlock(); -+ } -+ -+ return mergedMayOccludeBlock(newFirst, newSecond) && newFirst.orUnoptimized(newSecond).occludesFullBlock(); -+ // Paper end - optimise collisions - } - - public static boolean faceShapeOccludes(VoxelShape one, VoxelShape two) { -- return one == block() -- || two == block() -- || (!one.isEmpty() || !two.isEmpty()) && !joinIsNotEmpty(block(), joinUnoptimized(one, two, BooleanOp.OR), BooleanOp.ONLY_FIRST); -+ // Paper start - optimise collisions -+ if (one.occludesFullBlockIfCached() || two.occludesFullBlockIfCached()) { -+ return true; -+ } -+ -+ final boolean s1Empty = one.isEmpty(); -+ final boolean s2Empty = two.isEmpty(); -+ if (s1Empty & s2Empty) { -+ return false; -+ } -+ -+ if (s1Empty | s2Empty) { -+ return s2Empty ? one.occludesFullBlock() : two.occludesFullBlock(); -+ } -+ -+ if (one == two) { -+ return one.occludesFullBlock(); -+ } -+ -+ return mergedMayOccludeBlock(one, two) && (one.orUnoptimized(two)).occludesFullBlock(); -+ // Paper end - optimise collisions - } - - @VisibleForTesting -diff --git a/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java b/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java -index 53aa193f33a1a15376a59b8d6dd8cbc6cbec168b..a745ff8d115e1d0da6138e4f06726e0737bb1600 100644 ---- a/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java -+++ b/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java -@@ -12,6 +12,7 @@ public class SliceShape extends VoxelShape { - super(makeSlice(shape.shape, axis, sliceWidth)); - this.delegate = shape; - this.axis = axis; -+ this.initCache(); // Paper - optimise collisions - } - - private static DiscreteVoxelShape makeSlice(DiscreteVoxelShape voxelSet, Direction.Axis axis, int sliceWidth) { -diff --git a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java -index 2936c56e5690b42518010698e5177755422e4c5d..e6b17f32f2b6930739a98c6139442383c1847add 100644 ---- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java -+++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java -@@ -16,37 +16,438 @@ import net.minecraft.world.phys.BlockHitResult; - import net.minecraft.world.phys.Vec3; - - public abstract class VoxelShape { -- protected final DiscreteVoxelShape shape; -+ public final DiscreteVoxelShape shape; // Paper - optimise collisions - public - @Nullable - private VoxelShape[] faces; - -- VoxelShape(DiscreteVoxelShape voxels) { -+ // Paper start - optimise collisions -+ private double offsetX; -+ private double offsetY; -+ private double offsetZ; -+ @Nullable private AABB singleAABBRepresentation; -+ private double[] rootCoordinatesX; -+ private double[] rootCoordinatesY; -+ private double[] rootCoordinatesZ; -+ -+ private io.papermc.paper.util.collisions.CachedShapeData cachedShapeData; -+ private boolean isEmpty; -+ -+ private io.papermc.paper.util.collisions.CachedToAABBs cachedToAABBs; -+ private AABB cachedBounds; -+ -+ private Boolean isFullBlock; -+ -+ private Boolean occludesFullBlock; -+ -+ // must be power of two -+ private static final int MERGED_CACHE_SIZE = 16; -+ -+ private io.papermc.paper.util.collisions.MergedORCache[] mergedORCache; -+ -+ public final double offsetX() { -+ return this.offsetX; -+ } -+ -+ public final double offsetY() { -+ return this.offsetY; -+ } -+ -+ public final double offsetZ() { -+ return this.offsetZ; -+ } -+ -+ public final AABB getSingleAABBRepresentation() { -+ return this.singleAABBRepresentation; -+ } -+ -+ public final double[] rootCoordinatesX() { -+ return this.rootCoordinatesX; -+ } -+ -+ public final double[] rootCoordinatesY() { -+ return this.rootCoordinatesY; -+ } -+ -+ public final double[] rootCoordinatesZ() { -+ return this.rootCoordinatesZ; -+ } -+ -+ private static double[] extractRawArray(final DoubleList list) { -+ if (list instanceof it.unimi.dsi.fastutil.doubles.DoubleArrayList rawList) { -+ final double[] raw = rawList.elements(); -+ final int expected = rawList.size(); -+ if (raw.length == expected) { -+ return raw; -+ } else { -+ return java.util.Arrays.copyOf(raw, expected); -+ } -+ } else { -+ return list.toDoubleArray(); -+ } -+ } -+ -+ public final void initCache() { -+ this.cachedShapeData = this.shape.getOrCreateCachedShapeData(); -+ this.isEmpty = this.cachedShapeData.isEmpty(); -+ -+ final DoubleList xList = this.getCoords(Direction.Axis.X); -+ final DoubleList yList = this.getCoords(Direction.Axis.Y); -+ final DoubleList zList = this.getCoords(Direction.Axis.Z); -+ -+ if (xList instanceof OffsetDoubleList offsetDoubleList) { -+ this.offsetX = offsetDoubleList.offset; -+ this.rootCoordinatesX = extractRawArray(offsetDoubleList.delegate); -+ } else { -+ this.rootCoordinatesX = extractRawArray(xList); -+ } -+ -+ if (yList instanceof OffsetDoubleList offsetDoubleList) { -+ this.offsetY = offsetDoubleList.offset; -+ this.rootCoordinatesY = extractRawArray(offsetDoubleList.delegate); -+ } else { -+ this.rootCoordinatesY = extractRawArray(yList); -+ } -+ -+ if (zList instanceof OffsetDoubleList offsetDoubleList) { -+ this.offsetZ = offsetDoubleList.offset; -+ this.rootCoordinatesZ = extractRawArray(offsetDoubleList.delegate); -+ } else { -+ this.rootCoordinatesZ = extractRawArray(zList); -+ } -+ -+ if (this.cachedShapeData.hasSingleAABB()) { -+ this.singleAABBRepresentation = new AABB( -+ this.rootCoordinatesX[0] + this.offsetX, this.rootCoordinatesY[0] + this.offsetY, this.rootCoordinatesZ[0] + this.offsetZ, -+ this.rootCoordinatesX[1] + this.offsetX, this.rootCoordinatesY[1] + this.offsetY, this.rootCoordinatesZ[1] + this.offsetZ -+ ); -+ this.cachedBounds = this.singleAABBRepresentation; -+ } -+ } -+ -+ public final io.papermc.paper.util.collisions.CachedShapeData getCachedVoxelData() { -+ return this.cachedShapeData; -+ } -+ -+ private VoxelShape[] faceShapeClampedCache; -+ -+ public final VoxelShape getFaceShapeClamped(final Direction direction) { -+ if (this.isEmpty) { -+ return (VoxelShape)(Object)this; -+ } -+ if ((VoxelShape)(Object)this == Shapes.block()) { -+ return (VoxelShape)(Object)this; -+ } -+ -+ VoxelShape[] cache = this.faceShapeClampedCache; -+ if (cache != null) { -+ final VoxelShape ret = cache[direction.ordinal()]; -+ if (ret != null) { -+ return ret; -+ } -+ } -+ -+ -+ if (cache == null) { -+ this.faceShapeClampedCache = cache = new VoxelShape[6]; -+ } -+ -+ final Direction.Axis axis = direction.getAxis(); -+ -+ final VoxelShape ret; -+ -+ if (direction.getAxisDirection() == Direction.AxisDirection.POSITIVE) { -+ if (DoubleMath.fuzzyEquals(this.max(axis), 1.0, io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) { -+ ret = tryForceBlock(new SliceShape((VoxelShape)(Object)this, axis, this.shape.getSize(axis) - 1)); -+ } else { -+ ret = Shapes.empty(); -+ } -+ } else { -+ if (DoubleMath.fuzzyEquals(this.min(axis), 0.0, io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) { -+ ret = tryForceBlock(new SliceShape((VoxelShape)(Object)this, axis, 0)); -+ } else { -+ ret = Shapes.empty(); -+ } -+ } -+ -+ cache[direction.ordinal()] = ret; -+ -+ return ret; -+ } -+ -+ private static VoxelShape tryForceBlock(final VoxelShape other) { -+ if (other == Shapes.block()) { -+ return other; -+ } -+ -+ final AABB otherAABB = other.getSingleAABBRepresentation(); -+ if (otherAABB == null) { -+ return other; -+ } -+ -+ if (Shapes.block().getSingleAABBRepresentation().equals(otherAABB)) { -+ return Shapes.block(); -+ } -+ -+ return other; -+ } -+ -+ private boolean computeOccludesFullBlock() { -+ if (this.isEmpty) { -+ this.occludesFullBlock = Boolean.FALSE; -+ return false; -+ } -+ -+ if (this.isFullBlock()) { -+ this.occludesFullBlock = Boolean.TRUE; -+ return true; -+ } -+ -+ final AABB singleAABB = this.singleAABBRepresentation; -+ if (singleAABB != null) { -+ // check if the bounding box encloses the full cube -+ final boolean ret = -+ (singleAABB.minY <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && singleAABB.maxY >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) && -+ (singleAABB.minX <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && singleAABB.maxX >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) && -+ (singleAABB.minZ <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && singleAABB.maxZ >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)); -+ this.occludesFullBlock = Boolean.valueOf(ret); -+ return ret; -+ } -+ -+ final boolean ret = !Shapes.joinIsNotEmpty(Shapes.block(), ((VoxelShape)(Object)this), BooleanOp.ONLY_FIRST); -+ this.occludesFullBlock = Boolean.valueOf(ret); -+ return ret; -+ } -+ -+ public final boolean occludesFullBlock() { -+ final Boolean ret = this.occludesFullBlock; -+ if (ret != null) { -+ return ret.booleanValue(); -+ } -+ -+ return this.computeOccludesFullBlock(); -+ } -+ -+ public final boolean occludesFullBlockIfCached() { -+ final Boolean ret = this.occludesFullBlock; -+ return ret != null ? ret.booleanValue() : false; -+ } -+ -+ private static int hash(final VoxelShape key) { -+ return it.unimi.dsi.fastutil.HashCommon.mix(System.identityHashCode(key)); -+ } -+ -+ public final VoxelShape orUnoptimized(final VoxelShape other) { -+ // don't cache simple cases -+ if (((VoxelShape)(Object)this) == other) { -+ return other; -+ } -+ -+ if (this.isEmpty) { -+ return other; -+ } -+ -+ if (other.isEmpty()) { -+ return (VoxelShape)(Object)this; -+ } -+ -+ // try this cache first -+ final int thisCacheKey = hash(other) & (MERGED_CACHE_SIZE - 1); -+ final io.papermc.paper.util.collisions.MergedORCache cached = this.mergedORCache == null ? null : this.mergedORCache[thisCacheKey]; -+ if (cached != null && cached.key() == other) { -+ return cached.result(); -+ } -+ -+ // try other cache -+ final int otherCacheKey = hash(this) & (MERGED_CACHE_SIZE - 1); -+ final io.papermc.paper.util.collisions.MergedORCache otherCache = other.mergedORCache == null ? null : other.mergedORCache[otherCacheKey]; -+ if (otherCache != null && otherCache.key() == this) { -+ return otherCache.result(); -+ } -+ -+ // note: unsure if joinUnoptimized(1, 2, OR) == joinUnoptimized(2, 1, OR) for all cases -+ final VoxelShape result = Shapes.joinUnoptimized(this, other, BooleanOp.OR); -+ -+ if (cached != null && otherCache == null) { -+ // try to use second cache instead of replacing an entry in this cache -+ if (other.mergedORCache == null) { -+ other.mergedORCache = new io.papermc.paper.util.collisions.MergedORCache[MERGED_CACHE_SIZE]; -+ } -+ other.mergedORCache[otherCacheKey] = new io.papermc.paper.util.collisions.MergedORCache(this, result); -+ } else { -+ // line is not occupied or other cache line is full -+ // always bias to replace this cache, as this cache is the first we check -+ if (this.mergedORCache == null) { -+ this.mergedORCache = new io.papermc.paper.util.collisions.MergedORCache[MERGED_CACHE_SIZE]; -+ } -+ this.mergedORCache[thisCacheKey] = new io.papermc.paper.util.collisions.MergedORCache(other, result); -+ } -+ -+ return result; -+ } -+ -+ private boolean computeFullBlock() { -+ Boolean ret; -+ if (this.isEmpty) { -+ ret = Boolean.FALSE; -+ } else if ((VoxelShape)(Object)this == Shapes.block()) { -+ ret = Boolean.TRUE; -+ } else { -+ final AABB singleAABB = this.singleAABBRepresentation; -+ if (singleAABB == null) { -+ final io.papermc.paper.util.collisions.CachedShapeData shapeData = this.cachedShapeData; -+ final int sMinX = shapeData.minFullX(); -+ final int sMinY = shapeData.minFullY(); -+ final int sMinZ = shapeData.minFullZ(); -+ -+ final int sMaxX = shapeData.maxFullX(); -+ final int sMaxY = shapeData.maxFullY(); -+ final int sMaxZ = shapeData.maxFullZ(); -+ -+ if (Math.abs(this.rootCoordinatesX[sMinX] + this.offsetX) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && -+ Math.abs(this.rootCoordinatesY[sMinY] + this.offsetY) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && -+ Math.abs(this.rootCoordinatesZ[sMinZ] + this.offsetZ) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && -+ -+ Math.abs(1.0 - (this.rootCoordinatesX[sMaxX] + this.offsetX)) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && -+ Math.abs(1.0 - (this.rootCoordinatesY[sMaxY] + this.offsetY)) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && -+ Math.abs(1.0 - (this.rootCoordinatesZ[sMaxZ] + this.offsetZ)) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) { -+ -+ // index = z + y*sizeZ + x*(sizeZ*sizeY) -+ -+ final int sizeY = shapeData.sizeY(); -+ final int sizeZ = shapeData.sizeZ(); -+ -+ final long[] bitset = shapeData.voxelSet(); -+ -+ ret = Boolean.TRUE; -+ -+ check_full: -+ for (int x = sMinX; x < sMaxX; ++x) { -+ for (int y = sMinY; y < sMaxY; ++y) { -+ final int baseIndex = y*sizeZ + x*(sizeZ*sizeY); -+ if (!io.papermc.paper.util.collisions.FlatBitsetUtil.isRangeSet(bitset, baseIndex + sMinZ, baseIndex + sMaxZ)) { -+ ret = Boolean.FALSE; -+ break check_full; -+ } -+ } -+ } -+ } else { -+ ret = Boolean.FALSE; -+ } -+ } else { -+ ret = Boolean.valueOf( -+ Math.abs(singleAABB.minX) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && -+ Math.abs(singleAABB.minY) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && -+ Math.abs(singleAABB.minZ) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && -+ -+ Math.abs(1.0 - singleAABB.maxX) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && -+ Math.abs(1.0 - singleAABB.maxY) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && -+ Math.abs(1.0 - singleAABB.maxZ) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON -+ ); -+ } -+ } -+ -+ this.isFullBlock = ret; -+ -+ return ret.booleanValue(); -+ } -+ -+ public boolean isFullBlock() { -+ final Boolean ret = this.isFullBlock; -+ -+ if (ret != null) { -+ return ret.booleanValue(); -+ } -+ -+ return this.computeFullBlock(); -+ } -+ // Paper end - optimise collisions -+ -+ protected VoxelShape(DiscreteVoxelShape voxels) { // Paper - protected - this.shape = voxels; - } - - public double min(Direction.Axis axis) { -- int i = this.shape.firstFull(axis); -- return i >= this.shape.getSize(axis) ? Double.POSITIVE_INFINITY : this.get(axis, i); -+ // Paper start - optimise collisions -+ final io.papermc.paper.util.collisions.CachedShapeData shapeData = this.cachedShapeData; -+ switch (axis) { -+ case X: { -+ final int idx = shapeData.minFullX(); -+ return idx >= shapeData.sizeX() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesX[idx] + this.offsetX); -+ } -+ case Y: { -+ final int idx = shapeData.minFullY(); -+ return idx >= shapeData.sizeY() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesY[idx] + this.offsetY); -+ } -+ case Z: { -+ final int idx = shapeData.minFullZ(); -+ return idx >= shapeData.sizeZ() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesZ[idx] + this.offsetZ); -+ } -+ default: { -+ // should never get here -+ return Double.POSITIVE_INFINITY; -+ } -+ } -+ // Paper end - optimise collisions - } - - public double max(Direction.Axis axis) { -- int i = this.shape.lastFull(axis); -- return i <= 0 ? Double.NEGATIVE_INFINITY : this.get(axis, i); -+ // Paper start - optimise collisions -+ final io.papermc.paper.util.collisions.CachedShapeData shapeData = this.cachedShapeData; -+ switch (axis) { -+ case X: { -+ final int idx = shapeData.maxFullX(); -+ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesX[idx] + this.offsetX); -+ } -+ case Y: { -+ final int idx = shapeData.maxFullY(); -+ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesY[idx] + this.offsetY); -+ } -+ case Z: { -+ final int idx = shapeData.maxFullZ(); -+ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesZ[idx] + this.offsetZ); -+ } -+ default: { -+ // should never get here -+ return Double.NEGATIVE_INFINITY; -+ } -+ } -+ // Paper end - optimise collisions - } - - public AABB bounds() { -- if (this.isEmpty()) { -- throw (UnsupportedOperationException)Util.pauseInIde(new UnsupportedOperationException("No bounds for empty shape.")); -- } else { -- return new AABB( -- this.min(Direction.Axis.X), -- this.min(Direction.Axis.Y), -- this.min(Direction.Axis.Z), -- this.max(Direction.Axis.X), -- this.max(Direction.Axis.Y), -- this.max(Direction.Axis.Z) -- ); -+ // Paper start - optimise collisions -+ if (this.isEmpty) { -+ throw Util.pauseInIde(new UnsupportedOperationException("No bounds for empty shape.")); -+ } -+ AABB cached = this.cachedBounds; -+ if (cached != null) { -+ return cached; - } -+ -+ final io.papermc.paper.util.collisions.CachedShapeData shapeData = this.cachedShapeData; -+ -+ final double[] coordsX = this.rootCoordinatesX; -+ final double[] coordsY = this.rootCoordinatesY; -+ final double[] coordsZ = this.rootCoordinatesZ; -+ -+ final double offX = this.offsetX; -+ final double offY = this.offsetY; -+ final double offZ = this.offsetZ; -+ -+ // note: if not empty, then there is one full AABB so no bounds checks are needed on the minFull/maxFull indices -+ cached = new AABB( -+ coordsX[shapeData.minFullX()] + offX, -+ coordsY[shapeData.minFullY()] + offY, -+ coordsZ[shapeData.minFullZ()] + offZ, -+ -+ coordsX[shapeData.maxFullX()] + offX, -+ coordsY[shapeData.maxFullY()] + offY, -+ coordsZ[shapeData.maxFullZ()] + offZ -+ ); -+ -+ this.cachedBounds = cached; -+ return cached; -+ // Paper end - optimise collisions - } - - public VoxelShape singleEncompassing() { -@@ -69,28 +470,106 @@ public abstract class VoxelShape { - protected abstract DoubleList getCoords(Direction.Axis axis); - - public boolean isEmpty() { -- return this.shape.isEmpty(); -+ return this.isEmpty; // Paper - optimise collisions -+ } -+ -+ // Paper start - optimise collisions -+ private static DoubleList offsetList(final DoubleList src, final double by) { -+ if (src instanceof OffsetDoubleList offsetDoubleList) { -+ return new OffsetDoubleList(offsetDoubleList.delegate, by + offsetDoubleList.offset); -+ } -+ return new OffsetDoubleList(src, by); - } -+ // Paper end - optimise collisions - - public VoxelShape move(double x, double y, double z) { -- return (VoxelShape)(this.isEmpty() -- ? Shapes.empty() -- : new ArrayVoxelShape( -+ // Paper start - optimise collisions -+ if (this.isEmpty) { -+ return Shapes.empty(); -+ } -+ -+ final ArrayVoxelShape ret = new ArrayVoxelShape( - this.shape, -- new OffsetDoubleList(this.getCoords(Direction.Axis.X), x), -- new OffsetDoubleList(this.getCoords(Direction.Axis.Y), y), -- new OffsetDoubleList(this.getCoords(Direction.Axis.Z), z) -- )); -+ offsetList(this.getCoords(Direction.Axis.X), x), -+ offsetList(this.getCoords(Direction.Axis.Y), y), -+ offsetList(this.getCoords(Direction.Axis.Z), z) -+ ); -+ -+ final io.papermc.paper.util.collisions.CachedToAABBs cachedToAABBs = this.cachedToAABBs; -+ if (cachedToAABBs != null) { -+ ((VoxelShape)ret).cachedToAABBs = io.papermc.paper.util.collisions.CachedToAABBs.offset(cachedToAABBs, x, y, z); -+ } -+ -+ return ret; -+ // Paper end - optimise collisions - } - - public VoxelShape optimize() { -- VoxelShape[] voxelShapes = new VoxelShape[]{Shapes.empty()}; -- this.forAllBoxes( -- (minX, minY, minZ, maxX, maxY, maxZ) -> voxelShapes[0] = Shapes.joinUnoptimized( -- voxelShapes[0], Shapes.box(minX, minY, minZ, maxX, maxY, maxZ), BooleanOp.OR -- ) -- ); -- return voxelShapes[0]; -+ // Paper start - optimise collisions -+ // Optimise merge strategy to increase the number of simple joins, and additionally forward the toAabbs cache -+ // to result -+ if (this.isEmpty) { -+ return Shapes.empty(); -+ } -+ -+ if (this.singleAABBRepresentation != null) { -+ // note: the isFullBlock() is fuzzy, and Shapes.create() is also fuzzy which would return block() -+ return this.isFullBlock() ? Shapes.block() : this; -+ } -+ -+ final List aabbs = this.toAabbs(); -+ -+ if (aabbs.size() == 1) { -+ final AABB singleAABB = aabbs.get(0); -+ final VoxelShape ret = Shapes.create(singleAABB); -+ -+ // forward AABB cache -+ if (ret.cachedToAABBs == null) { -+ ret.cachedToAABBs = this.cachedToAABBs; -+ } -+ -+ return ret; -+ } else { -+ // reduce complexity of joins by splitting the merges (old complexity: n^2, new: nlogn) -+ -+ // set up flat array so that this merge is done in-place -+ final VoxelShape[] tmp = new VoxelShape[aabbs.size()]; -+ -+ // initialise as unmerged -+ for (int i = 0, len = aabbs.size(); i < len; ++i) { -+ tmp[i] = Shapes.create(aabbs.get(i)); -+ } -+ -+ int size = aabbs.size(); -+ while (size > 1) { -+ int newSize = 0; -+ for (int i = 0; i < size; i += 2) { -+ final int next = i + 1; -+ if (next >= size) { -+ // nothing to merge with, so leave it for next iteration -+ tmp[newSize++] = tmp[i]; -+ break; -+ } else { -+ // merge with adjacent -+ final VoxelShape first = tmp[i]; -+ final VoxelShape second = tmp[next]; -+ -+ tmp[newSize++] = Shapes.joinUnoptimized(first, second, BooleanOp.OR); -+ } -+ } -+ size = newSize; -+ } -+ -+ final VoxelShape ret = tmp[0]; -+ -+ // forward AABB cache -+ if (ret.cachedToAABBs == null) { -+ ret.cachedToAABBs = this.cachedToAABBs; -+ } -+ -+ return ret; -+ } -+ // Paper end - optimise collisions - } - - public void forAllEdges(Shapes.DoubleLineConsumer consumer) { -@@ -126,10 +605,43 @@ public abstract class VoxelShape { - ); - } - -+ // Paper start - optimise collisions -+ private List toAabbsUncached() { -+ final List ret = new java.util.ArrayList<>(); -+ if (this.singleAABBRepresentation != null) { -+ ret.add(this.singleAABBRepresentation); -+ } else { -+ this.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { -+ ret.add(new AABB(minX, minY, minZ, maxX, maxY, maxZ)); -+ }); -+ } -+ -+ // cache result -+ this.cachedToAABBs = new io.papermc.paper.util.collisions.CachedToAABBs(ret, false, 0.0, 0.0, 0.0); -+ -+ return ret; -+ } -+ // Paper end - optimise collisions -+ - public List toAabbs() { -- List list = Lists.newArrayList(); -- this.forAllBoxes((x1, y1, z1, x2, y2, z2) -> list.add(new AABB(x1, y1, z1, x2, y2, z2))); -- return list; -+ // Paper start - optimise collisions -+ io.papermc.paper.util.collisions.CachedToAABBs cachedToAABBs = this.cachedToAABBs; -+ if (cachedToAABBs != null) { -+ if (!cachedToAABBs.isOffset()) { -+ return cachedToAABBs.aabbs(); -+ } -+ -+ // all we need to do is offset the cache -+ cachedToAABBs = cachedToAABBs.removeOffset(); -+ // update cache -+ this.cachedToAABBs = cachedToAABBs; -+ -+ return cachedToAABBs.aabbs(); -+ } -+ -+ // make new cache -+ return this.toAabbsUncached(); -+ // Paper end - optimise collisions - } - - public double min(Direction.Axis axis, double from, double to) { -@@ -154,43 +666,85 @@ public abstract class VoxelShape { - return Mth.binarySearch(0, this.shape.getSize(axis) + 1, i -> coord < this.get(axis, i)) - 1; - } - -+ // Paper start - optimise collisions -+ /** -+ * Copy of AABB#clip but for one AABB -+ */ -+ private static BlockHitResult clip(final AABB aabb, final Vec3 from, final Vec3 to, final BlockPos offset) { -+ final double[] minDistanceArr = new double[] { 1.0 }; -+ final double diffX = to.x - from.x; -+ final double diffY = to.y - from.y; -+ final double diffZ = to.z - from.z; -+ -+ final Direction direction = AABB.getDirection(aabb.move(offset), from, minDistanceArr, null, diffX, diffY, diffZ); -+ -+ if (direction == null) { -+ return null; -+ } -+ -+ final double minDistance = minDistanceArr[0]; -+ return new BlockHitResult(from.add(minDistance * diffX, minDistance * diffY, minDistance * diffZ), direction, offset, false); -+ } -+ // Paper end - optimise collisions -+ - @Nullable - public BlockHitResult clip(Vec3 start, Vec3 end, BlockPos pos) { -- if (this.isEmpty()) { -+ // Paper start - optimise collisions -+ if (this.isEmpty) { - return null; -- } else { -- Vec3 vec3 = end.subtract(start); -- if (vec3.lengthSqr() < 1.0E-7) { -- return null; -- } else { -- Vec3 vec32 = start.add(vec3.scale(0.001)); -- return this.shape -- .isFullWide( -- this.findIndex(Direction.Axis.X, vec32.x - (double)pos.getX()), -- this.findIndex(Direction.Axis.Y, vec32.y - (double)pos.getY()), -- this.findIndex(Direction.Axis.Z, vec32.z - (double)pos.getZ()) -- ) -- ? new BlockHitResult(vec32, Direction.getNearest(vec3.x, vec3.y, vec3.z).getOpposite(), pos, true) -- : AABB.clip(this.toAabbs(), start, end, pos); -+ } -+ -+ final Vec3 directionOpposite = end.subtract(start); -+ if (directionOpposite.lengthSqr() < io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) { -+ return null; -+ } -+ -+ final Vec3 fromBehind = start.add(directionOpposite.scale(0.001)); -+ final double fromBehindOffsetX = fromBehind.x - (double)pos.getX(); -+ final double fromBehindOffsetY = fromBehind.y - (double)pos.getY(); -+ final double fromBehindOffsetZ = fromBehind.z - (double)pos.getZ(); -+ -+ final AABB singleAABB = this.singleAABBRepresentation; -+ if (singleAABB != null) { -+ if (singleAABB.contains(fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) { -+ return new BlockHitResult(fromBehind, Direction.getNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), pos, true); - } -+ return clip(singleAABB, start, end, pos); - } -+ -+ if (io.papermc.paper.util.CollisionUtil.strictlyContains(this, fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) { -+ return new BlockHitResult(fromBehind, Direction.getNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), pos, true); -+ } -+ -+ return AABB.clip(this.toAabbs(), start, end, pos); -+ // Paper end - optimise collisions - } - - public Optional closestPointTo(Vec3 target) { -- if (this.isEmpty()) { -+ // Paper start - optimise collisions -+ if (this.isEmpty) { - return Optional.empty(); -- } else { -- Vec3[] vec3s = new Vec3[1]; -- this.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { -- double d = Mth.clamp(target.x(), minX, maxX); -- double e = Mth.clamp(target.y(), minY, maxY); -- double f = Mth.clamp(target.z(), minZ, maxZ); -- if (vec3s[0] == null || target.distanceToSqr(d, e, f) < target.distanceToSqr(vec3s[0])) { -- vec3s[0] = new Vec3(d, e, f); -- } -- }); -- return Optional.of(vec3s[0]); - } -+ -+ Vec3 ret = null; -+ double retDistance = Double.MAX_VALUE; -+ -+ final List aabbs = this.toAabbs(); -+ for (int i = 0, len = aabbs.size(); i < len; ++i) { -+ final AABB aabb = aabbs.get(i); -+ final double x = Mth.clamp(target.x, aabb.minX, aabb.maxX); -+ final double y = Mth.clamp(target.y, aabb.minY, aabb.maxY); -+ final double z = Mth.clamp(target.z, aabb.minZ, aabb.maxZ); -+ -+ double dist = target.distanceToSqr(x, y, z); -+ if (dist < retDistance) { -+ ret = new Vec3(x, y, z); -+ retDistance = dist; -+ } -+ } -+ -+ return Optional.ofNullable(ret); -+ // Paper end - optimise collisions - } - - public VoxelShape getFaceShape(Direction facing) { -@@ -227,7 +781,28 @@ public abstract class VoxelShape { - } - - public double collide(Direction.Axis axis, AABB box, double maxDist) { -- return this.collideX(AxisCycle.between(axis, Direction.Axis.X), box, maxDist); -+ // Paper start - optimise collisions -+ if (this.isEmpty) { -+ return maxDist; -+ } -+ if (Math.abs(maxDist) < io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) { -+ return 0.0; -+ } -+ switch (axis) { -+ case X: { -+ return io.papermc.paper.util.CollisionUtil.collideX(this, box, maxDist); -+ } -+ case Y: { -+ return io.papermc.paper.util.CollisionUtil.collideY(this, box, maxDist); -+ } -+ case Z: { -+ return io.papermc.paper.util.CollisionUtil.collideZ(this, box, maxDist); -+ } -+ default: { -+ throw new RuntimeException("Unknown axis: " + axis); -+ } -+ } -+ // Paper end - optimise collisions - } - - protected double collideX(AxisCycle axisCycle, AABB box, double maxDist) { diff --git a/patches/server/1019-Optimise-collision-checking-in-player-move-packet-ha.patch b/patches/server/1019-Optimise-collision-checking-in-player-move-packet-ha.patch new file mode 100644 index 0000000000..378e882366 --- /dev/null +++ b/patches/server/1019-Optimise-collision-checking-in-player-move-packet-ha.patch @@ -0,0 +1,170 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Thu, 2 Jul 2020 12:02:43 -0700 +Subject: [PATCH] Optimise collision checking in player move packet handling + +Move collision logic to just the hasNewCollision call instead of getCubes + hasNewCollision + +CHECK ME + +diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 9c5614d42656d1306e6f0db961391e62a5f42ee7..ae927c2cdf151b901822636a2543668f85946430 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -572,7 +572,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + return; + } + +- boolean flag = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D)); ++ AABB oldBox = entity.getBoundingBox(); // Paper - copy from player movement packet + + d6 = d3 - this.vehicleLastGoodX; // Paper - diff on change, used for checking large move vectors above + d7 = d4 - this.vehicleLastGoodY - 1.0E-6D; // Paper - diff on change, used for checking large move vectors above +@@ -588,6 +588,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + + entity.move(MoverType.PLAYER, new Vec3(d6, d7, d8)); ++ boolean didCollide = toX != entity.getX() || toY != entity.getY() || toZ != entity.getZ(); // Paper - 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... + double d11 = d7; + + d6 = d3 - entity.getX(); +@@ -601,15 +602,23 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + boolean flag2 = false; + + if (d10 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot +- flag2 = true; ++ flag2 = true; // Paper - diff on change, this should be moved wrongly + ServerGamePacketListenerImpl.LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", new Object[]{entity.getName().getString(), this.player.getName().getString(), Math.sqrt(d10)}); + } + + entity.absMoveTo(d3, d4, d5, f, f1); + this.player.absMoveTo(d3, d4, d5, this.player.getYRot(), this.player.getXRot()); // CraftBukkit +- boolean flag3 = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D)); + +- if (flag && (flag2 || !flag3)) { ++ // Paper start - optimise out extra getCubes ++ boolean teleportBack = flag2; // violating this is always a fail ++ if (!teleportBack) { ++ // note: only call after setLocation, or else getBoundingBox is wrong ++ AABB newBox = entity.getBoundingBox(); ++ if (didCollide || !oldBox.equals(newBox)) { ++ teleportBack = this.hasNewCollision(worldserver, entity, oldBox, newBox); ++ } // else: no collision at all detected, why do we care? ++ } ++ if (teleportBack) { // Paper end - optimise out extra getCubes + entity.absMoveTo(d0, d1, d2, f, f1); + this.player.absMoveTo(d0, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit + this.send(new ClientboundMoveVehiclePacket(entity)); +@@ -688,7 +697,32 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + + private boolean noBlocksAround(Entity entity) { +- return entity.level().getBlockStates(entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D)).allMatch(BlockBehaviour.BlockStateBase::isAir); ++ // Paper start - stop using streams, this is already a known fixed problem in Entity#move ++ AABB box = entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D); ++ int minX = Mth.floor(box.minX); ++ int minY = Mth.floor(box.minY); ++ int minZ = Mth.floor(box.minZ); ++ int maxX = Mth.floor(box.maxX); ++ int maxY = Mth.floor(box.maxY); ++ int maxZ = Mth.floor(box.maxZ); ++ ++ Level world = entity.level(); ++ BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); ++ ++ for (int y = minY; y <= maxY; ++y) { ++ for (int z = minZ; z <= maxZ; ++z) { ++ for (int x = minX; x <= maxX; ++x) { ++ pos.set(x, y, z); ++ BlockState type = world.getBlockStateIfLoaded(pos); ++ if (type != null && !type.isAir()) { ++ return false; ++ } ++ } ++ } ++ } ++ ++ return true; ++ // Paper end - stop using streams, this is already a known fixed problem in Entity#move + } + + @Override +@@ -1283,7 +1317,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + + if (this.awaitingPositionFromClient != null) { +- if (this.tickCount - this.awaitingTeleportTime > 20) { ++ if (false && this.tickCount - this.awaitingTeleportTime > 20) { // Paper - this will greatly screw with clients with > 1000ms RTT + this.awaitingTeleportTime = this.tickCount; + this.teleport(this.awaitingPositionFromClient.x, this.awaitingPositionFromClient.y, this.awaitingPositionFromClient.z, this.player.getYRot(), this.player.getXRot()); + } +@@ -1392,7 +1426,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + } + +- AABB axisalignedbb = this.player.getBoundingBox(); ++ AABB axisalignedbb = this.player.getBoundingBox(); // Paper - diff on change, should be old AABB + + d6 = d0 - this.lastGoodX; // Paper - diff on change, used for checking large move vectors above + d7 = d1 - this.lastGoodY; // Paper - diff on change, used for checking large move vectors above +@@ -1434,6 +1468,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + + this.player.move(MoverType.PLAYER, new Vec3(d6, d7, d8)); + this.player.onGround = packet.isOnGround(); // CraftBukkit - SPIGOT-5810, SPIGOT-5835, SPIGOT-6828: reset by this.player.move ++ boolean didCollide = toX != this.player.getX() || toY != this.player.getY() || toZ != this.player.getZ(); // Paper - 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... + // Paper start - prevent position desync + if (this.awaitingPositionFromClient != null) { + return; // ... thanks Mojang for letting move calls teleport across dimensions. +@@ -1464,7 +1499,17 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + + // Paper start - Add fail move event +- boolean teleportBack = !this.player.noPhysics && !this.player.isSleeping() && (movedWrongly && worldserver.noCollision(this.player, axisalignedbb) || this.isPlayerCollidingWithAnythingNew(worldserver, axisalignedbb, d0, d1, d2)); ++ // Paper start - optimise out extra getCubes ++ boolean teleportBack = !this.player.noPhysics && !this.player.isSleeping() && movedWrongly; ++ this.player.absMoveTo(d0, d1, d2, f, f1); // prevent desync by tping to the set position, dropped for unknown reasons by mojang ++ if (!this.player.noPhysics && !this.player.isSleeping() && !teleportBack) { ++ AABB newBox = this.player.getBoundingBox(); ++ if (didCollide || !axisalignedbb.equals(newBox)) { ++ // note: only call after setLocation, or else getBoundingBox is wrong ++ teleportBack = this.hasNewCollision(worldserver, this.player, axisalignedbb, newBox); ++ } // else: no collision at all detected, why do we care? ++ } ++ // Paper end - optimise out extra getCubes + if (teleportBack) { + io.papermc.paper.event.player.PlayerFailMoveEvent event = fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason.CLIPPED_INTO_BLOCK, + toX, toY, toZ, toYaw, toPitch, false); +@@ -1570,6 +1615,33 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + } + ++ // Paper start - optimise out extra getCubes ++ private boolean hasNewCollision(final ServerLevel world, final Entity entity, final AABB oldBox, final AABB newBox) { ++ final List collisionsBB = new java.util.ArrayList<>(); ++ final List collisionsVoxel = new java.util.ArrayList<>(); ++ io.papermc.paper.util.CollisionUtil.getCollisions( ++ world, entity, newBox, collisionsVoxel, collisionsBB, ++ io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS | io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, ++ null, null ++ ); ++ ++ for (int i = 0, len = collisionsBB.size(); i < len; ++i) { ++ final AABB box = collisionsBB.get(i); ++ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(box, oldBox)) { ++ return true; ++ } ++ } ++ ++ for (int i = 0, len = collisionsVoxel.size(); i < len; ++i) { ++ final VoxelShape voxel = collisionsVoxel.get(i); ++ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersectNoEmpty(voxel, oldBox)) { ++ return true; ++ } ++ } ++ ++ return false; ++ } ++ // Paper end - optimise out extra getCubes + private boolean isPlayerCollidingWithAnythingNew(LevelReader world, AABB box, double newX, double newY, double newZ) { + AABB axisalignedbb1 = this.player.getBoundingBox().move(newX - this.player.getX(), newY - this.player.getY(), newZ - this.player.getZ()); + Iterable iterable = world.getCollisions(this.player, axisalignedbb1.deflate(9.999999747378752E-6D)); diff --git a/patches/server/1020-Fix-tripwire-disarming-not-working-as-intended.patch b/patches/server/1020-Fix-tripwire-disarming-not-working-as-intended.patch new file mode 100644 index 0000000000..fa7f6bde06 --- /dev/null +++ b/patches/server/1020-Fix-tripwire-disarming-not-working-as-intended.patch @@ -0,0 +1,22 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: DungeonDev +Date: Sun, 2 Jul 2023 02:34:54 +0100 +Subject: [PATCH] Fix tripwire disarming not working as intended + +Fixes MC-129055 + +diff --git a/src/main/java/net/minecraft/world/level/block/TripWireHookBlock.java b/src/main/java/net/minecraft/world/level/block/TripWireHookBlock.java +index 8614fad5b3df7a6030384b108b1689bf6b9f1209..76aca266d3f3222502ff4c196228f08fcd88c5f8 100644 +--- a/src/main/java/net/minecraft/world/level/block/TripWireHookBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/TripWireHookBlock.java +@@ -202,9 +202,8 @@ public class TripWireHookBlock extends Block { + BlockState iblockdata4 = aiblockdata[l]; + + if (iblockdata4 != null) { ++ if (world.getBlockState(blockposition2).is(Blocks.TRIPWIRE) || io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowTripwireDisarmingExploits) { // Paper - Fix tripwire disarming not working as intended + world.setBlock(blockposition2, (BlockState) iblockdata4.trySetValue(TripWireHookBlock.ATTACHED, flag4), 3); +- if (!world.getBlockState(blockposition2).isAir()) { +- ; + } + } + } diff --git a/patches/server/1020-Optimise-collision-checking-in-player-move-packet-ha.patch b/patches/server/1020-Optimise-collision-checking-in-player-move-packet-ha.patch deleted file mode 100644 index 378e882366..0000000000 --- a/patches/server/1020-Optimise-collision-checking-in-player-move-packet-ha.patch +++ /dev/null @@ -1,170 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Thu, 2 Jul 2020 12:02:43 -0700 -Subject: [PATCH] Optimise collision checking in player move packet handling - -Move collision logic to just the hasNewCollision call instead of getCubes + hasNewCollision - -CHECK ME - -diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index 9c5614d42656d1306e6f0db961391e62a5f42ee7..ae927c2cdf151b901822636a2543668f85946430 100644 ---- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -572,7 +572,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - return; - } - -- boolean flag = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D)); -+ AABB oldBox = entity.getBoundingBox(); // Paper - copy from player movement packet - - d6 = d3 - this.vehicleLastGoodX; // Paper - diff on change, used for checking large move vectors above - d7 = d4 - this.vehicleLastGoodY - 1.0E-6D; // Paper - diff on change, used for checking large move vectors above -@@ -588,6 +588,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - } - - entity.move(MoverType.PLAYER, new Vec3(d6, d7, d8)); -+ boolean didCollide = toX != entity.getX() || toY != entity.getY() || toZ != entity.getZ(); // Paper - 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... - double d11 = d7; - - d6 = d3 - entity.getX(); -@@ -601,15 +602,23 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - boolean flag2 = false; - - if (d10 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot -- flag2 = true; -+ flag2 = true; // Paper - diff on change, this should be moved wrongly - ServerGamePacketListenerImpl.LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", new Object[]{entity.getName().getString(), this.player.getName().getString(), Math.sqrt(d10)}); - } - - entity.absMoveTo(d3, d4, d5, f, f1); - this.player.absMoveTo(d3, d4, d5, this.player.getYRot(), this.player.getXRot()); // CraftBukkit -- boolean flag3 = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D)); - -- if (flag && (flag2 || !flag3)) { -+ // Paper start - optimise out extra getCubes -+ boolean teleportBack = flag2; // violating this is always a fail -+ if (!teleportBack) { -+ // note: only call after setLocation, or else getBoundingBox is wrong -+ AABB newBox = entity.getBoundingBox(); -+ if (didCollide || !oldBox.equals(newBox)) { -+ teleportBack = this.hasNewCollision(worldserver, entity, oldBox, newBox); -+ } // else: no collision at all detected, why do we care? -+ } -+ if (teleportBack) { // Paper end - optimise out extra getCubes - entity.absMoveTo(d0, d1, d2, f, f1); - this.player.absMoveTo(d0, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit - this.send(new ClientboundMoveVehiclePacket(entity)); -@@ -688,7 +697,32 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - } - - private boolean noBlocksAround(Entity entity) { -- return entity.level().getBlockStates(entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D)).allMatch(BlockBehaviour.BlockStateBase::isAir); -+ // Paper start - stop using streams, this is already a known fixed problem in Entity#move -+ AABB box = entity.getBoundingBox().inflate(0.0625D).expandTowards(0.0D, -0.55D, 0.0D); -+ int minX = Mth.floor(box.minX); -+ int minY = Mth.floor(box.minY); -+ int minZ = Mth.floor(box.minZ); -+ int maxX = Mth.floor(box.maxX); -+ int maxY = Mth.floor(box.maxY); -+ int maxZ = Mth.floor(box.maxZ); -+ -+ Level world = entity.level(); -+ BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); -+ -+ for (int y = minY; y <= maxY; ++y) { -+ for (int z = minZ; z <= maxZ; ++z) { -+ for (int x = minX; x <= maxX; ++x) { -+ pos.set(x, y, z); -+ BlockState type = world.getBlockStateIfLoaded(pos); -+ if (type != null && !type.isAir()) { -+ return false; -+ } -+ } -+ } -+ } -+ -+ return true; -+ // Paper end - stop using streams, this is already a known fixed problem in Entity#move - } - - @Override -@@ -1283,7 +1317,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - } - - if (this.awaitingPositionFromClient != null) { -- if (this.tickCount - this.awaitingTeleportTime > 20) { -+ if (false && this.tickCount - this.awaitingTeleportTime > 20) { // Paper - this will greatly screw with clients with > 1000ms RTT - this.awaitingTeleportTime = this.tickCount; - this.teleport(this.awaitingPositionFromClient.x, this.awaitingPositionFromClient.y, this.awaitingPositionFromClient.z, this.player.getYRot(), this.player.getXRot()); - } -@@ -1392,7 +1426,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - } - } - -- AABB axisalignedbb = this.player.getBoundingBox(); -+ AABB axisalignedbb = this.player.getBoundingBox(); // Paper - diff on change, should be old AABB - - d6 = d0 - this.lastGoodX; // Paper - diff on change, used for checking large move vectors above - d7 = d1 - this.lastGoodY; // Paper - diff on change, used for checking large move vectors above -@@ -1434,6 +1468,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - - this.player.move(MoverType.PLAYER, new Vec3(d6, d7, d8)); - this.player.onGround = packet.isOnGround(); // CraftBukkit - SPIGOT-5810, SPIGOT-5835, SPIGOT-6828: reset by this.player.move -+ boolean didCollide = toX != this.player.getX() || toY != this.player.getY() || toZ != this.player.getZ(); // Paper - 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... - // Paper start - prevent position desync - if (this.awaitingPositionFromClient != null) { - return; // ... thanks Mojang for letting move calls teleport across dimensions. -@@ -1464,7 +1499,17 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - } - - // Paper start - Add fail move event -- boolean teleportBack = !this.player.noPhysics && !this.player.isSleeping() && (movedWrongly && worldserver.noCollision(this.player, axisalignedbb) || this.isPlayerCollidingWithAnythingNew(worldserver, axisalignedbb, d0, d1, d2)); -+ // Paper start - optimise out extra getCubes -+ boolean teleportBack = !this.player.noPhysics && !this.player.isSleeping() && movedWrongly; -+ this.player.absMoveTo(d0, d1, d2, f, f1); // prevent desync by tping to the set position, dropped for unknown reasons by mojang -+ if (!this.player.noPhysics && !this.player.isSleeping() && !teleportBack) { -+ AABB newBox = this.player.getBoundingBox(); -+ if (didCollide || !axisalignedbb.equals(newBox)) { -+ // note: only call after setLocation, or else getBoundingBox is wrong -+ teleportBack = this.hasNewCollision(worldserver, this.player, axisalignedbb, newBox); -+ } // else: no collision at all detected, why do we care? -+ } -+ // Paper end - optimise out extra getCubes - if (teleportBack) { - io.papermc.paper.event.player.PlayerFailMoveEvent event = fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason.CLIPPED_INTO_BLOCK, - toX, toY, toZ, toYaw, toPitch, false); -@@ -1570,6 +1615,33 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - } - } - -+ // Paper start - optimise out extra getCubes -+ private boolean hasNewCollision(final ServerLevel world, final Entity entity, final AABB oldBox, final AABB newBox) { -+ final List collisionsBB = new java.util.ArrayList<>(); -+ final List collisionsVoxel = new java.util.ArrayList<>(); -+ io.papermc.paper.util.CollisionUtil.getCollisions( -+ world, entity, newBox, collisionsVoxel, collisionsBB, -+ io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS | io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER, -+ null, null -+ ); -+ -+ for (int i = 0, len = collisionsBB.size(); i < len; ++i) { -+ final AABB box = collisionsBB.get(i); -+ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(box, oldBox)) { -+ return true; -+ } -+ } -+ -+ for (int i = 0, len = collisionsVoxel.size(); i < len; ++i) { -+ final VoxelShape voxel = collisionsVoxel.get(i); -+ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersectNoEmpty(voxel, oldBox)) { -+ return true; -+ } -+ } -+ -+ return false; -+ } -+ // Paper end - optimise out extra getCubes - private boolean isPlayerCollidingWithAnythingNew(LevelReader world, AABB box, double newX, double newY, double newZ) { - AABB axisalignedbb1 = this.player.getBoundingBox().move(newX - this.player.getX(), newY - this.player.getY(), newZ - this.player.getZ()); - Iterable iterable = world.getCollisions(this.player, axisalignedbb1.deflate(9.999999747378752E-6D)); diff --git a/patches/server/1021-Fix-entity-type-tags-suggestions-in-selectors.patch b/patches/server/1021-Fix-entity-type-tags-suggestions-in-selectors.patch new file mode 100644 index 0000000000..a31aeb7f18 --- /dev/null +++ b/patches/server/1021-Fix-entity-type-tags-suggestions-in-selectors.patch @@ -0,0 +1,156 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Sun, 22 Aug 2021 15:21:57 -0700 +Subject: [PATCH] Fix entity type tags suggestions in selectors + +This would really be better as a client fix because just to fix it +all EntityArguments have been told to ask the server for completions +when if this was fixed on the client, that wouldn't be needed. + +Mojira Issue: https://bugs.mojang.com/browse/MC-235045 + +diff --git a/src/main/java/net/minecraft/commands/CommandSourceStack.java b/src/main/java/net/minecraft/commands/CommandSourceStack.java +index 3c0d2332207ba638faaaa4280bce18c334a01271..e6c7f62ed379a78645933670299e4fcda8540ed1 100644 +--- a/src/main/java/net/minecraft/commands/CommandSourceStack.java ++++ b/src/main/java/net/minecraft/commands/CommandSourceStack.java +@@ -462,4 +462,20 @@ public class CommandSourceStack implements ExecutionCommandSource getSelectedEntities() { ++ if (io.papermc.paper.configuration.GlobalConfiguration.get().commands.fixTargetSelectorTagCompletion && this.source instanceof ServerPlayer player) { ++ final Entity cameraEntity = player.getCamera(); ++ final double pickDistance = player.gameMode.getGameModeForPlayer().isCreative() ? 6.0F : 4.5F; ++ final Vec3 min = cameraEntity.getEyePosition(1.0F); ++ final Vec3 viewVector = cameraEntity.getViewVector(1.0F); ++ final Vec3 max = min.add(viewVector.x * pickDistance, viewVector.y * pickDistance, viewVector.z * pickDistance); ++ final net.minecraft.world.phys.AABB aabb = cameraEntity.getBoundingBox().expandTowards(viewVector.scale(pickDistance)).inflate(1.0D, 1.0D, 1.0D); ++ final net.minecraft.world.phys.EntityHitResult hitResult = net.minecraft.world.entity.projectile.ProjectileUtil.getEntityHitResult(cameraEntity, min, max, aabb, (e) -> !e.isSpectator() && e.isPickable(), pickDistance * pickDistance); ++ return hitResult != null ? java.util.Collections.singletonList(hitResult.getEntity().getStringUUID()) : SharedSuggestionProvider.super.getSelectedEntities(); ++ } ++ return SharedSuggestionProvider.super.getSelectedEntities(); ++ } ++ // Paper end - tell clients to ask server for suggestions for EntityArguments + } +diff --git a/src/main/java/net/minecraft/commands/Commands.java b/src/main/java/net/minecraft/commands/Commands.java +index 60406cb01c5a409ba6fe98677a5cc332effaebea..a0702dccbb07c79febe2e65a2ff82c4436c09f12 100644 +--- a/src/main/java/net/minecraft/commands/Commands.java ++++ b/src/main/java/net/minecraft/commands/Commands.java +@@ -525,6 +525,7 @@ public class Commands { + private void fillUsableCommands(CommandNode tree, CommandNode result, CommandSourceStack source, Map, CommandNode> resultNodes) { + Iterator iterator = tree.getChildren().iterator(); + ++ boolean registeredAskServerSuggestionsForTree = false; // Paper - tell clients to ask server for suggestions for EntityArguments + while (iterator.hasNext()) { + CommandNode commandnode2 = (CommandNode) iterator.next(); + // Paper start - Brigadier API +@@ -551,6 +552,12 @@ public class Commands { + + if (requiredargumentbuilder.getSuggestionsProvider() != null) { + requiredargumentbuilder.suggests(SuggestionProviders.safelySwap(requiredargumentbuilder.getSuggestionsProvider())); ++ // Paper start - tell clients to ask server for suggestions for EntityArguments ++ registeredAskServerSuggestionsForTree = requiredargumentbuilder.getSuggestionsProvider() == net.minecraft.commands.synchronization.SuggestionProviders.ASK_SERVER; ++ } else if (io.papermc.paper.configuration.GlobalConfiguration.get().commands.fixTargetSelectorTagCompletion && !registeredAskServerSuggestionsForTree && requiredargumentbuilder.getType() instanceof net.minecraft.commands.arguments.EntityArgument) { ++ requiredargumentbuilder.suggests(requiredargumentbuilder.getType()::listSuggestions); ++ registeredAskServerSuggestionsForTree = true; // You can only ++ // Paper end - tell clients to ask server for suggestions for EntityArguments + } + } + +diff --git a/src/main/java/net/minecraft/commands/arguments/EntityArgument.java b/src/main/java/net/minecraft/commands/arguments/EntityArgument.java +index 2043001c16b3530c2d3f52efda10bcad424881c0..7976885b902a6ce7d80f31e49448c99452eb9765 100644 +--- a/src/main/java/net/minecraft/commands/arguments/EntityArgument.java ++++ b/src/main/java/net/minecraft/commands/arguments/EntityArgument.java +@@ -131,7 +131,7 @@ public class EntityArgument implements ArgumentType { + final boolean permission = object instanceof CommandSourceStack stack + ? stack.bypassSelectorPermissions || stack.hasPermission(2, "minecraft.command.selector") + : icompletionprovider.hasPermission(2); +- EntitySelectorParser argumentparserselector = new EntitySelectorParser(stringreader, permission); ++ EntitySelectorParser argumentparserselector = new EntitySelectorParser(stringreader, permission, true); // Paper - tell clients to ask server for suggestions for EntityArguments + // Paper end - Fix EntityArgument suggestion permissions + + try { +@@ -141,7 +141,19 @@ public class EntityArgument implements ArgumentType { + } + + return argumentparserselector.fillSuggestions(suggestionsbuilder, (suggestionsbuilder1) -> { +- Collection collection = icompletionprovider.getOnlinePlayerNames(); ++ // Paper start - tell clients to ask server for suggestions for EntityArguments ++ final Collection collection; ++ if (icompletionprovider instanceof CommandSourceStack commandSourceStack && commandSourceStack.getEntity() instanceof ServerPlayer sourcePlayer) { ++ collection = new java.util.ArrayList<>(); ++ for (final ServerPlayer player : commandSourceStack.getServer().getPlayerList().getPlayers()) { ++ if (sourcePlayer.getBukkitEntity().canSee(player.getBukkitEntity())) { ++ collection.add(player.getGameProfile().getName()); ++ } ++ } ++ } else { ++ collection = icompletionprovider.getOnlinePlayerNames(); ++ } ++ // Paper end - tell clients to ask server for suggestions for EntityArguments + Iterable iterable = this.playersOnly ? collection : Iterables.concat(collection, icompletionprovider.getSelectedEntities()); + + SharedSuggestionProvider.suggest((Iterable) iterable, suggestionsbuilder1); +diff --git a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelectorParser.java b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelectorParser.java +index 3d897ec6920eff6176ddac9f0442a997b9ef14fd..abaf1ea340c69c8bee80e64567b44b5ce66d5fa3 100644 +--- a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelectorParser.java ++++ b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelectorParser.java +@@ -113,12 +113,19 @@ public class EntitySelectorParser { + private boolean hasScores; + private boolean hasAdvancements; + private boolean usesSelectors; ++ public boolean parsingEntityArgumentSuggestions; // Paper - tell clients to ask server for suggestions for EntityArguments + + public EntitySelectorParser(StringReader reader) { + this(reader, true); + } + + public EntitySelectorParser(StringReader reader, boolean atAllowed) { ++ // Paper start - tell clients to ask server for suggestions for EntityArguments ++ this(reader, atAllowed, false); ++ } ++ public EntitySelectorParser(StringReader reader, boolean atAllowed, boolean parsingEntityArgumentSuggestions) { ++ this.parsingEntityArgumentSuggestions = parsingEntityArgumentSuggestions; ++ // Paper end - tell clients to ask server for suggestions for EntityArguments + this.distance = MinMaxBounds.Doubles.ANY; + this.level = MinMaxBounds.Ints.ANY; + this.rotX = WrappedMinMaxBounds.ANY; +diff --git a/src/main/java/net/minecraft/commands/arguments/selector/options/EntitySelectorOptions.java b/src/main/java/net/minecraft/commands/arguments/selector/options/EntitySelectorOptions.java +index 44982ff2cdd6e03f0b9ce8c3cc87561ef183ef06..626fe7a45c2edba68eb201974e9f8f5eebf75cc0 100644 +--- a/src/main/java/net/minecraft/commands/arguments/selector/options/EntitySelectorOptions.java ++++ b/src/main/java/net/minecraft/commands/arguments/selector/options/EntitySelectorOptions.java +@@ -76,6 +76,19 @@ public class EntitySelectorOptions { + public static final DynamicCommandExceptionType ERROR_ENTITY_TYPE_INVALID = new DynamicCommandExceptionType( + entity -> Component.translatableEscape("argument.entity.options.type.invalid", entity) + ); ++ // Paper start - tell clients to ask server for suggestions for EntityArguments ++ public static final DynamicCommandExceptionType ERROR_ENTITY_TAG_INVALID = new DynamicCommandExceptionType((object) -> { ++ return io.papermc.paper.adventure.PaperAdventure ++ .asVanilla(net.kyori.adventure.text.Component ++ .text("Invalid or unknown entity type tag '" + object + "'") ++ .hoverEvent(net.kyori.adventure.text.event.HoverEvent ++ .showText(net.kyori.adventure.text.Component ++ .text("You can disable this error in 'paper.yml'") ++ ) ++ ) ++ ); ++ }); ++ // Paper end - tell clients to ask server for suggestions for EntityArguments + + private static void register(String id, EntitySelectorOptions.Modifier handler, Predicate condition, Component description) { + OPTIONS.put(id, new EntitySelectorOptions.Option(handler, condition, description)); +@@ -297,6 +310,12 @@ public class EntitySelectorOptions { + + if (reader.isTag()) { + TagKey> tagKey = TagKey.create(Registries.ENTITY_TYPE, ResourceLocation.read(reader.getReader())); ++ // Paper start - tell clients to ask server for suggestions for EntityArguments; throw error if invalid entity tag (only on suggestions to keep cmd success behavior) ++ if (reader.parsingEntityArgumentSuggestions && io.papermc.paper.configuration.GlobalConfiguration.get().commands.fixTargetSelectorTagCompletion && net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.getTag(tagKey).isEmpty()) { ++ reader.getReader().setCursor(i); ++ throw ERROR_ENTITY_TAG_INVALID.createWithContext(reader.getReader(), tagKey); ++ } ++ // Paper end - tell clients to ask server for suggestions for EntityArguments + reader.addPredicate(entity -> entity.getType().is(tagKey) != bl); + } else { + ResourceLocation resourceLocation = ResourceLocation.read(reader.getReader()); diff --git a/patches/server/1021-Fix-tripwire-disarming-not-working-as-intended.patch b/patches/server/1021-Fix-tripwire-disarming-not-working-as-intended.patch deleted file mode 100644 index fa7f6bde06..0000000000 --- a/patches/server/1021-Fix-tripwire-disarming-not-working-as-intended.patch +++ /dev/null @@ -1,22 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: DungeonDev -Date: Sun, 2 Jul 2023 02:34:54 +0100 -Subject: [PATCH] Fix tripwire disarming not working as intended - -Fixes MC-129055 - -diff --git a/src/main/java/net/minecraft/world/level/block/TripWireHookBlock.java b/src/main/java/net/minecraft/world/level/block/TripWireHookBlock.java -index 8614fad5b3df7a6030384b108b1689bf6b9f1209..76aca266d3f3222502ff4c196228f08fcd88c5f8 100644 ---- a/src/main/java/net/minecraft/world/level/block/TripWireHookBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/TripWireHookBlock.java -@@ -202,9 +202,8 @@ public class TripWireHookBlock extends Block { - BlockState iblockdata4 = aiblockdata[l]; - - if (iblockdata4 != null) { -+ if (world.getBlockState(blockposition2).is(Blocks.TRIPWIRE) || io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowTripwireDisarmingExploits) { // Paper - Fix tripwire disarming not working as intended - world.setBlock(blockposition2, (BlockState) iblockdata4.trySetValue(TripWireHookBlock.ATTACHED, flag4), 3); -- if (!world.getBlockState(blockposition2).isAir()) { -- ; - } - } - } diff --git a/patches/server/1022-Add-Alternate-Current-redstone-implementation.patch b/patches/server/1022-Add-Alternate-Current-redstone-implementation.patch new file mode 100644 index 0000000000..afb3758607 --- /dev/null +++ b/patches/server/1022-Add-Alternate-Current-redstone-implementation.patch @@ -0,0 +1,2114 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Space Walker +Date: Wed, 8 Jun 2022 18:47:18 +0200 +Subject: [PATCH] Add Alternate Current redstone implementation + +Author: Space Walker + +Original license: MIT +Original project: https://github.com/SpaceWalkerRS/alternate-current + +This patch adds Alternate Current's redstone implementation as an alternative to vanilla and Eigencraft's. +Performance of (de)powering redstone dust is many times faster than vanilla, and even exceeds Eigencraft. +Similar to Eigencraft, Alternate Current heavily changes the update order of redstone dust. This means any contraption that +is location dependent in vanilla will either work everywhere or nowhere when using Alternate Current/Eigencraft. Beyond that +parity issues should be rare for both implementations, though Alternate Current has not been tested as thoroughly, so I +cannot comment on how the two compare in that aspect. + +Alternate Current needs the following modifications: +* Level/ServerLevel: Each level has its own 'wire handler' that handles redstone dust power changes. +* RedStoneWireBlock: Replace calls to vanilla's or Eigencraft's methods for handling power changes with calls to +Alternate Current's wire handler. + +diff --git a/src/main/java/alternate/current/wire/LevelHelper.java b/src/main/java/alternate/current/wire/LevelHelper.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8b4697421d57f81ff1794c6f845258e10df91622 +--- /dev/null ++++ b/src/main/java/alternate/current/wire/LevelHelper.java +@@ -0,0 +1,66 @@ ++package alternate.current.wire; ++ ++import org.bukkit.craftbukkit.block.CraftBlock; ++import org.bukkit.event.block.BlockRedstoneEvent; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++import net.minecraft.world.level.chunk.LevelChunkSection; ++ ++public class LevelHelper { ++ ++ static int doRedstoneEvent(ServerLevel level, BlockPos pos, int prevPower, int newPower) { ++ BlockRedstoneEvent event = new BlockRedstoneEvent(CraftBlock.at(level, pos), prevPower, newPower); ++ level.getCraftServer().getPluginManager().callEvent(event); ++ ++ return event.getNewCurrent(); ++ } ++ ++ /** ++ * An optimized version of {@link net.minecraft.world.level.Level#setBlock ++ * Level.setBlock}. Since this method is only used to update redstone wire block ++ * states, lighting checks, height map updates, and block entity updates are ++ * omitted. ++ */ ++ static boolean setWireState(ServerLevel level, BlockPos pos, BlockState state, boolean updateNeighborShapes) { ++ int y = pos.getY(); ++ ++ if (y < level.getMinBuildHeight() || y >= level.getMaxBuildHeight()) { ++ return false; ++ } ++ ++ int x = pos.getX(); ++ int z = pos.getZ(); ++ int index = level.getSectionIndex(y); ++ ++ ChunkAccess chunk = level.getChunk(x >> 4, z >> 4, ChunkStatus.FULL, true); ++ LevelChunkSection section = chunk.getSections()[index]; ++ ++ if (section == null) { ++ return false; // we should never get here ++ } ++ ++ BlockState prevState = section.setBlockState(x & 15, y & 15, z & 15, state); ++ ++ if (state == prevState) { ++ return false; ++ } ++ ++ // notify clients of the BlockState change ++ level.getChunkSource().blockChanged(pos); ++ // mark the chunk for saving ++ chunk.setUnsaved(true); ++ ++ if (updateNeighborShapes) { ++ prevState.updateIndirectNeighbourShapes(level, pos, Block.UPDATE_CLIENTS); ++ state.updateNeighbourShapes(level, pos, Block.UPDATE_CLIENTS); ++ state.updateIndirectNeighbourShapes(level, pos, Block.UPDATE_CLIENTS); ++ } ++ ++ return true; ++ } ++} +diff --git a/src/main/java/alternate/current/wire/Node.java b/src/main/java/alternate/current/wire/Node.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8af6c69098e64945361d116b5fd6ac21e97fcd8d +--- /dev/null ++++ b/src/main/java/alternate/current/wire/Node.java +@@ -0,0 +1,113 @@ ++package alternate.current.wire; ++ ++import java.util.Arrays; ++ ++import alternate.current.wire.WireHandler.Directions; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.block.state.BlockState; ++ ++/** ++ * A Node represents a block in the world. It also holds a few other pieces of ++ * information that speed up the calculations in the WireHandler class. ++ * ++ * @author Space Walker ++ */ ++public class Node { ++ ++ // flags that encode the Node type ++ private static final int CONDUCTOR = 0b01; ++ private static final int SOURCE = 0b10; ++ ++ final ServerLevel level; ++ final Node[] neighbors; ++ ++ BlockPos pos; ++ BlockState state; ++ boolean invalid; ++ ++ private int flags; ++ ++ /** The previous node in the priority queue. */ ++ Node prev_node; ++ /** The next node in the priority queue. */ ++ Node next_node; ++ /** The priority with which this node was queued. */ ++ int priority; ++ /** The wire that queued this node for an update. */ ++ WireNode neighborWire; ++ ++ Node(ServerLevel level) { ++ this.level = level; ++ this.neighbors = new Node[Directions.ALL.length]; ++ } ++ ++ @Override ++ public boolean equals(Object obj) { ++ if (this == obj) { ++ return true; ++ } ++ if (!(obj instanceof Node)) { ++ return false; ++ } ++ ++ Node node = (Node)obj; ++ ++ return level == node.level && pos.equals(node.pos); ++ } ++ ++ @Override ++ public int hashCode() { ++ return pos.hashCode(); ++ } ++ ++ Node set(BlockPos pos, BlockState state, boolean clearNeighbors) { ++ if (state.is(Blocks.REDSTONE_WIRE)) { ++ throw new IllegalStateException("Cannot update a regular Node to a WireNode!"); ++ } ++ ++ if (clearNeighbors) { ++ Arrays.fill(neighbors, null); ++ } ++ ++ this.pos = pos.immutable(); ++ this.state = state; ++ this.invalid = false; ++ ++ this.flags = 0; ++ ++ if (this.state.isRedstoneConductor(this.level, this.pos)) { ++ this.flags |= CONDUCTOR; ++ } ++ if (this.state.isSignalSource()) { ++ this.flags |= SOURCE; ++ } ++ ++ return this; ++ } ++ ++ /** ++ * Determine the priority with which this node should be queued. ++ */ ++ int priority() { ++ return neighborWire.priority; ++ } ++ ++ public boolean isWire() { ++ return false; ++ } ++ ++ public boolean isConductor() { ++ return (flags & CONDUCTOR) != 0; ++ } ++ ++ public boolean isSignalSource() { ++ return (flags & SOURCE) != 0; ++ } ++ ++ public WireNode asWire() { ++ throw new UnsupportedOperationException("Not a WireNode!"); ++ } ++} +diff --git a/src/main/java/alternate/current/wire/PriorityQueue.java b/src/main/java/alternate/current/wire/PriorityQueue.java +new file mode 100644 +index 0000000000000000000000000000000000000000..d71b4d0e4c44a2620b41b89475412db53bea20ed +--- /dev/null ++++ b/src/main/java/alternate/current/wire/PriorityQueue.java +@@ -0,0 +1,211 @@ ++package alternate.current.wire; ++ ++import java.util.AbstractQueue; ++import java.util.Arrays; ++import java.util.Iterator; ++ ++import net.minecraft.world.level.redstone.Redstone; ++ ++public class PriorityQueue extends AbstractQueue { ++ ++ private static final int OFFSET = -Redstone.SIGNAL_MIN; ++ ++ /** The last node for each priority value. */ ++ private final Node[] tails; ++ ++ private Node head; ++ private Node tail; ++ ++ private int size; ++ ++ PriorityQueue() { ++ this.tails = new Node[(Redstone.SIGNAL_MAX + OFFSET) + 1]; ++ } ++ ++ @Override ++ public boolean offer(Node node) { ++ if (node == null) { ++ throw new NullPointerException(); ++ } ++ ++ int priority = node.priority(); ++ ++ if (contains(node)) { ++ if (node.priority == priority) { ++ // already queued with this priority; exit ++ return false; ++ } else { ++ // already queued with different priority; move it ++ move(node, priority); ++ } ++ } else { ++ insert(node, priority); ++ } ++ ++ return true; ++ } ++ ++ @Override ++ public Node poll() { ++ if (head == null) { ++ return null; ++ } ++ ++ Node node = head; ++ Node next = node.next_node; ++ ++ if (next == null) { ++ clear(); // reset the tails array ++ } else { ++ if (node.priority != next.priority) { ++ // If the head is also a tail, its entry in the array ++ // can be cleared; there is no previous node with the ++ // same priority to take its place. ++ tails[node.priority + OFFSET] = null; ++ } ++ ++ node.next_node = null; ++ next.prev_node = null; ++ head = next; ++ ++ size--; ++ } ++ ++ return node; ++ } ++ ++ @Override ++ public Node peek() { ++ return head; ++ } ++ ++ @Override ++ public void clear() { ++ for (Node node = head; node != null; ) { ++ Node n = node; ++ node = node.next_node; ++ ++ n.prev_node = null; ++ n.next_node = null; ++ } ++ ++ Arrays.fill(tails, null); ++ ++ head = null; ++ tail = null; ++ ++ size = 0; ++ } ++ ++ @Override ++ public Iterator iterator() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public int size() { ++ return size; ++ } ++ ++ public boolean contains(Node node) { ++ return node == head || node.prev_node != null; ++ } ++ ++ private void move(Node node, int priority) { ++ remove(node); ++ insert(node, priority); ++ } ++ ++ private void remove(Node node) { ++ Node prev = node.prev_node; ++ Node next = node.next_node; ++ ++ if (node == tail || node.priority != next.priority) { ++ // assign a new tail for this node's priority ++ if (node == head || node.priority != prev.priority) { ++ // there is no other node with the same priority; clear ++ tails[node.priority + OFFSET] = null; ++ } else { ++ // the previous node in the queue becomes the tail ++ tails[node.priority + OFFSET] = prev; ++ } ++ } ++ ++ if (node == head) { ++ head = next; ++ } else { ++ prev.next_node = next; ++ } ++ if (node == tail) { ++ tail = prev; ++ } else { ++ next.prev_node = prev; ++ } ++ ++ node.prev_node = null; ++ node.next_node = null; ++ ++ size--; ++ } ++ ++ private void insert(Node node, int priority) { ++ node.priority = priority; ++ ++ // nodes are sorted by priority (highest to lowest) ++ // nodes with the same priority are ordered FIFO ++ if (head == null) { ++ // first element in this queue \o/ ++ head = tail = node; ++ } else if (priority > head.priority) { ++ linkHead(node); ++ } else if (priority <= tail.priority) { ++ linkTail(node); ++ } else { ++ // since the node is neither the head nor the tail ++ // findPrev is guaranteed to find a non-null element ++ linkAfter(findPrev(node), node); ++ } ++ ++ tails[priority + OFFSET] = node; ++ ++ size++; ++ } ++ ++ private void linkHead(Node node) { ++ node.next_node = head; ++ head.prev_node = node; ++ head = node; ++ } ++ ++ private void linkTail(Node node) { ++ tail.next_node = node; ++ node.prev_node = tail; ++ tail = node; ++ } ++ ++ private void linkAfter(Node prev, Node node) { ++ linkBetween(prev, node, prev.next_node); ++ } ++ ++ private void linkBetween(Node prev, Node node, Node next) { ++ prev.next_node = node; ++ node.prev_node = prev; ++ ++ node.next_node = next; ++ next.prev_node = node; ++ } ++ ++ private Node findPrev(Node node) { ++ Node prev = null; ++ ++ for (int i = node.priority + OFFSET; i < tails.length; i++) { ++ prev = tails[i]; ++ ++ if (prev != null) { ++ break; ++ } ++ } ++ ++ return prev; ++ } ++} +diff --git a/src/main/java/alternate/current/wire/SimpleQueue.java b/src/main/java/alternate/current/wire/SimpleQueue.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2b30074252551e1dc55d5be17d26fb4a2d8eb2e4 +--- /dev/null ++++ b/src/main/java/alternate/current/wire/SimpleQueue.java +@@ -0,0 +1,112 @@ ++package alternate.current.wire; ++ ++import java.util.AbstractQueue; ++import java.util.Iterator; ++ ++public class SimpleQueue extends AbstractQueue { ++ ++ private WireNode head; ++ private WireNode tail; ++ ++ private int size; ++ ++ SimpleQueue() { ++ ++ } ++ ++ @Override ++ public boolean offer(WireNode node) { ++ if (node == null) { ++ throw new NullPointerException(); ++ } ++ ++ if (tail == null) { ++ head = tail = node; ++ } else { ++ tail.next_wire = node; ++ tail = node; ++ } ++ ++ size++; ++ ++ return true; ++ } ++ ++ @Override ++ public WireNode poll() { ++ if (head == null) { ++ return null; ++ } ++ ++ WireNode node = head; ++ WireNode next = node.next_wire; ++ ++ if (next == null) { ++ head = tail = null; ++ } else { ++ node.next_wire = null; ++ head = next; ++ } ++ ++ size--; ++ ++ return node; ++ } ++ ++ @Override ++ public WireNode peek() { ++ return head; ++ } ++ ++ @Override ++ public void clear() { ++ for (WireNode node = head; node != null; ) { ++ WireNode n = node; ++ node = node.next_wire; ++ ++ n.next_wire = null; ++ } ++ ++ head = null; ++ tail = null; ++ ++ size = 0; ++ } ++ ++ @Override ++ public Iterator iterator() { ++ return new SimpleIterator(); ++ } ++ ++ @Override ++ public int size() { ++ return size; ++ } ++ ++ private class SimpleIterator implements Iterator { ++ ++ private WireNode curr; ++ private WireNode next; ++ ++ private SimpleIterator() { ++ next = head; ++ } ++ ++ @Override ++ public boolean hasNext() { ++ if (next == null && curr != null) { ++ next = curr.next_wire; ++ } ++ ++ return next != null; ++ } ++ ++ @Override ++ public WireNode next() { ++ curr = next; ++ next = curr.next_wire; ++ ++ return curr; ++ } ++ } ++} +diff --git a/src/main/java/alternate/current/wire/WireConnection.java b/src/main/java/alternate/current/wire/WireConnection.java +new file mode 100644 +index 0000000000000000000000000000000000000000..4fd8cb29024330397cfe4cbc1f237d285bfb7b3e +--- /dev/null ++++ b/src/main/java/alternate/current/wire/WireConnection.java +@@ -0,0 +1,30 @@ ++package alternate.current.wire; ++ ++/** ++ * This class represents a connection between some WireNode (the 'owner') and a ++ * neighboring WireNode. Two wires are considered to be connected if power can ++ * flow from one wire to the other (and/or vice versa). ++ * ++ * @author Space Walker ++ */ ++public class WireConnection { ++ ++ /** The connected wire. */ ++ final WireNode wire; ++ /** Cardinal direction to the connected wire. */ ++ final int iDir; ++ /** True if the owner of the connection can provide power to the connected wire. */ ++ final boolean offer; ++ /** True if the connected wire can provide power to the owner of the connection. */ ++ final boolean accept; ++ ++ /** The next connection in the sequence. */ ++ WireConnection next; ++ ++ WireConnection(WireNode wire, int iDir, boolean offer, boolean accept) { ++ this.wire = wire; ++ this.iDir = iDir; ++ this.offer = offer; ++ this.accept = accept; ++ } ++} +diff --git a/src/main/java/alternate/current/wire/WireConnectionManager.java b/src/main/java/alternate/current/wire/WireConnectionManager.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5a7209f05b549c222f6c9bc2af2a35790964947e +--- /dev/null ++++ b/src/main/java/alternate/current/wire/WireConnectionManager.java +@@ -0,0 +1,136 @@ ++package alternate.current.wire; ++ ++import java.util.Arrays; ++import java.util.function.Consumer; ++ ++import alternate.current.wire.WireHandler.Directions; ++import alternate.current.wire.WireHandler.NodeProvider; ++ ++public class WireConnectionManager { ++ ++ /** The owner of these connections. */ ++ final WireNode owner; ++ ++ /** The first connection for each cardinal direction. */ ++ private final WireConnection[] heads; ++ ++ private WireConnection head; ++ private WireConnection tail; ++ ++ /** The total number of connections. */ ++ int total; ++ ++ /** ++ * A 4 bit number that encodes in which direction(s) the owner has connections ++ * to other wires. ++ */ ++ private int flowTotal; ++ /** The direction of flow based connections to other wires. */ ++ int iFlowDir; ++ ++ WireConnectionManager(WireNode owner) { ++ this.owner = owner; ++ ++ this.heads = new WireConnection[Directions.HORIZONTAL.length]; ++ ++ this.total = 0; ++ ++ this.flowTotal = 0; ++ this.iFlowDir = -1; ++ } ++ ++ void set(NodeProvider nodes) { ++ if (total > 0) { ++ clear(); ++ } ++ ++ boolean belowIsConductor = nodes.getNeighbor(owner, Directions.DOWN).isConductor(); ++ boolean aboveIsConductor = nodes.getNeighbor(owner, Directions.UP).isConductor(); ++ ++ for (int iDir = 0; iDir < Directions.HORIZONTAL.length; iDir++) { ++ Node neighbor = nodes.getNeighbor(owner, iDir); ++ ++ if (neighbor.isWire()) { ++ add(neighbor.asWire(), iDir, true, true); ++ ++ continue; ++ } ++ ++ boolean sideIsConductor = neighbor.isConductor(); ++ ++ if (!sideIsConductor) { ++ Node node = nodes.getNeighbor(neighbor, Directions.DOWN); ++ ++ if (node.isWire()) { ++ add(node.asWire(), iDir, belowIsConductor, true); ++ } ++ } ++ if (!aboveIsConductor) { ++ Node node = nodes.getNeighbor(neighbor, Directions.UP); ++ ++ if (node.isWire()) { ++ add(node.asWire(), iDir, true, sideIsConductor); ++ } ++ } ++ } ++ ++ if (total > 0) { ++ iFlowDir = WireHandler.FLOW_IN_TO_FLOW_OUT[flowTotal]; ++ } ++ } ++ ++ private void clear() { ++ Arrays.fill(heads, null); ++ ++ head = null; ++ tail = null; ++ ++ total = 0; ++ ++ flowTotal = 0; ++ iFlowDir = -1; ++ } ++ ++ private void add(WireNode wire, int iDir, boolean offer, boolean accept) { ++ add(new WireConnection(wire, iDir, offer, accept)); ++ } ++ ++ private void add(WireConnection connection) { ++ if (head == null) { ++ head = connection; ++ tail = connection; ++ } else { ++ tail.next = connection; ++ tail = connection; ++ } ++ ++ total++; ++ ++ if (heads[connection.iDir] == null) { ++ heads[connection.iDir] = connection; ++ flowTotal |= (1 << connection.iDir); ++ } ++ } ++ ++ /** ++ * Iterate over all connections. Use this method if the iteration order is not ++ * important. ++ */ ++ void forEach(Consumer consumer) { ++ for (WireConnection c = head; c != null; c = c.next) { ++ consumer.accept(c); ++ } ++ } ++ ++ /** ++ * Iterate over all connections. Use this method if the iteration order is ++ * important. ++ */ ++ void forEach(Consumer consumer, int iFlowDir) { ++ for (int iDir : WireHandler.CARDINAL_UPDATE_ORDERS[iFlowDir]) { ++ for (WireConnection c = heads[iDir]; c != null && c.iDir == iDir; c = c.next) { ++ consumer.accept(c); ++ } ++ } ++ } ++} +diff --git a/src/main/java/alternate/current/wire/WireHandler.java b/src/main/java/alternate/current/wire/WireHandler.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e943fdcbc15d5c17450659c2cd9e0be73ae06c0b +--- /dev/null ++++ b/src/main/java/alternate/current/wire/WireHandler.java +@@ -0,0 +1,1150 @@ ++package alternate.current.wire; ++ ++import java.util.Iterator; ++import java.util.Queue; ++import java.util.function.Consumer; ++ ++import it.unimi.dsi.fastutil.longs.Long2ObjectMap; ++import it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry; ++import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; ++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.Direction; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.redstone.Redstone; ++ ++/** ++ * This class handles power changes for redstone wire. The algorithm was ++ * designed with the following goals in mind: ++ *
++ * 1. Minimize the number of times a wire checks its surroundings to determine ++ * its power level. ++ *
++ * 2. Minimize the number of block and shape updates emitted. ++ *
++ * 3. Emit block and shape updates in a deterministic, non-locational order, ++ * fixing bug MC-11193. ++ * ++ *

++ * In Vanilla redstone wire is laggy because it fails on points 1 and 2. ++ * ++ *

++ * Redstone wire updates recursively and each wire calculates its power level in ++ * isolation rather than in the context of the network it is a part of. This ++ * means a wire in a grid can change its power level over half a dozen times ++ * before settling on its final value. This problem used to be worse in 1.13 and ++ * below, where a wire would only decrease its power level by 1 at a time. ++ * ++ *

++ * In addition to this, a wire emits 42 block updates and up to 22 shape updates ++ * each time it changes its power level. ++ * ++ *

++ * Of those 42 block updates, 6 are to itself, which are thus not only ++ * redundant, but a big source of lag, since those cause the wire to ++ * unnecessarily re-calculate its power level. A block only has 24 neighbors ++ * within a Manhattan distance of 2, meaning 12 of the remaining 36 block ++ * updates are duplicates and thus also redundant. ++ * ++ *

++ * Of the 22 shape updates, only 6 are strictly necessary. The other 16 are sent ++ * to blocks diagonally above and below. These are necessary if a wire changes ++ * its connections, but not when it changes its power level. ++ * ++ *

++ * Redstone wire in Vanilla also fails on point 3, though this is more of a ++ * quality-of-life issue than a lag issue. The recursive nature in which it ++ * updates, combined with the location-dependent order in which each wire ++ * updates its neighbors, makes the order in which neighbors of a wire network ++ * are updated incredibly inconsistent and seemingly random. ++ * ++ *

++ * Alternate Current fixes each of these problems as follows. ++ * ++ *

++ * 1. To make sure a wire calculates its power level as little as possible, we ++ * remove the recursive nature in which redstone wire updates in Vanilla. ++ * Instead, we build a network of connected wires, find those wires that receive ++ * redstone power from "outside" the network, and spread the power from there. ++ * This has a few advantages: ++ *
++ * - Each wire checks for power from non-wire components at most once, and from ++ * nearby wires just twice. ++ *
++ * - Each wire only sets its power level in the world once. This is important, ++ * because calls to Level.setBlock are even more expensive than calls to ++ * Level.getBlockState. ++ * ++ *

++ * 2. There are 2 obvious ways in which we can reduce the number of block and ++ * shape updates. ++ *
++ * - Get rid of the 18 redundant block updates and 16 redundant shape updates, ++ * so each wire only emits 24 block updates and 6 shape updates whenever it ++ * changes its power level. ++ *
++ * - Only emit block updates and shape updates once a wire reaches its final ++ * power level, rather than at each intermediary stage. ++ *
++ * For an individual wire, these two optimizations are the best you can do, but ++ * for an entire grid, you can do better! ++ * ++ *

++ * Since we calculate the power of the entire network, sending block and shape ++ * updates to the wires in it is redundant. Removing those updates can reduce ++ * the number of block and shape updates by up to 20%. ++ * ++ *

++ * 3. To make the order of block updates to neighbors of a network ++ * deterministic, the first thing we must do is to replace the location- ++ * dependent order in which a wire updates its neighbors. Instead, we base it on ++ * the direction of power flow. This part of the algorithm was heavily inspired ++ * by theosib's 'RedstoneWireTurbo', which you can read more about in theosib's ++ * comment on Mojira here ++ * or by checking out its implementation in carpet mod here. ++ * ++ *

++ * The idea is to determine the direction of power flow through a wire based on ++ * the power it receives from neighboring wires. For example, if the only power ++ * a wire receives is from a neighboring wire to its west, it can be said that ++ * the direction of power flow through the wire is east. ++ * ++ *

++ * We make the order of block updates to neighbors of a wire depend on what is ++ * determined to be the direction of power flow. This not only removes ++ * locationality entirely, it even removes directionality in a large number of ++ * cases. Unlike in 'RedstoneWireTurbo', however, I have decided to keep a ++ * directional element in ambiguous cases, rather than to introduce randomness, ++ * though this is trivial to change. ++ * ++ *

++ * While this change fixes the block update order of individual wires, we must ++ * still address the overall block update order of a network. This turns out to ++ * be a simple fix, because of a change we made earlier: we search through the ++ * network for wires that receive power from outside it, and spread the power ++ * from there. If we make each wire transmit its power to neighboring wires in ++ * an order dependent on the direction of power flow, we end up with a ++ * non-locational and largely non-directional wire update order. ++ * ++ * @author Space Walker ++ */ ++public class WireHandler { ++ ++ public static class Directions { ++ ++ public static final Direction[] ALL = { Direction.WEST, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.DOWN, Direction.UP }; ++ public static final Direction[] HORIZONTAL = { Direction.WEST, Direction.NORTH, Direction.EAST, Direction.SOUTH }; ++ ++ // Indices for the arrays above. ++ // The cardinal directions are ordered clockwise. This allows ++ // for conversion between relative and absolute directions ++ // ('left' 'right' vs 'east' 'west') with simple arithmetic: ++ // If some Direction index 'iDir' is considered 'forward', then ++ // '(iDir + 1) & 0b11' is 'right', '(iDir + 2) & 0b11' is 'backward', etc. ++ public static final int WEST = 0b000; // 0 ++ public static final int NORTH = 0b001; // 1 ++ public static final int EAST = 0b010; // 2 ++ public static final int SOUTH = 0b011; // 3 ++ public static final int DOWN = 0b100; // 4 ++ public static final int UP = 0b101; // 5 ++ ++ public static int iOpposite(int iDir) { ++ return iDir ^ (0b10 >>> (iDir >>> 2)); ++ } ++ ++ // Each array is placed at the index that encodes the direction that is missing ++ // from the array. ++ private static final int[][] I_EXCEPT = { ++ { NORTH, EAST, SOUTH, DOWN, UP }, ++ { WEST, EAST, SOUTH, DOWN, UP }, ++ { WEST, NORTH, SOUTH, DOWN, UP }, ++ { WEST, NORTH, EAST, DOWN, UP }, ++ { WEST, NORTH, EAST, SOUTH, UP }, ++ { WEST, NORTH, EAST, SOUTH, DOWN } ++ }; ++ private static final int[][] I_EXCEPT_CARDINAL = { ++ { NORTH, EAST, SOUTH }, ++ { WEST, EAST, SOUTH }, ++ { WEST, NORTH, SOUTH }, ++ { WEST, NORTH, EAST, }, ++ { WEST, NORTH, EAST, SOUTH }, ++ { WEST, NORTH, EAST, SOUTH } ++ }; ++ } ++ ++ /** ++ * This conversion table takes in information about incoming flow, and outputs ++ * the determined outgoing flow. ++ * ++ *

++ * The input is a 4 bit number that encodes the incoming flow. Each bit ++ * represents a cardinal direction, and when it is 'on', there is flow in that ++ * direction. ++ * ++ *

++ * The output is a single Direction index, or -1 for ambiguous cases. ++ * ++ *

++ * The outgoing flow is determined as follows: ++ * ++ *

++ * If there is just 1 direction of incoming flow, that direction will be the ++ * direction of outgoing flow. ++ * ++ *

++ * If there are 2 directions of incoming flow, and these directions are not each ++ * other's opposites, the direction that is 'more clockwise' will be the ++ * direction of outgoing flow. More precisely, the direction that is 1 clockwise ++ * turn from the other is picked. ++ * ++ *

++ * If there are 3 directions of incoming flow, the two opposing directions ++ * cancel each other out, and the remaining direction will be the direction of ++ * outgoing flow. ++ * ++ *

++ * In all other cases, the flow is completely ambiguous. ++ */ ++ static final int[] FLOW_IN_TO_FLOW_OUT = { ++ -1, // 0b0000: - -> x ++ Directions.WEST, // 0b0001: west -> west ++ Directions.NORTH, // 0b0010: north -> north ++ Directions.NORTH, // 0b0011: west/north -> north ++ Directions.EAST, // 0b0100: east -> east ++ -1, // 0b0101: west/east -> x ++ Directions.EAST, // 0b0110: north/east -> east ++ Directions.NORTH, // 0b0111: west/north/east -> north ++ Directions.SOUTH, // 0b1000: south -> south ++ Directions.WEST, // 0b1001: west/south -> west ++ -1, // 0b1010: north/south -> x ++ Directions.WEST, // 0b1011: west/north/south -> west ++ Directions.SOUTH, // 0b1100: east/south -> south ++ Directions.SOUTH, // 0b1101: west/east/south -> south ++ Directions.EAST, // 0b1110: north/east/south -> east ++ -1, // 0b1111: west/north/east/south -> x ++ }; ++ /** ++ * Update orders of all directions. Given that the index encodes the direction ++ * that is to be considered 'forward', the resulting update order is ++ * { front, back, right, left, down, up }. ++ */ ++ static final int[][] FULL_UPDATE_ORDERS = { ++ { Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH, Directions.DOWN, Directions.UP }, ++ { Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST , Directions.DOWN, Directions.UP }, ++ { Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH, Directions.DOWN, Directions.UP }, ++ { Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST , Directions.DOWN, Directions.UP } ++ }; ++ /** ++ * The default update order of all directions. It is equivalent to the order of ++ * shape updates in vanilla Minecraft. ++ */ ++ static final int[] DEFAULT_FULL_UPDATE_ORDER = FULL_UPDATE_ORDERS[0]; ++ /** ++ * Update orders of cardinal directions. Given that the index encodes the ++ * direction that is to be considered 'forward', the resulting update order is ++ * { front, back, right, left }. ++ */ ++ static final int[][] CARDINAL_UPDATE_ORDERS = { ++ { Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH }, ++ { Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST }, ++ { Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH }, ++ { Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST } ++ }; ++ /** ++ * The default update order of all cardinal directions. ++ */ ++ static final int[] DEFAULT_CARDINAL_UPDATE_ORDER = CARDINAL_UPDATE_ORDERS[0]; ++ ++ private static final int POWER_MIN = Redstone.SIGNAL_MIN; ++ private static final int POWER_MAX = Redstone.SIGNAL_MAX; ++ private static final int POWER_STEP = 1; ++ ++ // If Vanilla will ever multi-thread the ticking of levels, there should ++ // be only one WireHandler per level, in case redstone updates in multiple ++ // levels at the same time. There are already mods that add multi-threading ++ // as well. ++ private final ServerLevel level; ++ ++ /** Map of wires and neighboring blocks. */ ++ private final Long2ObjectMap nodes; ++ /** Queue for the breadth-first search through the network. */ ++ private final Queue search; ++ /** Queue of updates to wires and neighboring blocks. */ ++ private final Queue updates; ++ ++ // Rather than creating new nodes every time a network is updated we keep ++ // a cache of nodes that can be re-used. ++ private Node[] nodeCache; ++ private int nodeCount; ++ ++ /** Is this WireHandler currently working through the update queue? */ ++ private boolean updating; ++ ++ public WireHandler(ServerLevel level) { ++ this.level = level; ++ ++ this.nodes = new Long2ObjectOpenHashMap<>(); ++ this.search = new SimpleQueue(); ++ this.updates = new PriorityQueue(); ++ ++ this.nodeCache = new Node[16]; ++ this.fillNodeCache(0, 16); ++ } ++ ++ /** ++ * Retrieve the {@link Node Node} that represents the ++ * block at the given position in the level. ++ */ ++ private Node getOrAddNode(BlockPos pos) { ++ return nodes.compute(pos.asLong(), (key, node) -> { ++ if (node == null) { ++ // If there is not yet a node at this position, retrieve and ++ // update one from the cache. ++ return getNextNode(pos); ++ } ++ if (node.invalid) { ++ return revalidateNode(node); ++ } ++ ++ return node; ++ }); ++ } ++ ++ /** ++ * Remove and return the {@link Node Node} at the given ++ * position. ++ */ ++ private Node removeNode(BlockPos pos) { ++ return nodes.remove(pos.asLong()); ++ } ++ ++ /** ++ * Return a {@link Node Node} that represents the block ++ * at the given position. ++ */ ++ private Node getNextNode(BlockPos pos) { ++ return getNextNode(pos, level.getBlockState(pos)); ++ } ++ ++ /** ++ * Return a node that represents the given position and block state. If it is a ++ * wire, then create a new {@link WireNode WireNode}. ++ * Otherwise, grab the next {@link Node Node} from the ++ * cache and update it. ++ */ ++ private Node getNextNode(BlockPos pos, BlockState state) { ++ return state.is(Blocks.REDSTONE_WIRE) ? new WireNode(level, pos, state) : getNextNode().set(pos, state, true); ++ } ++ ++ /** ++ * Grab the first unused node from the cache. If all of the cache is already in ++ * use, increase it in size first. ++ */ ++ private Node getNextNode() { ++ if (nodeCount == nodeCache.length) { ++ increaseNodeCache(); ++ } ++ ++ return nodeCache[nodeCount++]; ++ } ++ ++ private void increaseNodeCache() { ++ Node[] oldCache = nodeCache; ++ nodeCache = new Node[oldCache.length << 1]; ++ ++ for (int index = 0; index < oldCache.length; index++) { ++ nodeCache[index] = oldCache[index]; ++ } ++ ++ fillNodeCache(oldCache.length, nodeCache.length); ++ } ++ ++ private void fillNodeCache(int start, int end) { ++ for (int index = start; index < end; index++) { ++ nodeCache[index] = new Node(level); ++ } ++ } ++ ++ /** ++ * Try to revalidate the given node by looking at the block state that is ++ * occupying its position. If the given node is a wire but the block state is ++ * not, or vice versa, a new node must be created/grabbed from the cache. ++ * Otherwise, the node can be quickly revalidated with the new block state. ++ */ ++ private Node revalidateNode(Node node) { ++ BlockPos pos = node.pos; ++ BlockState state = level.getBlockState(pos); ++ ++ boolean wasWire = node.isWire(); ++ boolean isWire = state.is(Blocks.REDSTONE_WIRE); ++ ++ if (wasWire != isWire) { ++ return getNextNode(pos, state); ++ } ++ ++ node.invalid = false; ++ ++ if (isWire) { ++ // No need to update the block state of this wire - it will grab ++ // the current block state just before setting power anyway. ++ WireNode wire = node.asWire(); ++ ++ wire.root = false; ++ wire.discovered = false; ++ wire.searched = false; ++ } else { ++ node.set(pos, state, false); ++ } ++ ++ return node; ++ } ++ ++ /** ++ * Retrieve the neighbor of a node in the given direction and create a link ++ * between the two nodes if they are not yet linked. This link makes accessing ++ * neighbors of a node signficantly faster. ++ */ ++ private Node getNeighbor(Node node, int iDir) { ++ Node neighbor = node.neighbors[iDir]; ++ ++ if (neighbor == null || neighbor.invalid) { ++ Direction dir = Directions.ALL[iDir]; ++ BlockPos pos = node.pos.relative(dir); ++ ++ Node oldNeighbor = neighbor; ++ neighbor = getOrAddNode(pos); ++ ++ if (neighbor != oldNeighbor) { ++ int iOpp = Directions.iOpposite(iDir); ++ ++ node.neighbors[iDir] = neighbor; ++ neighbor.neighbors[iOpp] = node; ++ } ++ } ++ ++ return neighbor; ++ } ++ ++ /** ++ * Iterate over all neighboring nodes of the given wire. The iteration order is ++ * designed to be an extension of the default block update order, and is ++ * determined as follows: ++ *
++ * 1. The direction of power flow through the wire is to be considered ++ * 'forward'. The iteration order depends on the neighbors' relative positions ++ * to the wire. ++ *
++ * 2. Each neighbor is identified by the step(s) you must take, starting at the ++ * wire, to reach it. Each step is 1 block, thus the position of a neighbor is ++ * encoded by the direction(s) of the step(s), e.g. (right), (down), (up, left), ++ * etc. ++ *
++ * 3. Neighbors are iterated over in pairs that lie on opposite sides of the ++ * wire. ++ *
++ * 4. Neighbors are iterated over in order of their distance from the wire. This ++ * means they are iterated over in 3 groups: direct neighbors first, then ++ * diagonal neighbors, and last are the far neighbors that are 2 blocks directly ++ * out. ++ *
++ * 5. The order within each group is determined using the following basic order: ++ * { front, back, right, left, down, up }. This order was chosen because it ++ * converts to the following order of absolute directions when west is said to ++ * be 'forward': { west, east, north, south, down, up } - this is the order of ++ * shape updates. ++ */ ++ private void forEachNeighbor(WireNode wire, Consumer consumer) { ++ int forward = wire.iFlowDir; ++ int rightward = (forward + 1) & 0b11; ++ int backward = (forward + 2) & 0b11; ++ int leftward = (forward + 3) & 0b11; ++ int downward = Directions.DOWN; ++ int upward = Directions.UP; ++ ++ Node front = getNeighbor(wire, forward); ++ Node right = getNeighbor(wire, rightward); ++ Node back = getNeighbor(wire, backward); ++ Node left = getNeighbor(wire, leftward); ++ Node below = getNeighbor(wire, downward); ++ Node above = getNeighbor(wire, upward); ++ ++ // direct neighbors (6) ++ consumer.accept(front); ++ consumer.accept(back); ++ consumer.accept(right); ++ consumer.accept(left); ++ consumer.accept(below); ++ consumer.accept(above); ++ ++ // diagonal neighbors (12) ++ consumer.accept(getNeighbor(front, rightward)); ++ consumer.accept(getNeighbor(back, leftward)); ++ consumer.accept(getNeighbor(front, leftward)); ++ consumer.accept(getNeighbor(back, rightward)); ++ consumer.accept(getNeighbor(front, downward)); ++ consumer.accept(getNeighbor(back, upward)); ++ consumer.accept(getNeighbor(front, upward)); ++ consumer.accept(getNeighbor(back, downward)); ++ consumer.accept(getNeighbor(right, downward)); ++ consumer.accept(getNeighbor(left, upward)); ++ consumer.accept(getNeighbor(right, upward)); ++ consumer.accept(getNeighbor(left, downward)); ++ ++ // far neighbors (6) ++ consumer.accept(getNeighbor(front, forward)); ++ consumer.accept(getNeighbor(back, backward)); ++ consumer.accept(getNeighbor(right, rightward)); ++ consumer.accept(getNeighbor(left, leftward)); ++ consumer.accept(getNeighbor(below, downward)); ++ consumer.accept(getNeighbor(above, upward)); ++ } ++ ++ /** ++ * This method should be called whenever a wire receives a block update. ++ */ ++ public void onWireUpdated(BlockPos pos) { ++ invalidate(); ++ findRoots(pos); ++ tryUpdate(); ++ } ++ ++ /** ++ * This method should be called whenever a wire is placed. ++ */ ++ public void onWireAdded(BlockPos pos) { ++ Node node = getOrAddNode(pos); ++ ++ if (!node.isWire()) { ++ return; // we should never get here ++ } ++ ++ WireNode wire = node.asWire(); ++ wire.added = true; ++ ++ invalidate(); ++ revalidateNode(wire); ++ findRoot(wire); ++ tryUpdate(); ++ } ++ ++ /** ++ * This method should be called whenever a wire is removed. ++ */ ++ public void onWireRemoved(BlockPos pos, BlockState state) { ++ Node node = removeNode(pos); ++ WireNode wire; ++ ++ if (node == null || !node.isWire()) { ++ wire = new WireNode(level, pos, state); ++ } else { ++ wire = node.asWire(); ++ } ++ ++ wire.invalid = true; ++ wire.removed = true; ++ ++ // If these fields are set to 'true', the removal of this wire was part of ++ // already ongoing power changes, so we can exit early here. ++ if (updating && wire.shouldBreak) { ++ return; ++ } ++ ++ invalidate(); ++ revalidateNode(wire); ++ findRoot(wire); ++ tryUpdate(); ++ } ++ ++ /** ++ * The nodes map is a snapshot of the state of the world. It becomes invalid ++ * when power changes are carried out, since the block and shape updates can ++ * lead to block changes. If these block changes cause the network to be updated ++ * again every node must be invalidated, and revalidated before it is used ++ * again. This ensures the power calculations of the network are accurate. ++ */ ++ private void invalidate() { ++ if (updating && !nodes.isEmpty()) { ++ Iterator> it = Long2ObjectMaps.fastIterator(nodes); ++ ++ while (it.hasNext()) { ++ Entry entry = it.next(); ++ Node node = entry.getValue(); ++ ++ node.invalid = true; ++ } ++ } ++ } ++ ++ /** ++ * Look for wires at and around the given position that are in an invalid state ++ * and require power changes. These wires are called 'roots' because it is only ++ * when these wires change power level that neighboring wires must adjust as ++ * well. ++ * ++ *

++ * While it is strictly only necessary to check the wire at the given position, ++ * if that wire is part of a network, it is beneficial to check its surroundings ++ * for other wires that require power changes. This is because a network can ++ * receive power at multiple points. Consider the following setup: ++ * ++ *

++ * (top-down view, W = wire, L = lever, _ = air/other) ++ *
{@code _ _ W _ _ } ++ *
{@code _ W W W _ } ++ *
{@code W W L W W } ++ *
{@code _ W W W _ } ++ *
{@code _ _ W _ _ } ++ * ++ *

++ * The lever powers four wires in the network at once. If this is identified ++ * correctly, the entire network can (un)power at once. While it is not ++ * practical to cover every possible situation where a network is (un)powered ++ * from multiple points at once, checking for common cases like the one ++ * described above is relatively straight-forward. ++ */ ++ private void findRoots(BlockPos pos) { ++ Node node = getOrAddNode(pos); ++ ++ if (!node.isWire()) { ++ return; // we should never get here ++ } ++ ++ WireNode wire = node.asWire(); ++ findRoot(wire); ++ ++ // If the wire at the given position is not in an invalid state ++ // we can exit early. ++ if (!wire.searched) { ++ return; ++ } ++ ++ for (int iDir : FULL_UPDATE_ORDERS[wire.iFlowDir]) { ++ Node neighbor = getNeighbor(wire, iDir); ++ ++ if (neighbor.isConductor() || neighbor.isSignalSource()) { ++ findRootsAround(neighbor, Directions.iOpposite(iDir)); ++ } ++ } ++ } ++ ++ /** ++ * Look for wires around the given node that require power changes. ++ */ ++ private void findRootsAround(Node node, int except) { ++ for (int iDir : Directions.I_EXCEPT_CARDINAL[except]) { ++ Node neighbor = getNeighbor(node, iDir); ++ ++ if (neighbor.isWire()) { ++ findRoot(neighbor.asWire()); ++ } ++ } ++ } ++ ++ /** ++ * Check if the given wire requires power changes. If it does, queue it for the ++ * breadth-first search as a root. ++ */ ++ private void findRoot(WireNode wire) { ++ // Each wire only needs to be checked once. ++ if (wire.discovered) { ++ return; ++ } ++ ++ discover(wire); ++ findExternalPower(wire); ++ findPower(wire, false); ++ ++ if (needsUpdate(wire)) { ++ searchRoot(wire); ++ } ++ } ++ ++ /** ++ * Prepare the given wire for the breadth-first search. This means: ++ *
++ * - Check if the wire should break. Rather than breaking the wire right away, ++ * its effects are integrated into the power calculations. ++ *
++ * - Reset the virtual and external power. ++ *
++ * - Find connections to neighboring wires. ++ */ ++ private void discover(WireNode wire) { ++ if (wire.discovered) { ++ return; ++ } ++ ++ wire.discovered = true; ++ wire.searched = false; ++ ++ if (!wire.removed && !wire.shouldBreak && !wire.state.canSurvive(level, wire.pos)) { ++ wire.shouldBreak = true; ++ } ++ ++ wire.virtualPower = wire.currentPower; ++ wire.externalPower = POWER_MIN - 1; ++ ++ wire.connections.set(this::getNeighbor); ++ } ++ ++ /** ++ * Determine the power level the given wire receives from the blocks around it. ++ * Power from non-wire components only needs to be computed if power from ++ * neighboring wires has decreased, so as to determine how low the power of the ++ * wire can fall. ++ */ ++ private void findPower(WireNode wire, boolean ignoreSearched) { ++ // As wire power is (re-)computed, flow information must be reset. ++ wire.virtualPower = wire.externalPower; ++ wire.flowIn = 0; ++ ++ // If the wire is removed or going to break, its power level should always be ++ // the minimum value. This is because it (effectively) no longer exists, so ++ // cannot provide any power to neighboring wires. ++ if (wire.removed || wire.shouldBreak) { ++ return; ++ } ++ ++ // Power received from neighboring wires will never exceed POWER_MAX - ++ // POWER_STEP, so if the external power is already larger than or equal to ++ // that, there is no need to check for power from neighboring wires. ++ if (wire.externalPower < (POWER_MAX - POWER_STEP)) { ++ findWirePower(wire, ignoreSearched); ++ } ++ } ++ ++ /** ++ * Determine the power the given wire receives from connected neighboring wires ++ * and update the virtual power accordingly. ++ */ ++ private void findWirePower(WireNode wire, boolean ignoreSearched) { ++ wire.connections.forEach(connection -> { ++ if (!connection.accept) { ++ return; ++ } ++ ++ WireNode neighbor = connection.wire; ++ ++ if (!ignoreSearched || !neighbor.searched) { ++ int power = Math.max(POWER_MIN, neighbor.virtualPower - POWER_STEP); ++ int iOpp = Directions.iOpposite(connection.iDir); ++ ++ wire.offerPower(power, iOpp); ++ } ++ }); ++ } ++ ++ /** ++ * Determine the redstone signal the given wire receives from non-wire ++ * components and update the virtual power accordingly. ++ */ ++ private void findExternalPower(WireNode wire) { ++ // If the wire is removed or going to break, its power level should always be ++ // the minimum value. Thus external power need not be computed. ++ // In other cases external power need only be computed once. ++ if (wire.removed || wire.shouldBreak || wire.externalPower >= POWER_MIN) { ++ return; ++ } ++ ++ wire.externalPower = getExternalPower(wire); ++ ++ if (wire.externalPower > wire.virtualPower) { ++ wire.virtualPower = wire.externalPower; ++ } ++ } ++ ++ /** ++ * Determine the redstone signal the given wire receives from non-wire ++ * components. ++ */ ++ private int getExternalPower(WireNode wire) { ++ int power = POWER_MIN; ++ ++ for (int iDir = 0; iDir < Directions.ALL.length; iDir++) { ++ Node neighbor = getNeighbor(wire, iDir); ++ ++ // Power from wires is handled separately. ++ if (neighbor.isWire()) { ++ continue; ++ } ++ ++ // Since 1.16 there is a block that is both a conductor and a signal ++ // source: the target block! ++ if (neighbor.isConductor()) { ++ power = Math.max(power, getDirectSignalTo(wire, neighbor, Directions.iOpposite(iDir))); ++ } ++ if (neighbor.isSignalSource()) { ++ power = Math.max(power, neighbor.state.getSignal(level, neighbor.pos, Directions.ALL[iDir])); ++ } ++ ++ if (power >= POWER_MAX) { ++ return POWER_MAX; ++ } ++ } ++ ++ return power; ++ } ++ ++ /** ++ * Determine the direct signal the given wire receives from neighboring blocks ++ * through the given conductor node. ++ */ ++ private int getDirectSignalTo(WireNode wire, Node node, int except) { ++ int power = POWER_MIN; ++ ++ for (int iDir : Directions.I_EXCEPT[except]) { ++ Node neighbor = getNeighbor(node, iDir); ++ ++ if (neighbor.isSignalSource()) { ++ power = Math.max(power, neighbor.state.getDirectSignal(level, neighbor.pos, Directions.ALL[iDir])); ++ ++ if (power >= POWER_MAX) { ++ return POWER_MAX; ++ } ++ } ++ } ++ ++ return power; ++ } ++ ++ /** ++ * Check if the given wire needs to update its state in the world. ++ */ ++ private boolean needsUpdate(WireNode wire) { ++ return wire.removed || wire.shouldBreak || wire.virtualPower != wire.currentPower; ++ } ++ ++ /** ++ * Queue the given wire for the breadth-first search as a root. ++ */ ++ private void searchRoot(WireNode wire) { ++ int iBackupFlowDir; ++ ++ if (wire.connections.iFlowDir < 0) { ++ iBackupFlowDir = 0; ++ } else { ++ iBackupFlowDir = wire.connections.iFlowDir; ++ } ++ ++ search(wire, true, iBackupFlowDir); ++ } ++ ++ /** ++ * Queue the given wire for the breadth-first search and set a backup flow ++ * direction. ++ */ ++ private void search(WireNode wire, boolean root, int iBackupFlowDir) { ++ search.offer(wire); ++ ++ wire.root = root; ++ wire.searched = true; ++ // Normally the flow is not set until the power level is updated. However, ++ // in networks with multiple power sources the update order between them ++ // depends on which was discovered first. To make this less prone to ++ // directionality, each wire node is given a 'backup' flow. For roots, this ++ // is the determined flow of their connections. For non-roots this is the ++ // direction from which they were discovered. ++ wire.iFlowDir = iBackupFlowDir; ++ } ++ ++ private void tryUpdate() { ++ if (!search.isEmpty()) { ++ update(); ++ } ++ if (!updating) { ++ nodes.clear(); ++ nodeCount = 0; ++ } ++ } ++ ++ /** ++ * Update the network and neighboring blocks. This is done in 3 steps. ++ * ++ *

++ * 1. Search through the network ++ *
++ * Conduct a breadth-first search around the roots to find wires that are in an ++ * invalid state and need power changes. ++ * ++ *

++ * 2. Depower the network ++ *
++ * Depower all wires in the network. This allows power to be spread most ++ * efficiently. ++ * ++ *

++ * 3. Power the network ++ *
++ * Work through the update queue, setting the new power level of each wire and ++ * updating neighboring blocks. After a wire has updated its power level, it ++ * will emit shape updates and queue updates for neighboring wires and blocks. ++ */ ++ private void update() { ++ // Search through the network for wires that need power changes. This includes ++ // the roots as well as any wires that will be affected by power changes to ++ // those roots. ++ searchNetwork(); ++ ++ // Depower all the wires in the network. ++ depowerNetwork(); ++ ++ // Bring each wire up to its new power level and update neighboring blocks. ++ try { ++ powerNetwork(); ++ } catch (Throwable t) { ++ // If anything goes wrong while carrying out power changes, this field must ++ // be reset to 'false', or the wire handler will be locked out of carrying ++ // out power changes until the world is reloaded. ++ updating = false; ++ ++ throw t; ++ } ++ } ++ ++ /** ++ * Search through the network for wires that are in an invalid state and need ++ * power changes. These wires are added to the end of the queue, so that their ++ * neighbors can be searched next. ++ */ ++ private void searchNetwork() { ++ for (WireNode wire : search) { ++ // The order in which wires are searched will influence the order in ++ // which they update their power levels. ++ wire.connections.forEach(connection -> { ++ if (!connection.offer) { ++ return; ++ } ++ ++ WireNode neighbor = connection.wire; ++ ++ if (neighbor.searched) { ++ return; ++ } ++ ++ discover(neighbor); ++ findPower(neighbor, false); ++ ++ // If power from neighboring wires has decreased, check for power ++ // from non-wire components so as to determine how low power can ++ // fall. ++ if (neighbor.virtualPower < neighbor.currentPower) { ++ findExternalPower(neighbor); ++ } ++ ++ if (needsUpdate(neighbor)) { ++ search(neighbor, false, connection.iDir); ++ } ++ }, wire.iFlowDir); ++ } ++ } ++ ++ /** ++ * Depower all wires in the network so that power can be spread from the power ++ * sources. ++ */ ++ private void depowerNetwork() { ++ while (!search.isEmpty()) { ++ WireNode wire = search.poll(); ++ findPower(wire, true); ++ ++ if (wire.root || wire.removed || wire.shouldBreak || wire.virtualPower > POWER_MIN) { ++ queueWire(wire); ++ } else { ++ // Wires that do not receive any power do not queue power changes ++ // until they are offered power from a neighboring wire. To ensure ++ // that they accept any power from neighboring wires and thus queue ++ // their power changes, their virtual power is set to below the ++ // minimum. ++ wire.virtualPower--; ++ } ++ } ++ } ++ ++ /** ++ * Work through the update queue, setting the new power level of each wire, then ++ * queueing updates to connected wires and neighboring blocks. ++ */ ++ private void powerNetwork() { ++ // If an instantaneous update chain causes updates to another network ++ // (or the same network in another place), new power changes will be ++ // integrated into the already ongoing power queue, so we can exit early ++ // here. ++ if (updating) { ++ return; ++ } ++ ++ updating = true; ++ ++ while (!updates.isEmpty()) { ++ Node node = updates.poll(); ++ ++ if (node.isWire()) { ++ WireNode wire = node.asWire(); ++ ++ if (!needsUpdate(wire)) { ++ continue; ++ } ++ ++ findPowerFlow(wire); ++ transmitPower(wire); ++ ++ if (wire.setPower()) { ++ queueNeighbors(wire); ++ ++ // If the wire was newly placed or removed, shape updates have ++ // already been emitted. However, unlike before 1.19, neighbor ++ // updates are now queued, so to preserve behavior parity with ++ // previous versions, we emit extra shape updates here to ++ // notify neighboring observers. ++ updateNeighborShapes(wire); ++ } ++ } else { ++ WireNode neighborWire = node.neighborWire; ++ ++ if (neighborWire != null) { ++ BlockPos neighborPos = neighborWire.pos; ++ Block neighborBlock = neighborWire.state.getBlock(); ++ ++ updateBlock(node, neighborPos, neighborBlock); ++ } ++ } ++ } ++ ++ updating = false; ++ } ++ ++ /** ++ * Use the information of incoming power flow to determine the direction of ++ * power flow through this wire. If that flow is ambiguous, try to use a flow ++ * direction based on connections to neighboring wires. If that is also ++ * ambiguous, use the backup value that was set when the wire was first added to ++ * the network. ++ */ ++ private void findPowerFlow(WireNode wire) { ++ int flow = FLOW_IN_TO_FLOW_OUT[wire.flowIn]; ++ ++ if (flow >= 0) { ++ wire.iFlowDir = flow; ++ } else if (wire.connections.iFlowDir >= 0) { ++ wire.iFlowDir = wire.connections.iFlowDir; ++ } ++ } ++ ++ /** ++ * Transmit power from the given wire to neighboring wires and queue updates to ++ * those wires. ++ */ ++ private void transmitPower(WireNode wire) { ++ wire.connections.forEach(connection -> { ++ if (!connection.offer) { ++ return; ++ } ++ ++ WireNode neighbor = connection.wire; ++ ++ int power = Math.max(POWER_MIN, wire.virtualPower - POWER_STEP); ++ int iDir = connection.iDir; ++ ++ if (neighbor.offerPower(power, iDir)) { ++ queueWire(neighbor); ++ } ++ }, wire.iFlowDir); ++ } ++ ++ /** ++ * Emit shape updates around the given wire. ++ */ ++ private void updateNeighborShapes(WireNode wire) { ++ BlockPos wirePos = wire.pos; ++ BlockState wireState = wire.state; ++ ++ for (int iDir : DEFAULT_FULL_UPDATE_ORDER) { ++ Node neighbor = getNeighbor(wire, iDir); ++ ++ if (!neighbor.isWire()) { ++ int iOpp = Directions.iOpposite(iDir); ++ Direction opp = Directions.ALL[iOpp]; ++ ++ updateShape(neighbor, opp, wirePos, wireState); ++ } ++ } ++ } ++ ++ private void updateShape(Node node, Direction dir, BlockPos neighborPos, BlockState neighborState) { ++ BlockPos pos = node.pos; ++ BlockState state = level.getBlockState(pos); ++ ++ // Shape updates to redstone wire are very expensive, and should never happen ++ // as a result of power changes anyway. ++ if (!state.isAir() && !state.is(Blocks.REDSTONE_WIRE)) { ++ BlockState newState = state.updateShape(dir, neighborState, level, pos, neighborPos); ++ Block.updateOrDestroy(state, newState, level, pos, Block.UPDATE_CLIENTS); ++ } ++ } ++ ++ /** ++ * Queue block updates to nodes around the given wire. ++ */ ++ private void queueNeighbors(WireNode wire) { ++ forEachNeighbor(wire, neighbor -> { ++ queueNeighbor(neighbor, wire); ++ }); ++ } ++ ++ /** ++ * Queue the given node for an update from the given neighboring wire. ++ */ ++ private void queueNeighbor(Node node, WireNode neighborWire) { ++ // Updates to wires are queued when power is transmitted. ++ if (!node.isWire()) { ++ node.neighborWire = neighborWire; ++ updates.offer(node); ++ } ++ } ++ ++ /** ++ * Queue the given wire for a power change. If the wire does not need a power ++ * change (perhaps because its power has already changed), transmit power to ++ * neighboring wires. ++ */ ++ private void queueWire(WireNode wire) { ++ if (needsUpdate(wire)) { ++ updates.offer(wire); ++ } else { ++ findPowerFlow(wire); ++ transmitPower(wire); ++ } ++ } ++ ++ /** ++ * Emit a block update to the given node. ++ */ ++ private void updateBlock(Node node, BlockPos neighborPos, Block neighborBlock) { ++ BlockPos pos = node.pos; ++ BlockState state = level.getBlockState(pos); ++ ++ // While this check makes sure wires in the network are not given block ++ // updates, it also prevents block updates to wires in neighboring networks. ++ // While this should not make a difference in theory, in practice, it is ++ // possible to force a network into an invalid state without updating it, even ++ // if it is relatively obscure. ++ // While I was willing to make this compromise in return for some significant ++ // performance gains in certain setups, if you are not, you can add all the ++ // positions of the network to a set and filter out block updates to wires in ++ // the network that way. ++ if (!state.isAir() && !state.is(Blocks.REDSTONE_WIRE)) { ++ state.handleNeighborChanged(level, pos, neighborBlock, neighborPos, false); ++ } ++ } ++ ++ @FunctionalInterface ++ public static interface NodeProvider { ++ ++ public Node getNeighbor(Node node, int iDir); ++ ++ } ++} +diff --git a/src/main/java/alternate/current/wire/WireNode.java b/src/main/java/alternate/current/wire/WireNode.java +new file mode 100644 +index 0000000000000000000000000000000000000000..33cd90c30c22200a4e1ae64f40a0bf7864546b33 +--- /dev/null ++++ b/src/main/java/alternate/current/wire/WireNode.java +@@ -0,0 +1,122 @@ ++package alternate.current.wire; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.util.Mth; ++import net.minecraft.world.level.block.Block; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.block.RedStoneWireBlock; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.level.redstone.Redstone; ++ ++/** ++ * A WireNode is a Node that represents a wire in the world. It stores all the ++ * information about the wire that the WireHandler needs to calculate power ++ * changes. ++ * ++ * @author Space Walker ++ */ ++public class WireNode extends Node { ++ ++ final WireConnectionManager connections; ++ ++ /** The power level this wire currently holds in the world. */ ++ int currentPower; ++ /** ++ * While calculating power changes for a network, this field is used to keep ++ * track of the power level this wire should have. ++ */ ++ int virtualPower; ++ /** The power level received from non-wire components. */ ++ int externalPower; ++ /** ++ * A 4-bit number that keeps track of the power flow of the wires that give this ++ * wire its power level. ++ */ ++ int flowIn; ++ /** The direction of power flow, based on the incoming flow. */ ++ int iFlowDir; ++ boolean added; ++ boolean removed; ++ boolean shouldBreak; ++ boolean root; ++ boolean discovered; ++ boolean searched; ++ ++ /** The next wire in the simple queue. */ ++ WireNode next_wire; ++ ++ WireNode(ServerLevel level, BlockPos pos, BlockState state) { ++ super(level); ++ ++ this.pos = pos.immutable(); ++ this.state = state; ++ ++ this.connections = new WireConnectionManager(this); ++ ++ this.virtualPower = this.currentPower = this.state.getValue(RedStoneWireBlock.POWER); ++ this.priority = priority(); ++ } ++ ++ @Override ++ Node set(BlockPos pos, BlockState state, boolean clearNeighbors) { ++ throw new UnsupportedOperationException("Cannot update a WireNode!"); ++ } ++ ++ @Override ++ int priority() { ++ return Mth.clamp(virtualPower, Redstone.SIGNAL_MIN, Redstone.SIGNAL_MAX); ++ } ++ ++ @Override ++ public boolean isWire() { ++ return true; ++ } ++ ++ @Override ++ public WireNode asWire() { ++ return this; ++ } ++ ++ boolean offerPower(int power, int iDir) { ++ if (removed || shouldBreak) { ++ return false; ++ } ++ if (power == virtualPower) { ++ flowIn |= (1 << iDir); ++ return false; ++ } ++ if (power > virtualPower) { ++ virtualPower = power; ++ flowIn = (1 << iDir); ++ ++ return true; ++ } ++ ++ return false; ++ } ++ ++ boolean setPower() { ++ if (removed) { ++ return true; ++ } ++ ++ state = level.getBlockState(pos); ++ ++ if (!state.is(Blocks.REDSTONE_WIRE)) { ++ return false; // we should never get here ++ } ++ ++ if (shouldBreak) { ++ Block.dropResources(state, level, pos); ++ level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_CLIENTS); ++ ++ return true; ++ } ++ ++ currentPower = LevelHelper.doRedstoneEvent(level, pos, currentPower, Mth.clamp(virtualPower, Redstone.SIGNAL_MIN, Redstone.SIGNAL_MAX)); ++ state = state.setValue(RedStoneWireBlock.POWER, currentPower); ++ ++ return LevelHelper.setWireState(level, pos, state, added); ++ } ++} +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index aabaaf89f5e344791604a7e05122d9865bb0224e..2bfebbe0bc5a7f72c9ebe60fded260b3f94ac749 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -228,6 +228,7 @@ public class ServerLevel extends Level implements WorldGenLevel { + public final UUID uuid; + public boolean hasPhysicsEvent = true; // Paper - BlockPhysicsEvent + public boolean hasEntityMoveEvent; // Paper - Add EntityMoveEvent ++ private final alternate.current.wire.WireHandler wireHandler = new alternate.current.wire.WireHandler(this); // Paper - optimize redstone (Alternate Current) + + public LevelChunk getChunkIfLoaded(int x, int z) { + return this.chunkSource.getChunkAtIfLoadedImmediately(x, z); // Paper - Use getChunkIfLoadedImmediately +@@ -2566,6 +2567,13 @@ public class ServerLevel extends Level implements WorldGenLevel { + return crashreportsystemdetails; + } + ++ // Paper start - optimize redstone (Alternate Current) ++ @Override ++ public alternate.current.wire.WireHandler getWireHandler() { ++ return wireHandler; ++ } ++ // Paper end - optimize redstone (Alternate Current) ++ + private final class EntityCallbacks implements LevelCallback { + + EntityCallbacks() {} +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 07281d73fcbca2ea5f8bce25f6bf961d258bf8a0..0a8eeebb2d702ebcefd9f26cc0f41d1eab497902 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -1881,4 +1881,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + return ret; + } + // Paper end ++ // Paper start - optimize redstone (Alternate Current) ++ public alternate.current.wire.WireHandler getWireHandler() { ++ // This method is overridden in ServerLevel. ++ // Since Paper is a server platform there is no risk ++ // of this implementation being called. It is here ++ // only so this method can be called without casting ++ // an instance of Level to ServerLevel. ++ return null; ++ } ++ // Paper end - optimize redstone (Alternate Current) + } +diff --git a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java +index 18ed178223cca85dbba65b1e07741622e266d318..c131734cad123a35456d18f8a161f77a4ac9ac99 100644 +--- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java +@@ -258,7 +258,7 @@ public class RedStoneWireBlock extends Block { + return floor.isFaceSturdy(world, pos, Direction.UP) || floor.is(Blocks.HOPPER); + } + +- // Paper start - Optimize redstone ++ // Paper start - Optimize redstone (Eigencraft) + // The bulk of the new functionality is found in RedstoneWireTurbo.java + com.destroystokyo.paper.util.RedstoneWireTurbo turbo = new com.destroystokyo.paper.util.RedstoneWireTurbo(this); + +@@ -460,7 +460,13 @@ public class RedStoneWireBlock extends Block { + @Override + protected void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) { + if (!oldState.is(state.getBlock()) && !world.isClientSide) { +- this.updateSurroundingRedstone(world, pos, state, null); // Paper - Optimize redstone ++ // Paper start - optimize redstone - replace call to updatePowerStrength ++ if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { ++ world.getWireHandler().onWireAdded(pos); // Alternate Current ++ } else { ++ this.updateSurroundingRedstone(world, pos, state, null); // vanilla/Eigencraft ++ } ++ // Paper end - optimize redstone + Iterator iterator = Direction.Plane.VERTICAL.iterator(); + + while (iterator.hasNext()) { +@@ -487,7 +493,13 @@ public class RedStoneWireBlock extends Block { + world.updateNeighborsAt(pos.relative(enumdirection), this); + } + +- this.updateSurroundingRedstone(world, pos, state, null); // Paper - Optimize redstone ++ // Paper start - optimize redstone - replace call to updatePowerStrength ++ if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { ++ world.getWireHandler().onWireRemoved(pos, state); // Alternate Current ++ } else { ++ this.updateSurroundingRedstone(world, pos, state, null); // vanilla/Eigencraft ++ } ++ // Paper end - optimize redstone + this.updateNeighborsOfNeighboringWires(world, pos); + } + } +@@ -521,8 +533,14 @@ public class RedStoneWireBlock extends Block { + @Override + protected void neighborChanged(BlockState state, Level world, BlockPos pos, Block sourceBlock, BlockPos sourcePos, boolean notify) { + if (!world.isClientSide) { ++ // Paper start - optimize redstone (Alternate Current) ++ // Alternate Current handles breaking of redstone wires in the WireHandler. ++ if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { ++ world.getWireHandler().onWireUpdated(pos); ++ } else ++ // Paper end - optimize redstone (Alternate Current) + if (state.canSurvive(world, pos)) { +- this.updateSurroundingRedstone(world, pos, state, sourcePos); // Paper - Optimize redstone ++ this.updateSurroundingRedstone(world, pos, state, sourcePos); // Paper - Optimize redstone (Eigencraft) + } else { + dropResources(state, world, pos); + world.removeBlock(pos, false); diff --git a/patches/server/1022-Fix-entity-type-tags-suggestions-in-selectors.patch b/patches/server/1022-Fix-entity-type-tags-suggestions-in-selectors.patch deleted file mode 100644 index a31aeb7f18..0000000000 --- a/patches/server/1022-Fix-entity-type-tags-suggestions-in-selectors.patch +++ /dev/null @@ -1,156 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Sun, 22 Aug 2021 15:21:57 -0700 -Subject: [PATCH] Fix entity type tags suggestions in selectors - -This would really be better as a client fix because just to fix it -all EntityArguments have been told to ask the server for completions -when if this was fixed on the client, that wouldn't be needed. - -Mojira Issue: https://bugs.mojang.com/browse/MC-235045 - -diff --git a/src/main/java/net/minecraft/commands/CommandSourceStack.java b/src/main/java/net/minecraft/commands/CommandSourceStack.java -index 3c0d2332207ba638faaaa4280bce18c334a01271..e6c7f62ed379a78645933670299e4fcda8540ed1 100644 ---- a/src/main/java/net/minecraft/commands/CommandSourceStack.java -+++ b/src/main/java/net/minecraft/commands/CommandSourceStack.java -@@ -462,4 +462,20 @@ public class CommandSourceStack implements ExecutionCommandSource getSelectedEntities() { -+ if (io.papermc.paper.configuration.GlobalConfiguration.get().commands.fixTargetSelectorTagCompletion && this.source instanceof ServerPlayer player) { -+ final Entity cameraEntity = player.getCamera(); -+ final double pickDistance = player.gameMode.getGameModeForPlayer().isCreative() ? 6.0F : 4.5F; -+ final Vec3 min = cameraEntity.getEyePosition(1.0F); -+ final Vec3 viewVector = cameraEntity.getViewVector(1.0F); -+ final Vec3 max = min.add(viewVector.x * pickDistance, viewVector.y * pickDistance, viewVector.z * pickDistance); -+ final net.minecraft.world.phys.AABB aabb = cameraEntity.getBoundingBox().expandTowards(viewVector.scale(pickDistance)).inflate(1.0D, 1.0D, 1.0D); -+ final net.minecraft.world.phys.EntityHitResult hitResult = net.minecraft.world.entity.projectile.ProjectileUtil.getEntityHitResult(cameraEntity, min, max, aabb, (e) -> !e.isSpectator() && e.isPickable(), pickDistance * pickDistance); -+ return hitResult != null ? java.util.Collections.singletonList(hitResult.getEntity().getStringUUID()) : SharedSuggestionProvider.super.getSelectedEntities(); -+ } -+ return SharedSuggestionProvider.super.getSelectedEntities(); -+ } -+ // Paper end - tell clients to ask server for suggestions for EntityArguments - } -diff --git a/src/main/java/net/minecraft/commands/Commands.java b/src/main/java/net/minecraft/commands/Commands.java -index 60406cb01c5a409ba6fe98677a5cc332effaebea..a0702dccbb07c79febe2e65a2ff82c4436c09f12 100644 ---- a/src/main/java/net/minecraft/commands/Commands.java -+++ b/src/main/java/net/minecraft/commands/Commands.java -@@ -525,6 +525,7 @@ public class Commands { - private void fillUsableCommands(CommandNode tree, CommandNode result, CommandSourceStack source, Map, CommandNode> resultNodes) { - Iterator iterator = tree.getChildren().iterator(); - -+ boolean registeredAskServerSuggestionsForTree = false; // Paper - tell clients to ask server for suggestions for EntityArguments - while (iterator.hasNext()) { - CommandNode commandnode2 = (CommandNode) iterator.next(); - // Paper start - Brigadier API -@@ -551,6 +552,12 @@ public class Commands { - - if (requiredargumentbuilder.getSuggestionsProvider() != null) { - requiredargumentbuilder.suggests(SuggestionProviders.safelySwap(requiredargumentbuilder.getSuggestionsProvider())); -+ // Paper start - tell clients to ask server for suggestions for EntityArguments -+ registeredAskServerSuggestionsForTree = requiredargumentbuilder.getSuggestionsProvider() == net.minecraft.commands.synchronization.SuggestionProviders.ASK_SERVER; -+ } else if (io.papermc.paper.configuration.GlobalConfiguration.get().commands.fixTargetSelectorTagCompletion && !registeredAskServerSuggestionsForTree && requiredargumentbuilder.getType() instanceof net.minecraft.commands.arguments.EntityArgument) { -+ requiredargumentbuilder.suggests(requiredargumentbuilder.getType()::listSuggestions); -+ registeredAskServerSuggestionsForTree = true; // You can only -+ // Paper end - tell clients to ask server for suggestions for EntityArguments - } - } - -diff --git a/src/main/java/net/minecraft/commands/arguments/EntityArgument.java b/src/main/java/net/minecraft/commands/arguments/EntityArgument.java -index 2043001c16b3530c2d3f52efda10bcad424881c0..7976885b902a6ce7d80f31e49448c99452eb9765 100644 ---- a/src/main/java/net/minecraft/commands/arguments/EntityArgument.java -+++ b/src/main/java/net/minecraft/commands/arguments/EntityArgument.java -@@ -131,7 +131,7 @@ public class EntityArgument implements ArgumentType { - final boolean permission = object instanceof CommandSourceStack stack - ? stack.bypassSelectorPermissions || stack.hasPermission(2, "minecraft.command.selector") - : icompletionprovider.hasPermission(2); -- EntitySelectorParser argumentparserselector = new EntitySelectorParser(stringreader, permission); -+ EntitySelectorParser argumentparserselector = new EntitySelectorParser(stringreader, permission, true); // Paper - tell clients to ask server for suggestions for EntityArguments - // Paper end - Fix EntityArgument suggestion permissions - - try { -@@ -141,7 +141,19 @@ public class EntityArgument implements ArgumentType { - } - - return argumentparserselector.fillSuggestions(suggestionsbuilder, (suggestionsbuilder1) -> { -- Collection collection = icompletionprovider.getOnlinePlayerNames(); -+ // Paper start - tell clients to ask server for suggestions for EntityArguments -+ final Collection collection; -+ if (icompletionprovider instanceof CommandSourceStack commandSourceStack && commandSourceStack.getEntity() instanceof ServerPlayer sourcePlayer) { -+ collection = new java.util.ArrayList<>(); -+ for (final ServerPlayer player : commandSourceStack.getServer().getPlayerList().getPlayers()) { -+ if (sourcePlayer.getBukkitEntity().canSee(player.getBukkitEntity())) { -+ collection.add(player.getGameProfile().getName()); -+ } -+ } -+ } else { -+ collection = icompletionprovider.getOnlinePlayerNames(); -+ } -+ // Paper end - tell clients to ask server for suggestions for EntityArguments - Iterable iterable = this.playersOnly ? collection : Iterables.concat(collection, icompletionprovider.getSelectedEntities()); - - SharedSuggestionProvider.suggest((Iterable) iterable, suggestionsbuilder1); -diff --git a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelectorParser.java b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelectorParser.java -index 3d897ec6920eff6176ddac9f0442a997b9ef14fd..abaf1ea340c69c8bee80e64567b44b5ce66d5fa3 100644 ---- a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelectorParser.java -+++ b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelectorParser.java -@@ -113,12 +113,19 @@ public class EntitySelectorParser { - private boolean hasScores; - private boolean hasAdvancements; - private boolean usesSelectors; -+ public boolean parsingEntityArgumentSuggestions; // Paper - tell clients to ask server for suggestions for EntityArguments - - public EntitySelectorParser(StringReader reader) { - this(reader, true); - } - - public EntitySelectorParser(StringReader reader, boolean atAllowed) { -+ // Paper start - tell clients to ask server for suggestions for EntityArguments -+ this(reader, atAllowed, false); -+ } -+ public EntitySelectorParser(StringReader reader, boolean atAllowed, boolean parsingEntityArgumentSuggestions) { -+ this.parsingEntityArgumentSuggestions = parsingEntityArgumentSuggestions; -+ // Paper end - tell clients to ask server for suggestions for EntityArguments - this.distance = MinMaxBounds.Doubles.ANY; - this.level = MinMaxBounds.Ints.ANY; - this.rotX = WrappedMinMaxBounds.ANY; -diff --git a/src/main/java/net/minecraft/commands/arguments/selector/options/EntitySelectorOptions.java b/src/main/java/net/minecraft/commands/arguments/selector/options/EntitySelectorOptions.java -index 44982ff2cdd6e03f0b9ce8c3cc87561ef183ef06..626fe7a45c2edba68eb201974e9f8f5eebf75cc0 100644 ---- a/src/main/java/net/minecraft/commands/arguments/selector/options/EntitySelectorOptions.java -+++ b/src/main/java/net/minecraft/commands/arguments/selector/options/EntitySelectorOptions.java -@@ -76,6 +76,19 @@ public class EntitySelectorOptions { - public static final DynamicCommandExceptionType ERROR_ENTITY_TYPE_INVALID = new DynamicCommandExceptionType( - entity -> Component.translatableEscape("argument.entity.options.type.invalid", entity) - ); -+ // Paper start - tell clients to ask server for suggestions for EntityArguments -+ public static final DynamicCommandExceptionType ERROR_ENTITY_TAG_INVALID = new DynamicCommandExceptionType((object) -> { -+ return io.papermc.paper.adventure.PaperAdventure -+ .asVanilla(net.kyori.adventure.text.Component -+ .text("Invalid or unknown entity type tag '" + object + "'") -+ .hoverEvent(net.kyori.adventure.text.event.HoverEvent -+ .showText(net.kyori.adventure.text.Component -+ .text("You can disable this error in 'paper.yml'") -+ ) -+ ) -+ ); -+ }); -+ // Paper end - tell clients to ask server for suggestions for EntityArguments - - private static void register(String id, EntitySelectorOptions.Modifier handler, Predicate condition, Component description) { - OPTIONS.put(id, new EntitySelectorOptions.Option(handler, condition, description)); -@@ -297,6 +310,12 @@ public class EntitySelectorOptions { - - if (reader.isTag()) { - TagKey> tagKey = TagKey.create(Registries.ENTITY_TYPE, ResourceLocation.read(reader.getReader())); -+ // Paper start - tell clients to ask server for suggestions for EntityArguments; throw error if invalid entity tag (only on suggestions to keep cmd success behavior) -+ if (reader.parsingEntityArgumentSuggestions && io.papermc.paper.configuration.GlobalConfiguration.get().commands.fixTargetSelectorTagCompletion && net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.getTag(tagKey).isEmpty()) { -+ reader.getReader().setCursor(i); -+ throw ERROR_ENTITY_TAG_INVALID.createWithContext(reader.getReader(), tagKey); -+ } -+ // Paper end - tell clients to ask server for suggestions for EntityArguments - reader.addPredicate(entity -> entity.getType().is(tagKey) != bl); - } else { - ResourceLocation resourceLocation = ResourceLocation.read(reader.getReader()); diff --git a/patches/server/1023-Add-Alternate-Current-redstone-implementation.patch b/patches/server/1023-Add-Alternate-Current-redstone-implementation.patch deleted file mode 100644 index afb3758607..0000000000 --- a/patches/server/1023-Add-Alternate-Current-redstone-implementation.patch +++ /dev/null @@ -1,2114 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Space Walker -Date: Wed, 8 Jun 2022 18:47:18 +0200 -Subject: [PATCH] Add Alternate Current redstone implementation - -Author: Space Walker - -Original license: MIT -Original project: https://github.com/SpaceWalkerRS/alternate-current - -This patch adds Alternate Current's redstone implementation as an alternative to vanilla and Eigencraft's. -Performance of (de)powering redstone dust is many times faster than vanilla, and even exceeds Eigencraft. -Similar to Eigencraft, Alternate Current heavily changes the update order of redstone dust. This means any contraption that -is location dependent in vanilla will either work everywhere or nowhere when using Alternate Current/Eigencraft. Beyond that -parity issues should be rare for both implementations, though Alternate Current has not been tested as thoroughly, so I -cannot comment on how the two compare in that aspect. - -Alternate Current needs the following modifications: -* Level/ServerLevel: Each level has its own 'wire handler' that handles redstone dust power changes. -* RedStoneWireBlock: Replace calls to vanilla's or Eigencraft's methods for handling power changes with calls to -Alternate Current's wire handler. - -diff --git a/src/main/java/alternate/current/wire/LevelHelper.java b/src/main/java/alternate/current/wire/LevelHelper.java -new file mode 100644 -index 0000000000000000000000000000000000000000..8b4697421d57f81ff1794c6f845258e10df91622 ---- /dev/null -+++ b/src/main/java/alternate/current/wire/LevelHelper.java -@@ -0,0 +1,66 @@ -+package alternate.current.wire; -+ -+import org.bukkit.craftbukkit.block.CraftBlock; -+import org.bukkit.event.block.BlockRedstoneEvent; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.level.block.Block; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.level.chunk.ChunkAccess; -+import net.minecraft.world.level.chunk.status.ChunkStatus; -+import net.minecraft.world.level.chunk.LevelChunkSection; -+ -+public class LevelHelper { -+ -+ static int doRedstoneEvent(ServerLevel level, BlockPos pos, int prevPower, int newPower) { -+ BlockRedstoneEvent event = new BlockRedstoneEvent(CraftBlock.at(level, pos), prevPower, newPower); -+ level.getCraftServer().getPluginManager().callEvent(event); -+ -+ return event.getNewCurrent(); -+ } -+ -+ /** -+ * An optimized version of {@link net.minecraft.world.level.Level#setBlock -+ * Level.setBlock}. Since this method is only used to update redstone wire block -+ * states, lighting checks, height map updates, and block entity updates are -+ * omitted. -+ */ -+ static boolean setWireState(ServerLevel level, BlockPos pos, BlockState state, boolean updateNeighborShapes) { -+ int y = pos.getY(); -+ -+ if (y < level.getMinBuildHeight() || y >= level.getMaxBuildHeight()) { -+ return false; -+ } -+ -+ int x = pos.getX(); -+ int z = pos.getZ(); -+ int index = level.getSectionIndex(y); -+ -+ ChunkAccess chunk = level.getChunk(x >> 4, z >> 4, ChunkStatus.FULL, true); -+ LevelChunkSection section = chunk.getSections()[index]; -+ -+ if (section == null) { -+ return false; // we should never get here -+ } -+ -+ BlockState prevState = section.setBlockState(x & 15, y & 15, z & 15, state); -+ -+ if (state == prevState) { -+ return false; -+ } -+ -+ // notify clients of the BlockState change -+ level.getChunkSource().blockChanged(pos); -+ // mark the chunk for saving -+ chunk.setUnsaved(true); -+ -+ if (updateNeighborShapes) { -+ prevState.updateIndirectNeighbourShapes(level, pos, Block.UPDATE_CLIENTS); -+ state.updateNeighbourShapes(level, pos, Block.UPDATE_CLIENTS); -+ state.updateIndirectNeighbourShapes(level, pos, Block.UPDATE_CLIENTS); -+ } -+ -+ return true; -+ } -+} -diff --git a/src/main/java/alternate/current/wire/Node.java b/src/main/java/alternate/current/wire/Node.java -new file mode 100644 -index 0000000000000000000000000000000000000000..8af6c69098e64945361d116b5fd6ac21e97fcd8d ---- /dev/null -+++ b/src/main/java/alternate/current/wire/Node.java -@@ -0,0 +1,113 @@ -+package alternate.current.wire; -+ -+import java.util.Arrays; -+ -+import alternate.current.wire.WireHandler.Directions; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.level.block.Blocks; -+import net.minecraft.world.level.block.state.BlockState; -+ -+/** -+ * A Node represents a block in the world. It also holds a few other pieces of -+ * information that speed up the calculations in the WireHandler class. -+ * -+ * @author Space Walker -+ */ -+public class Node { -+ -+ // flags that encode the Node type -+ private static final int CONDUCTOR = 0b01; -+ private static final int SOURCE = 0b10; -+ -+ final ServerLevel level; -+ final Node[] neighbors; -+ -+ BlockPos pos; -+ BlockState state; -+ boolean invalid; -+ -+ private int flags; -+ -+ /** The previous node in the priority queue. */ -+ Node prev_node; -+ /** The next node in the priority queue. */ -+ Node next_node; -+ /** The priority with which this node was queued. */ -+ int priority; -+ /** The wire that queued this node for an update. */ -+ WireNode neighborWire; -+ -+ Node(ServerLevel level) { -+ this.level = level; -+ this.neighbors = new Node[Directions.ALL.length]; -+ } -+ -+ @Override -+ public boolean equals(Object obj) { -+ if (this == obj) { -+ return true; -+ } -+ if (!(obj instanceof Node)) { -+ return false; -+ } -+ -+ Node node = (Node)obj; -+ -+ return level == node.level && pos.equals(node.pos); -+ } -+ -+ @Override -+ public int hashCode() { -+ return pos.hashCode(); -+ } -+ -+ Node set(BlockPos pos, BlockState state, boolean clearNeighbors) { -+ if (state.is(Blocks.REDSTONE_WIRE)) { -+ throw new IllegalStateException("Cannot update a regular Node to a WireNode!"); -+ } -+ -+ if (clearNeighbors) { -+ Arrays.fill(neighbors, null); -+ } -+ -+ this.pos = pos.immutable(); -+ this.state = state; -+ this.invalid = false; -+ -+ this.flags = 0; -+ -+ if (this.state.isRedstoneConductor(this.level, this.pos)) { -+ this.flags |= CONDUCTOR; -+ } -+ if (this.state.isSignalSource()) { -+ this.flags |= SOURCE; -+ } -+ -+ return this; -+ } -+ -+ /** -+ * Determine the priority with which this node should be queued. -+ */ -+ int priority() { -+ return neighborWire.priority; -+ } -+ -+ public boolean isWire() { -+ return false; -+ } -+ -+ public boolean isConductor() { -+ return (flags & CONDUCTOR) != 0; -+ } -+ -+ public boolean isSignalSource() { -+ return (flags & SOURCE) != 0; -+ } -+ -+ public WireNode asWire() { -+ throw new UnsupportedOperationException("Not a WireNode!"); -+ } -+} -diff --git a/src/main/java/alternate/current/wire/PriorityQueue.java b/src/main/java/alternate/current/wire/PriorityQueue.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d71b4d0e4c44a2620b41b89475412db53bea20ed ---- /dev/null -+++ b/src/main/java/alternate/current/wire/PriorityQueue.java -@@ -0,0 +1,211 @@ -+package alternate.current.wire; -+ -+import java.util.AbstractQueue; -+import java.util.Arrays; -+import java.util.Iterator; -+ -+import net.minecraft.world.level.redstone.Redstone; -+ -+public class PriorityQueue extends AbstractQueue { -+ -+ private static final int OFFSET = -Redstone.SIGNAL_MIN; -+ -+ /** The last node for each priority value. */ -+ private final Node[] tails; -+ -+ private Node head; -+ private Node tail; -+ -+ private int size; -+ -+ PriorityQueue() { -+ this.tails = new Node[(Redstone.SIGNAL_MAX + OFFSET) + 1]; -+ } -+ -+ @Override -+ public boolean offer(Node node) { -+ if (node == null) { -+ throw new NullPointerException(); -+ } -+ -+ int priority = node.priority(); -+ -+ if (contains(node)) { -+ if (node.priority == priority) { -+ // already queued with this priority; exit -+ return false; -+ } else { -+ // already queued with different priority; move it -+ move(node, priority); -+ } -+ } else { -+ insert(node, priority); -+ } -+ -+ return true; -+ } -+ -+ @Override -+ public Node poll() { -+ if (head == null) { -+ return null; -+ } -+ -+ Node node = head; -+ Node next = node.next_node; -+ -+ if (next == null) { -+ clear(); // reset the tails array -+ } else { -+ if (node.priority != next.priority) { -+ // If the head is also a tail, its entry in the array -+ // can be cleared; there is no previous node with the -+ // same priority to take its place. -+ tails[node.priority + OFFSET] = null; -+ } -+ -+ node.next_node = null; -+ next.prev_node = null; -+ head = next; -+ -+ size--; -+ } -+ -+ return node; -+ } -+ -+ @Override -+ public Node peek() { -+ return head; -+ } -+ -+ @Override -+ public void clear() { -+ for (Node node = head; node != null; ) { -+ Node n = node; -+ node = node.next_node; -+ -+ n.prev_node = null; -+ n.next_node = null; -+ } -+ -+ Arrays.fill(tails, null); -+ -+ head = null; -+ tail = null; -+ -+ size = 0; -+ } -+ -+ @Override -+ public Iterator iterator() { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public int size() { -+ return size; -+ } -+ -+ public boolean contains(Node node) { -+ return node == head || node.prev_node != null; -+ } -+ -+ private void move(Node node, int priority) { -+ remove(node); -+ insert(node, priority); -+ } -+ -+ private void remove(Node node) { -+ Node prev = node.prev_node; -+ Node next = node.next_node; -+ -+ if (node == tail || node.priority != next.priority) { -+ // assign a new tail for this node's priority -+ if (node == head || node.priority != prev.priority) { -+ // there is no other node with the same priority; clear -+ tails[node.priority + OFFSET] = null; -+ } else { -+ // the previous node in the queue becomes the tail -+ tails[node.priority + OFFSET] = prev; -+ } -+ } -+ -+ if (node == head) { -+ head = next; -+ } else { -+ prev.next_node = next; -+ } -+ if (node == tail) { -+ tail = prev; -+ } else { -+ next.prev_node = prev; -+ } -+ -+ node.prev_node = null; -+ node.next_node = null; -+ -+ size--; -+ } -+ -+ private void insert(Node node, int priority) { -+ node.priority = priority; -+ -+ // nodes are sorted by priority (highest to lowest) -+ // nodes with the same priority are ordered FIFO -+ if (head == null) { -+ // first element in this queue \o/ -+ head = tail = node; -+ } else if (priority > head.priority) { -+ linkHead(node); -+ } else if (priority <= tail.priority) { -+ linkTail(node); -+ } else { -+ // since the node is neither the head nor the tail -+ // findPrev is guaranteed to find a non-null element -+ linkAfter(findPrev(node), node); -+ } -+ -+ tails[priority + OFFSET] = node; -+ -+ size++; -+ } -+ -+ private void linkHead(Node node) { -+ node.next_node = head; -+ head.prev_node = node; -+ head = node; -+ } -+ -+ private void linkTail(Node node) { -+ tail.next_node = node; -+ node.prev_node = tail; -+ tail = node; -+ } -+ -+ private void linkAfter(Node prev, Node node) { -+ linkBetween(prev, node, prev.next_node); -+ } -+ -+ private void linkBetween(Node prev, Node node, Node next) { -+ prev.next_node = node; -+ node.prev_node = prev; -+ -+ node.next_node = next; -+ next.prev_node = node; -+ } -+ -+ private Node findPrev(Node node) { -+ Node prev = null; -+ -+ for (int i = node.priority + OFFSET; i < tails.length; i++) { -+ prev = tails[i]; -+ -+ if (prev != null) { -+ break; -+ } -+ } -+ -+ return prev; -+ } -+} -diff --git a/src/main/java/alternate/current/wire/SimpleQueue.java b/src/main/java/alternate/current/wire/SimpleQueue.java -new file mode 100644 -index 0000000000000000000000000000000000000000..2b30074252551e1dc55d5be17d26fb4a2d8eb2e4 ---- /dev/null -+++ b/src/main/java/alternate/current/wire/SimpleQueue.java -@@ -0,0 +1,112 @@ -+package alternate.current.wire; -+ -+import java.util.AbstractQueue; -+import java.util.Iterator; -+ -+public class SimpleQueue extends AbstractQueue { -+ -+ private WireNode head; -+ private WireNode tail; -+ -+ private int size; -+ -+ SimpleQueue() { -+ -+ } -+ -+ @Override -+ public boolean offer(WireNode node) { -+ if (node == null) { -+ throw new NullPointerException(); -+ } -+ -+ if (tail == null) { -+ head = tail = node; -+ } else { -+ tail.next_wire = node; -+ tail = node; -+ } -+ -+ size++; -+ -+ return true; -+ } -+ -+ @Override -+ public WireNode poll() { -+ if (head == null) { -+ return null; -+ } -+ -+ WireNode node = head; -+ WireNode next = node.next_wire; -+ -+ if (next == null) { -+ head = tail = null; -+ } else { -+ node.next_wire = null; -+ head = next; -+ } -+ -+ size--; -+ -+ return node; -+ } -+ -+ @Override -+ public WireNode peek() { -+ return head; -+ } -+ -+ @Override -+ public void clear() { -+ for (WireNode node = head; node != null; ) { -+ WireNode n = node; -+ node = node.next_wire; -+ -+ n.next_wire = null; -+ } -+ -+ head = null; -+ tail = null; -+ -+ size = 0; -+ } -+ -+ @Override -+ public Iterator iterator() { -+ return new SimpleIterator(); -+ } -+ -+ @Override -+ public int size() { -+ return size; -+ } -+ -+ private class SimpleIterator implements Iterator { -+ -+ private WireNode curr; -+ private WireNode next; -+ -+ private SimpleIterator() { -+ next = head; -+ } -+ -+ @Override -+ public boolean hasNext() { -+ if (next == null && curr != null) { -+ next = curr.next_wire; -+ } -+ -+ return next != null; -+ } -+ -+ @Override -+ public WireNode next() { -+ curr = next; -+ next = curr.next_wire; -+ -+ return curr; -+ } -+ } -+} -diff --git a/src/main/java/alternate/current/wire/WireConnection.java b/src/main/java/alternate/current/wire/WireConnection.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4fd8cb29024330397cfe4cbc1f237d285bfb7b3e ---- /dev/null -+++ b/src/main/java/alternate/current/wire/WireConnection.java -@@ -0,0 +1,30 @@ -+package alternate.current.wire; -+ -+/** -+ * This class represents a connection between some WireNode (the 'owner') and a -+ * neighboring WireNode. Two wires are considered to be connected if power can -+ * flow from one wire to the other (and/or vice versa). -+ * -+ * @author Space Walker -+ */ -+public class WireConnection { -+ -+ /** The connected wire. */ -+ final WireNode wire; -+ /** Cardinal direction to the connected wire. */ -+ final int iDir; -+ /** True if the owner of the connection can provide power to the connected wire. */ -+ final boolean offer; -+ /** True if the connected wire can provide power to the owner of the connection. */ -+ final boolean accept; -+ -+ /** The next connection in the sequence. */ -+ WireConnection next; -+ -+ WireConnection(WireNode wire, int iDir, boolean offer, boolean accept) { -+ this.wire = wire; -+ this.iDir = iDir; -+ this.offer = offer; -+ this.accept = accept; -+ } -+} -diff --git a/src/main/java/alternate/current/wire/WireConnectionManager.java b/src/main/java/alternate/current/wire/WireConnectionManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..5a7209f05b549c222f6c9bc2af2a35790964947e ---- /dev/null -+++ b/src/main/java/alternate/current/wire/WireConnectionManager.java -@@ -0,0 +1,136 @@ -+package alternate.current.wire; -+ -+import java.util.Arrays; -+import java.util.function.Consumer; -+ -+import alternate.current.wire.WireHandler.Directions; -+import alternate.current.wire.WireHandler.NodeProvider; -+ -+public class WireConnectionManager { -+ -+ /** The owner of these connections. */ -+ final WireNode owner; -+ -+ /** The first connection for each cardinal direction. */ -+ private final WireConnection[] heads; -+ -+ private WireConnection head; -+ private WireConnection tail; -+ -+ /** The total number of connections. */ -+ int total; -+ -+ /** -+ * A 4 bit number that encodes in which direction(s) the owner has connections -+ * to other wires. -+ */ -+ private int flowTotal; -+ /** The direction of flow based connections to other wires. */ -+ int iFlowDir; -+ -+ WireConnectionManager(WireNode owner) { -+ this.owner = owner; -+ -+ this.heads = new WireConnection[Directions.HORIZONTAL.length]; -+ -+ this.total = 0; -+ -+ this.flowTotal = 0; -+ this.iFlowDir = -1; -+ } -+ -+ void set(NodeProvider nodes) { -+ if (total > 0) { -+ clear(); -+ } -+ -+ boolean belowIsConductor = nodes.getNeighbor(owner, Directions.DOWN).isConductor(); -+ boolean aboveIsConductor = nodes.getNeighbor(owner, Directions.UP).isConductor(); -+ -+ for (int iDir = 0; iDir < Directions.HORIZONTAL.length; iDir++) { -+ Node neighbor = nodes.getNeighbor(owner, iDir); -+ -+ if (neighbor.isWire()) { -+ add(neighbor.asWire(), iDir, true, true); -+ -+ continue; -+ } -+ -+ boolean sideIsConductor = neighbor.isConductor(); -+ -+ if (!sideIsConductor) { -+ Node node = nodes.getNeighbor(neighbor, Directions.DOWN); -+ -+ if (node.isWire()) { -+ add(node.asWire(), iDir, belowIsConductor, true); -+ } -+ } -+ if (!aboveIsConductor) { -+ Node node = nodes.getNeighbor(neighbor, Directions.UP); -+ -+ if (node.isWire()) { -+ add(node.asWire(), iDir, true, sideIsConductor); -+ } -+ } -+ } -+ -+ if (total > 0) { -+ iFlowDir = WireHandler.FLOW_IN_TO_FLOW_OUT[flowTotal]; -+ } -+ } -+ -+ private void clear() { -+ Arrays.fill(heads, null); -+ -+ head = null; -+ tail = null; -+ -+ total = 0; -+ -+ flowTotal = 0; -+ iFlowDir = -1; -+ } -+ -+ private void add(WireNode wire, int iDir, boolean offer, boolean accept) { -+ add(new WireConnection(wire, iDir, offer, accept)); -+ } -+ -+ private void add(WireConnection connection) { -+ if (head == null) { -+ head = connection; -+ tail = connection; -+ } else { -+ tail.next = connection; -+ tail = connection; -+ } -+ -+ total++; -+ -+ if (heads[connection.iDir] == null) { -+ heads[connection.iDir] = connection; -+ flowTotal |= (1 << connection.iDir); -+ } -+ } -+ -+ /** -+ * Iterate over all connections. Use this method if the iteration order is not -+ * important. -+ */ -+ void forEach(Consumer consumer) { -+ for (WireConnection c = head; c != null; c = c.next) { -+ consumer.accept(c); -+ } -+ } -+ -+ /** -+ * Iterate over all connections. Use this method if the iteration order is -+ * important. -+ */ -+ void forEach(Consumer consumer, int iFlowDir) { -+ for (int iDir : WireHandler.CARDINAL_UPDATE_ORDERS[iFlowDir]) { -+ for (WireConnection c = heads[iDir]; c != null && c.iDir == iDir; c = c.next) { -+ consumer.accept(c); -+ } -+ } -+ } -+} -diff --git a/src/main/java/alternate/current/wire/WireHandler.java b/src/main/java/alternate/current/wire/WireHandler.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e943fdcbc15d5c17450659c2cd9e0be73ae06c0b ---- /dev/null -+++ b/src/main/java/alternate/current/wire/WireHandler.java -@@ -0,0 +1,1150 @@ -+package alternate.current.wire; -+ -+import java.util.Iterator; -+import java.util.Queue; -+import java.util.function.Consumer; -+ -+import it.unimi.dsi.fastutil.longs.Long2ObjectMap; -+import it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry; -+import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; -+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.Direction; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.level.block.Block; -+import net.minecraft.world.level.block.Blocks; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.level.redstone.Redstone; -+ -+/** -+ * This class handles power changes for redstone wire. The algorithm was -+ * designed with the following goals in mind: -+ *
-+ * 1. Minimize the number of times a wire checks its surroundings to determine -+ * its power level. -+ *
-+ * 2. Minimize the number of block and shape updates emitted. -+ *
-+ * 3. Emit block and shape updates in a deterministic, non-locational order, -+ * fixing bug MC-11193. -+ * -+ *

-+ * In Vanilla redstone wire is laggy because it fails on points 1 and 2. -+ * -+ *

-+ * Redstone wire updates recursively and each wire calculates its power level in -+ * isolation rather than in the context of the network it is a part of. This -+ * means a wire in a grid can change its power level over half a dozen times -+ * before settling on its final value. This problem used to be worse in 1.13 and -+ * below, where a wire would only decrease its power level by 1 at a time. -+ * -+ *

-+ * In addition to this, a wire emits 42 block updates and up to 22 shape updates -+ * each time it changes its power level. -+ * -+ *

-+ * Of those 42 block updates, 6 are to itself, which are thus not only -+ * redundant, but a big source of lag, since those cause the wire to -+ * unnecessarily re-calculate its power level. A block only has 24 neighbors -+ * within a Manhattan distance of 2, meaning 12 of the remaining 36 block -+ * updates are duplicates and thus also redundant. -+ * -+ *

-+ * Of the 22 shape updates, only 6 are strictly necessary. The other 16 are sent -+ * to blocks diagonally above and below. These are necessary if a wire changes -+ * its connections, but not when it changes its power level. -+ * -+ *

-+ * Redstone wire in Vanilla also fails on point 3, though this is more of a -+ * quality-of-life issue than a lag issue. The recursive nature in which it -+ * updates, combined with the location-dependent order in which each wire -+ * updates its neighbors, makes the order in which neighbors of a wire network -+ * are updated incredibly inconsistent and seemingly random. -+ * -+ *

-+ * Alternate Current fixes each of these problems as follows. -+ * -+ *

-+ * 1. To make sure a wire calculates its power level as little as possible, we -+ * remove the recursive nature in which redstone wire updates in Vanilla. -+ * Instead, we build a network of connected wires, find those wires that receive -+ * redstone power from "outside" the network, and spread the power from there. -+ * This has a few advantages: -+ *
-+ * - Each wire checks for power from non-wire components at most once, and from -+ * nearby wires just twice. -+ *
-+ * - Each wire only sets its power level in the world once. This is important, -+ * because calls to Level.setBlock are even more expensive than calls to -+ * Level.getBlockState. -+ * -+ *

-+ * 2. There are 2 obvious ways in which we can reduce the number of block and -+ * shape updates. -+ *
-+ * - Get rid of the 18 redundant block updates and 16 redundant shape updates, -+ * so each wire only emits 24 block updates and 6 shape updates whenever it -+ * changes its power level. -+ *
-+ * - Only emit block updates and shape updates once a wire reaches its final -+ * power level, rather than at each intermediary stage. -+ *
-+ * For an individual wire, these two optimizations are the best you can do, but -+ * for an entire grid, you can do better! -+ * -+ *

-+ * Since we calculate the power of the entire network, sending block and shape -+ * updates to the wires in it is redundant. Removing those updates can reduce -+ * the number of block and shape updates by up to 20%. -+ * -+ *

-+ * 3. To make the order of block updates to neighbors of a network -+ * deterministic, the first thing we must do is to replace the location- -+ * dependent order in which a wire updates its neighbors. Instead, we base it on -+ * the direction of power flow. This part of the algorithm was heavily inspired -+ * by theosib's 'RedstoneWireTurbo', which you can read more about in theosib's -+ * comment on Mojira here -+ * or by checking out its implementation in carpet mod here. -+ * -+ *

-+ * The idea is to determine the direction of power flow through a wire based on -+ * the power it receives from neighboring wires. For example, if the only power -+ * a wire receives is from a neighboring wire to its west, it can be said that -+ * the direction of power flow through the wire is east. -+ * -+ *

-+ * We make the order of block updates to neighbors of a wire depend on what is -+ * determined to be the direction of power flow. This not only removes -+ * locationality entirely, it even removes directionality in a large number of -+ * cases. Unlike in 'RedstoneWireTurbo', however, I have decided to keep a -+ * directional element in ambiguous cases, rather than to introduce randomness, -+ * though this is trivial to change. -+ * -+ *

-+ * While this change fixes the block update order of individual wires, we must -+ * still address the overall block update order of a network. This turns out to -+ * be a simple fix, because of a change we made earlier: we search through the -+ * network for wires that receive power from outside it, and spread the power -+ * from there. If we make each wire transmit its power to neighboring wires in -+ * an order dependent on the direction of power flow, we end up with a -+ * non-locational and largely non-directional wire update order. -+ * -+ * @author Space Walker -+ */ -+public class WireHandler { -+ -+ public static class Directions { -+ -+ public static final Direction[] ALL = { Direction.WEST, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.DOWN, Direction.UP }; -+ public static final Direction[] HORIZONTAL = { Direction.WEST, Direction.NORTH, Direction.EAST, Direction.SOUTH }; -+ -+ // Indices for the arrays above. -+ // The cardinal directions are ordered clockwise. This allows -+ // for conversion between relative and absolute directions -+ // ('left' 'right' vs 'east' 'west') with simple arithmetic: -+ // If some Direction index 'iDir' is considered 'forward', then -+ // '(iDir + 1) & 0b11' is 'right', '(iDir + 2) & 0b11' is 'backward', etc. -+ public static final int WEST = 0b000; // 0 -+ public static final int NORTH = 0b001; // 1 -+ public static final int EAST = 0b010; // 2 -+ public static final int SOUTH = 0b011; // 3 -+ public static final int DOWN = 0b100; // 4 -+ public static final int UP = 0b101; // 5 -+ -+ public static int iOpposite(int iDir) { -+ return iDir ^ (0b10 >>> (iDir >>> 2)); -+ } -+ -+ // Each array is placed at the index that encodes the direction that is missing -+ // from the array. -+ private static final int[][] I_EXCEPT = { -+ { NORTH, EAST, SOUTH, DOWN, UP }, -+ { WEST, EAST, SOUTH, DOWN, UP }, -+ { WEST, NORTH, SOUTH, DOWN, UP }, -+ { WEST, NORTH, EAST, DOWN, UP }, -+ { WEST, NORTH, EAST, SOUTH, UP }, -+ { WEST, NORTH, EAST, SOUTH, DOWN } -+ }; -+ private static final int[][] I_EXCEPT_CARDINAL = { -+ { NORTH, EAST, SOUTH }, -+ { WEST, EAST, SOUTH }, -+ { WEST, NORTH, SOUTH }, -+ { WEST, NORTH, EAST, }, -+ { WEST, NORTH, EAST, SOUTH }, -+ { WEST, NORTH, EAST, SOUTH } -+ }; -+ } -+ -+ /** -+ * This conversion table takes in information about incoming flow, and outputs -+ * the determined outgoing flow. -+ * -+ *

-+ * The input is a 4 bit number that encodes the incoming flow. Each bit -+ * represents a cardinal direction, and when it is 'on', there is flow in that -+ * direction. -+ * -+ *

-+ * The output is a single Direction index, or -1 for ambiguous cases. -+ * -+ *

-+ * The outgoing flow is determined as follows: -+ * -+ *

-+ * If there is just 1 direction of incoming flow, that direction will be the -+ * direction of outgoing flow. -+ * -+ *

-+ * If there are 2 directions of incoming flow, and these directions are not each -+ * other's opposites, the direction that is 'more clockwise' will be the -+ * direction of outgoing flow. More precisely, the direction that is 1 clockwise -+ * turn from the other is picked. -+ * -+ *

-+ * If there are 3 directions of incoming flow, the two opposing directions -+ * cancel each other out, and the remaining direction will be the direction of -+ * outgoing flow. -+ * -+ *

-+ * In all other cases, the flow is completely ambiguous. -+ */ -+ static final int[] FLOW_IN_TO_FLOW_OUT = { -+ -1, // 0b0000: - -> x -+ Directions.WEST, // 0b0001: west -> west -+ Directions.NORTH, // 0b0010: north -> north -+ Directions.NORTH, // 0b0011: west/north -> north -+ Directions.EAST, // 0b0100: east -> east -+ -1, // 0b0101: west/east -> x -+ Directions.EAST, // 0b0110: north/east -> east -+ Directions.NORTH, // 0b0111: west/north/east -> north -+ Directions.SOUTH, // 0b1000: south -> south -+ Directions.WEST, // 0b1001: west/south -> west -+ -1, // 0b1010: north/south -> x -+ Directions.WEST, // 0b1011: west/north/south -> west -+ Directions.SOUTH, // 0b1100: east/south -> south -+ Directions.SOUTH, // 0b1101: west/east/south -> south -+ Directions.EAST, // 0b1110: north/east/south -> east -+ -1, // 0b1111: west/north/east/south -> x -+ }; -+ /** -+ * Update orders of all directions. Given that the index encodes the direction -+ * that is to be considered 'forward', the resulting update order is -+ * { front, back, right, left, down, up }. -+ */ -+ static final int[][] FULL_UPDATE_ORDERS = { -+ { Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH, Directions.DOWN, Directions.UP }, -+ { Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST , Directions.DOWN, Directions.UP }, -+ { Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH, Directions.DOWN, Directions.UP }, -+ { Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST , Directions.DOWN, Directions.UP } -+ }; -+ /** -+ * The default update order of all directions. It is equivalent to the order of -+ * shape updates in vanilla Minecraft. -+ */ -+ static final int[] DEFAULT_FULL_UPDATE_ORDER = FULL_UPDATE_ORDERS[0]; -+ /** -+ * Update orders of cardinal directions. Given that the index encodes the -+ * direction that is to be considered 'forward', the resulting update order is -+ * { front, back, right, left }. -+ */ -+ static final int[][] CARDINAL_UPDATE_ORDERS = { -+ { Directions.WEST , Directions.EAST , Directions.NORTH, Directions.SOUTH }, -+ { Directions.NORTH, Directions.SOUTH, Directions.EAST , Directions.WEST }, -+ { Directions.EAST , Directions.WEST , Directions.SOUTH, Directions.NORTH }, -+ { Directions.SOUTH, Directions.NORTH, Directions.WEST , Directions.EAST } -+ }; -+ /** -+ * The default update order of all cardinal directions. -+ */ -+ static final int[] DEFAULT_CARDINAL_UPDATE_ORDER = CARDINAL_UPDATE_ORDERS[0]; -+ -+ private static final int POWER_MIN = Redstone.SIGNAL_MIN; -+ private static final int POWER_MAX = Redstone.SIGNAL_MAX; -+ private static final int POWER_STEP = 1; -+ -+ // If Vanilla will ever multi-thread the ticking of levels, there should -+ // be only one WireHandler per level, in case redstone updates in multiple -+ // levels at the same time. There are already mods that add multi-threading -+ // as well. -+ private final ServerLevel level; -+ -+ /** Map of wires and neighboring blocks. */ -+ private final Long2ObjectMap nodes; -+ /** Queue for the breadth-first search through the network. */ -+ private final Queue search; -+ /** Queue of updates to wires and neighboring blocks. */ -+ private final Queue updates; -+ -+ // Rather than creating new nodes every time a network is updated we keep -+ // a cache of nodes that can be re-used. -+ private Node[] nodeCache; -+ private int nodeCount; -+ -+ /** Is this WireHandler currently working through the update queue? */ -+ private boolean updating; -+ -+ public WireHandler(ServerLevel level) { -+ this.level = level; -+ -+ this.nodes = new Long2ObjectOpenHashMap<>(); -+ this.search = new SimpleQueue(); -+ this.updates = new PriorityQueue(); -+ -+ this.nodeCache = new Node[16]; -+ this.fillNodeCache(0, 16); -+ } -+ -+ /** -+ * Retrieve the {@link Node Node} that represents the -+ * block at the given position in the level. -+ */ -+ private Node getOrAddNode(BlockPos pos) { -+ return nodes.compute(pos.asLong(), (key, node) -> { -+ if (node == null) { -+ // If there is not yet a node at this position, retrieve and -+ // update one from the cache. -+ return getNextNode(pos); -+ } -+ if (node.invalid) { -+ return revalidateNode(node); -+ } -+ -+ return node; -+ }); -+ } -+ -+ /** -+ * Remove and return the {@link Node Node} at the given -+ * position. -+ */ -+ private Node removeNode(BlockPos pos) { -+ return nodes.remove(pos.asLong()); -+ } -+ -+ /** -+ * Return a {@link Node Node} that represents the block -+ * at the given position. -+ */ -+ private Node getNextNode(BlockPos pos) { -+ return getNextNode(pos, level.getBlockState(pos)); -+ } -+ -+ /** -+ * Return a node that represents the given position and block state. If it is a -+ * wire, then create a new {@link WireNode WireNode}. -+ * Otherwise, grab the next {@link Node Node} from the -+ * cache and update it. -+ */ -+ private Node getNextNode(BlockPos pos, BlockState state) { -+ return state.is(Blocks.REDSTONE_WIRE) ? new WireNode(level, pos, state) : getNextNode().set(pos, state, true); -+ } -+ -+ /** -+ * Grab the first unused node from the cache. If all of the cache is already in -+ * use, increase it in size first. -+ */ -+ private Node getNextNode() { -+ if (nodeCount == nodeCache.length) { -+ increaseNodeCache(); -+ } -+ -+ return nodeCache[nodeCount++]; -+ } -+ -+ private void increaseNodeCache() { -+ Node[] oldCache = nodeCache; -+ nodeCache = new Node[oldCache.length << 1]; -+ -+ for (int index = 0; index < oldCache.length; index++) { -+ nodeCache[index] = oldCache[index]; -+ } -+ -+ fillNodeCache(oldCache.length, nodeCache.length); -+ } -+ -+ private void fillNodeCache(int start, int end) { -+ for (int index = start; index < end; index++) { -+ nodeCache[index] = new Node(level); -+ } -+ } -+ -+ /** -+ * Try to revalidate the given node by looking at the block state that is -+ * occupying its position. If the given node is a wire but the block state is -+ * not, or vice versa, a new node must be created/grabbed from the cache. -+ * Otherwise, the node can be quickly revalidated with the new block state. -+ */ -+ private Node revalidateNode(Node node) { -+ BlockPos pos = node.pos; -+ BlockState state = level.getBlockState(pos); -+ -+ boolean wasWire = node.isWire(); -+ boolean isWire = state.is(Blocks.REDSTONE_WIRE); -+ -+ if (wasWire != isWire) { -+ return getNextNode(pos, state); -+ } -+ -+ node.invalid = false; -+ -+ if (isWire) { -+ // No need to update the block state of this wire - it will grab -+ // the current block state just before setting power anyway. -+ WireNode wire = node.asWire(); -+ -+ wire.root = false; -+ wire.discovered = false; -+ wire.searched = false; -+ } else { -+ node.set(pos, state, false); -+ } -+ -+ return node; -+ } -+ -+ /** -+ * Retrieve the neighbor of a node in the given direction and create a link -+ * between the two nodes if they are not yet linked. This link makes accessing -+ * neighbors of a node signficantly faster. -+ */ -+ private Node getNeighbor(Node node, int iDir) { -+ Node neighbor = node.neighbors[iDir]; -+ -+ if (neighbor == null || neighbor.invalid) { -+ Direction dir = Directions.ALL[iDir]; -+ BlockPos pos = node.pos.relative(dir); -+ -+ Node oldNeighbor = neighbor; -+ neighbor = getOrAddNode(pos); -+ -+ if (neighbor != oldNeighbor) { -+ int iOpp = Directions.iOpposite(iDir); -+ -+ node.neighbors[iDir] = neighbor; -+ neighbor.neighbors[iOpp] = node; -+ } -+ } -+ -+ return neighbor; -+ } -+ -+ /** -+ * Iterate over all neighboring nodes of the given wire. The iteration order is -+ * designed to be an extension of the default block update order, and is -+ * determined as follows: -+ *
-+ * 1. The direction of power flow through the wire is to be considered -+ * 'forward'. The iteration order depends on the neighbors' relative positions -+ * to the wire. -+ *
-+ * 2. Each neighbor is identified by the step(s) you must take, starting at the -+ * wire, to reach it. Each step is 1 block, thus the position of a neighbor is -+ * encoded by the direction(s) of the step(s), e.g. (right), (down), (up, left), -+ * etc. -+ *
-+ * 3. Neighbors are iterated over in pairs that lie on opposite sides of the -+ * wire. -+ *
-+ * 4. Neighbors are iterated over in order of their distance from the wire. This -+ * means they are iterated over in 3 groups: direct neighbors first, then -+ * diagonal neighbors, and last are the far neighbors that are 2 blocks directly -+ * out. -+ *
-+ * 5. The order within each group is determined using the following basic order: -+ * { front, back, right, left, down, up }. This order was chosen because it -+ * converts to the following order of absolute directions when west is said to -+ * be 'forward': { west, east, north, south, down, up } - this is the order of -+ * shape updates. -+ */ -+ private void forEachNeighbor(WireNode wire, Consumer consumer) { -+ int forward = wire.iFlowDir; -+ int rightward = (forward + 1) & 0b11; -+ int backward = (forward + 2) & 0b11; -+ int leftward = (forward + 3) & 0b11; -+ int downward = Directions.DOWN; -+ int upward = Directions.UP; -+ -+ Node front = getNeighbor(wire, forward); -+ Node right = getNeighbor(wire, rightward); -+ Node back = getNeighbor(wire, backward); -+ Node left = getNeighbor(wire, leftward); -+ Node below = getNeighbor(wire, downward); -+ Node above = getNeighbor(wire, upward); -+ -+ // direct neighbors (6) -+ consumer.accept(front); -+ consumer.accept(back); -+ consumer.accept(right); -+ consumer.accept(left); -+ consumer.accept(below); -+ consumer.accept(above); -+ -+ // diagonal neighbors (12) -+ consumer.accept(getNeighbor(front, rightward)); -+ consumer.accept(getNeighbor(back, leftward)); -+ consumer.accept(getNeighbor(front, leftward)); -+ consumer.accept(getNeighbor(back, rightward)); -+ consumer.accept(getNeighbor(front, downward)); -+ consumer.accept(getNeighbor(back, upward)); -+ consumer.accept(getNeighbor(front, upward)); -+ consumer.accept(getNeighbor(back, downward)); -+ consumer.accept(getNeighbor(right, downward)); -+ consumer.accept(getNeighbor(left, upward)); -+ consumer.accept(getNeighbor(right, upward)); -+ consumer.accept(getNeighbor(left, downward)); -+ -+ // far neighbors (6) -+ consumer.accept(getNeighbor(front, forward)); -+ consumer.accept(getNeighbor(back, backward)); -+ consumer.accept(getNeighbor(right, rightward)); -+ consumer.accept(getNeighbor(left, leftward)); -+ consumer.accept(getNeighbor(below, downward)); -+ consumer.accept(getNeighbor(above, upward)); -+ } -+ -+ /** -+ * This method should be called whenever a wire receives a block update. -+ */ -+ public void onWireUpdated(BlockPos pos) { -+ invalidate(); -+ findRoots(pos); -+ tryUpdate(); -+ } -+ -+ /** -+ * This method should be called whenever a wire is placed. -+ */ -+ public void onWireAdded(BlockPos pos) { -+ Node node = getOrAddNode(pos); -+ -+ if (!node.isWire()) { -+ return; // we should never get here -+ } -+ -+ WireNode wire = node.asWire(); -+ wire.added = true; -+ -+ invalidate(); -+ revalidateNode(wire); -+ findRoot(wire); -+ tryUpdate(); -+ } -+ -+ /** -+ * This method should be called whenever a wire is removed. -+ */ -+ public void onWireRemoved(BlockPos pos, BlockState state) { -+ Node node = removeNode(pos); -+ WireNode wire; -+ -+ if (node == null || !node.isWire()) { -+ wire = new WireNode(level, pos, state); -+ } else { -+ wire = node.asWire(); -+ } -+ -+ wire.invalid = true; -+ wire.removed = true; -+ -+ // If these fields are set to 'true', the removal of this wire was part of -+ // already ongoing power changes, so we can exit early here. -+ if (updating && wire.shouldBreak) { -+ return; -+ } -+ -+ invalidate(); -+ revalidateNode(wire); -+ findRoot(wire); -+ tryUpdate(); -+ } -+ -+ /** -+ * The nodes map is a snapshot of the state of the world. It becomes invalid -+ * when power changes are carried out, since the block and shape updates can -+ * lead to block changes. If these block changes cause the network to be updated -+ * again every node must be invalidated, and revalidated before it is used -+ * again. This ensures the power calculations of the network are accurate. -+ */ -+ private void invalidate() { -+ if (updating && !nodes.isEmpty()) { -+ Iterator> it = Long2ObjectMaps.fastIterator(nodes); -+ -+ while (it.hasNext()) { -+ Entry entry = it.next(); -+ Node node = entry.getValue(); -+ -+ node.invalid = true; -+ } -+ } -+ } -+ -+ /** -+ * Look for wires at and around the given position that are in an invalid state -+ * and require power changes. These wires are called 'roots' because it is only -+ * when these wires change power level that neighboring wires must adjust as -+ * well. -+ * -+ *

-+ * While it is strictly only necessary to check the wire at the given position, -+ * if that wire is part of a network, it is beneficial to check its surroundings -+ * for other wires that require power changes. This is because a network can -+ * receive power at multiple points. Consider the following setup: -+ * -+ *

-+ * (top-down view, W = wire, L = lever, _ = air/other) -+ *
{@code _ _ W _ _ } -+ *
{@code _ W W W _ } -+ *
{@code W W L W W } -+ *
{@code _ W W W _ } -+ *
{@code _ _ W _ _ } -+ * -+ *

-+ * The lever powers four wires in the network at once. If this is identified -+ * correctly, the entire network can (un)power at once. While it is not -+ * practical to cover every possible situation where a network is (un)powered -+ * from multiple points at once, checking for common cases like the one -+ * described above is relatively straight-forward. -+ */ -+ private void findRoots(BlockPos pos) { -+ Node node = getOrAddNode(pos); -+ -+ if (!node.isWire()) { -+ return; // we should never get here -+ } -+ -+ WireNode wire = node.asWire(); -+ findRoot(wire); -+ -+ // If the wire at the given position is not in an invalid state -+ // we can exit early. -+ if (!wire.searched) { -+ return; -+ } -+ -+ for (int iDir : FULL_UPDATE_ORDERS[wire.iFlowDir]) { -+ Node neighbor = getNeighbor(wire, iDir); -+ -+ if (neighbor.isConductor() || neighbor.isSignalSource()) { -+ findRootsAround(neighbor, Directions.iOpposite(iDir)); -+ } -+ } -+ } -+ -+ /** -+ * Look for wires around the given node that require power changes. -+ */ -+ private void findRootsAround(Node node, int except) { -+ for (int iDir : Directions.I_EXCEPT_CARDINAL[except]) { -+ Node neighbor = getNeighbor(node, iDir); -+ -+ if (neighbor.isWire()) { -+ findRoot(neighbor.asWire()); -+ } -+ } -+ } -+ -+ /** -+ * Check if the given wire requires power changes. If it does, queue it for the -+ * breadth-first search as a root. -+ */ -+ private void findRoot(WireNode wire) { -+ // Each wire only needs to be checked once. -+ if (wire.discovered) { -+ return; -+ } -+ -+ discover(wire); -+ findExternalPower(wire); -+ findPower(wire, false); -+ -+ if (needsUpdate(wire)) { -+ searchRoot(wire); -+ } -+ } -+ -+ /** -+ * Prepare the given wire for the breadth-first search. This means: -+ *
-+ * - Check if the wire should break. Rather than breaking the wire right away, -+ * its effects are integrated into the power calculations. -+ *
-+ * - Reset the virtual and external power. -+ *
-+ * - Find connections to neighboring wires. -+ */ -+ private void discover(WireNode wire) { -+ if (wire.discovered) { -+ return; -+ } -+ -+ wire.discovered = true; -+ wire.searched = false; -+ -+ if (!wire.removed && !wire.shouldBreak && !wire.state.canSurvive(level, wire.pos)) { -+ wire.shouldBreak = true; -+ } -+ -+ wire.virtualPower = wire.currentPower; -+ wire.externalPower = POWER_MIN - 1; -+ -+ wire.connections.set(this::getNeighbor); -+ } -+ -+ /** -+ * Determine the power level the given wire receives from the blocks around it. -+ * Power from non-wire components only needs to be computed if power from -+ * neighboring wires has decreased, so as to determine how low the power of the -+ * wire can fall. -+ */ -+ private void findPower(WireNode wire, boolean ignoreSearched) { -+ // As wire power is (re-)computed, flow information must be reset. -+ wire.virtualPower = wire.externalPower; -+ wire.flowIn = 0; -+ -+ // If the wire is removed or going to break, its power level should always be -+ // the minimum value. This is because it (effectively) no longer exists, so -+ // cannot provide any power to neighboring wires. -+ if (wire.removed || wire.shouldBreak) { -+ return; -+ } -+ -+ // Power received from neighboring wires will never exceed POWER_MAX - -+ // POWER_STEP, so if the external power is already larger than or equal to -+ // that, there is no need to check for power from neighboring wires. -+ if (wire.externalPower < (POWER_MAX - POWER_STEP)) { -+ findWirePower(wire, ignoreSearched); -+ } -+ } -+ -+ /** -+ * Determine the power the given wire receives from connected neighboring wires -+ * and update the virtual power accordingly. -+ */ -+ private void findWirePower(WireNode wire, boolean ignoreSearched) { -+ wire.connections.forEach(connection -> { -+ if (!connection.accept) { -+ return; -+ } -+ -+ WireNode neighbor = connection.wire; -+ -+ if (!ignoreSearched || !neighbor.searched) { -+ int power = Math.max(POWER_MIN, neighbor.virtualPower - POWER_STEP); -+ int iOpp = Directions.iOpposite(connection.iDir); -+ -+ wire.offerPower(power, iOpp); -+ } -+ }); -+ } -+ -+ /** -+ * Determine the redstone signal the given wire receives from non-wire -+ * components and update the virtual power accordingly. -+ */ -+ private void findExternalPower(WireNode wire) { -+ // If the wire is removed or going to break, its power level should always be -+ // the minimum value. Thus external power need not be computed. -+ // In other cases external power need only be computed once. -+ if (wire.removed || wire.shouldBreak || wire.externalPower >= POWER_MIN) { -+ return; -+ } -+ -+ wire.externalPower = getExternalPower(wire); -+ -+ if (wire.externalPower > wire.virtualPower) { -+ wire.virtualPower = wire.externalPower; -+ } -+ } -+ -+ /** -+ * Determine the redstone signal the given wire receives from non-wire -+ * components. -+ */ -+ private int getExternalPower(WireNode wire) { -+ int power = POWER_MIN; -+ -+ for (int iDir = 0; iDir < Directions.ALL.length; iDir++) { -+ Node neighbor = getNeighbor(wire, iDir); -+ -+ // Power from wires is handled separately. -+ if (neighbor.isWire()) { -+ continue; -+ } -+ -+ // Since 1.16 there is a block that is both a conductor and a signal -+ // source: the target block! -+ if (neighbor.isConductor()) { -+ power = Math.max(power, getDirectSignalTo(wire, neighbor, Directions.iOpposite(iDir))); -+ } -+ if (neighbor.isSignalSource()) { -+ power = Math.max(power, neighbor.state.getSignal(level, neighbor.pos, Directions.ALL[iDir])); -+ } -+ -+ if (power >= POWER_MAX) { -+ return POWER_MAX; -+ } -+ } -+ -+ return power; -+ } -+ -+ /** -+ * Determine the direct signal the given wire receives from neighboring blocks -+ * through the given conductor node. -+ */ -+ private int getDirectSignalTo(WireNode wire, Node node, int except) { -+ int power = POWER_MIN; -+ -+ for (int iDir : Directions.I_EXCEPT[except]) { -+ Node neighbor = getNeighbor(node, iDir); -+ -+ if (neighbor.isSignalSource()) { -+ power = Math.max(power, neighbor.state.getDirectSignal(level, neighbor.pos, Directions.ALL[iDir])); -+ -+ if (power >= POWER_MAX) { -+ return POWER_MAX; -+ } -+ } -+ } -+ -+ return power; -+ } -+ -+ /** -+ * Check if the given wire needs to update its state in the world. -+ */ -+ private boolean needsUpdate(WireNode wire) { -+ return wire.removed || wire.shouldBreak || wire.virtualPower != wire.currentPower; -+ } -+ -+ /** -+ * Queue the given wire for the breadth-first search as a root. -+ */ -+ private void searchRoot(WireNode wire) { -+ int iBackupFlowDir; -+ -+ if (wire.connections.iFlowDir < 0) { -+ iBackupFlowDir = 0; -+ } else { -+ iBackupFlowDir = wire.connections.iFlowDir; -+ } -+ -+ search(wire, true, iBackupFlowDir); -+ } -+ -+ /** -+ * Queue the given wire for the breadth-first search and set a backup flow -+ * direction. -+ */ -+ private void search(WireNode wire, boolean root, int iBackupFlowDir) { -+ search.offer(wire); -+ -+ wire.root = root; -+ wire.searched = true; -+ // Normally the flow is not set until the power level is updated. However, -+ // in networks with multiple power sources the update order between them -+ // depends on which was discovered first. To make this less prone to -+ // directionality, each wire node is given a 'backup' flow. For roots, this -+ // is the determined flow of their connections. For non-roots this is the -+ // direction from which they were discovered. -+ wire.iFlowDir = iBackupFlowDir; -+ } -+ -+ private void tryUpdate() { -+ if (!search.isEmpty()) { -+ update(); -+ } -+ if (!updating) { -+ nodes.clear(); -+ nodeCount = 0; -+ } -+ } -+ -+ /** -+ * Update the network and neighboring blocks. This is done in 3 steps. -+ * -+ *

-+ * 1. Search through the network -+ *
-+ * Conduct a breadth-first search around the roots to find wires that are in an -+ * invalid state and need power changes. -+ * -+ *

-+ * 2. Depower the network -+ *
-+ * Depower all wires in the network. This allows power to be spread most -+ * efficiently. -+ * -+ *

-+ * 3. Power the network -+ *
-+ * Work through the update queue, setting the new power level of each wire and -+ * updating neighboring blocks. After a wire has updated its power level, it -+ * will emit shape updates and queue updates for neighboring wires and blocks. -+ */ -+ private void update() { -+ // Search through the network for wires that need power changes. This includes -+ // the roots as well as any wires that will be affected by power changes to -+ // those roots. -+ searchNetwork(); -+ -+ // Depower all the wires in the network. -+ depowerNetwork(); -+ -+ // Bring each wire up to its new power level and update neighboring blocks. -+ try { -+ powerNetwork(); -+ } catch (Throwable t) { -+ // If anything goes wrong while carrying out power changes, this field must -+ // be reset to 'false', or the wire handler will be locked out of carrying -+ // out power changes until the world is reloaded. -+ updating = false; -+ -+ throw t; -+ } -+ } -+ -+ /** -+ * Search through the network for wires that are in an invalid state and need -+ * power changes. These wires are added to the end of the queue, so that their -+ * neighbors can be searched next. -+ */ -+ private void searchNetwork() { -+ for (WireNode wire : search) { -+ // The order in which wires are searched will influence the order in -+ // which they update their power levels. -+ wire.connections.forEach(connection -> { -+ if (!connection.offer) { -+ return; -+ } -+ -+ WireNode neighbor = connection.wire; -+ -+ if (neighbor.searched) { -+ return; -+ } -+ -+ discover(neighbor); -+ findPower(neighbor, false); -+ -+ // If power from neighboring wires has decreased, check for power -+ // from non-wire components so as to determine how low power can -+ // fall. -+ if (neighbor.virtualPower < neighbor.currentPower) { -+ findExternalPower(neighbor); -+ } -+ -+ if (needsUpdate(neighbor)) { -+ search(neighbor, false, connection.iDir); -+ } -+ }, wire.iFlowDir); -+ } -+ } -+ -+ /** -+ * Depower all wires in the network so that power can be spread from the power -+ * sources. -+ */ -+ private void depowerNetwork() { -+ while (!search.isEmpty()) { -+ WireNode wire = search.poll(); -+ findPower(wire, true); -+ -+ if (wire.root || wire.removed || wire.shouldBreak || wire.virtualPower > POWER_MIN) { -+ queueWire(wire); -+ } else { -+ // Wires that do not receive any power do not queue power changes -+ // until they are offered power from a neighboring wire. To ensure -+ // that they accept any power from neighboring wires and thus queue -+ // their power changes, their virtual power is set to below the -+ // minimum. -+ wire.virtualPower--; -+ } -+ } -+ } -+ -+ /** -+ * Work through the update queue, setting the new power level of each wire, then -+ * queueing updates to connected wires and neighboring blocks. -+ */ -+ private void powerNetwork() { -+ // If an instantaneous update chain causes updates to another network -+ // (or the same network in another place), new power changes will be -+ // integrated into the already ongoing power queue, so we can exit early -+ // here. -+ if (updating) { -+ return; -+ } -+ -+ updating = true; -+ -+ while (!updates.isEmpty()) { -+ Node node = updates.poll(); -+ -+ if (node.isWire()) { -+ WireNode wire = node.asWire(); -+ -+ if (!needsUpdate(wire)) { -+ continue; -+ } -+ -+ findPowerFlow(wire); -+ transmitPower(wire); -+ -+ if (wire.setPower()) { -+ queueNeighbors(wire); -+ -+ // If the wire was newly placed or removed, shape updates have -+ // already been emitted. However, unlike before 1.19, neighbor -+ // updates are now queued, so to preserve behavior parity with -+ // previous versions, we emit extra shape updates here to -+ // notify neighboring observers. -+ updateNeighborShapes(wire); -+ } -+ } else { -+ WireNode neighborWire = node.neighborWire; -+ -+ if (neighborWire != null) { -+ BlockPos neighborPos = neighborWire.pos; -+ Block neighborBlock = neighborWire.state.getBlock(); -+ -+ updateBlock(node, neighborPos, neighborBlock); -+ } -+ } -+ } -+ -+ updating = false; -+ } -+ -+ /** -+ * Use the information of incoming power flow to determine the direction of -+ * power flow through this wire. If that flow is ambiguous, try to use a flow -+ * direction based on connections to neighboring wires. If that is also -+ * ambiguous, use the backup value that was set when the wire was first added to -+ * the network. -+ */ -+ private void findPowerFlow(WireNode wire) { -+ int flow = FLOW_IN_TO_FLOW_OUT[wire.flowIn]; -+ -+ if (flow >= 0) { -+ wire.iFlowDir = flow; -+ } else if (wire.connections.iFlowDir >= 0) { -+ wire.iFlowDir = wire.connections.iFlowDir; -+ } -+ } -+ -+ /** -+ * Transmit power from the given wire to neighboring wires and queue updates to -+ * those wires. -+ */ -+ private void transmitPower(WireNode wire) { -+ wire.connections.forEach(connection -> { -+ if (!connection.offer) { -+ return; -+ } -+ -+ WireNode neighbor = connection.wire; -+ -+ int power = Math.max(POWER_MIN, wire.virtualPower - POWER_STEP); -+ int iDir = connection.iDir; -+ -+ if (neighbor.offerPower(power, iDir)) { -+ queueWire(neighbor); -+ } -+ }, wire.iFlowDir); -+ } -+ -+ /** -+ * Emit shape updates around the given wire. -+ */ -+ private void updateNeighborShapes(WireNode wire) { -+ BlockPos wirePos = wire.pos; -+ BlockState wireState = wire.state; -+ -+ for (int iDir : DEFAULT_FULL_UPDATE_ORDER) { -+ Node neighbor = getNeighbor(wire, iDir); -+ -+ if (!neighbor.isWire()) { -+ int iOpp = Directions.iOpposite(iDir); -+ Direction opp = Directions.ALL[iOpp]; -+ -+ updateShape(neighbor, opp, wirePos, wireState); -+ } -+ } -+ } -+ -+ private void updateShape(Node node, Direction dir, BlockPos neighborPos, BlockState neighborState) { -+ BlockPos pos = node.pos; -+ BlockState state = level.getBlockState(pos); -+ -+ // Shape updates to redstone wire are very expensive, and should never happen -+ // as a result of power changes anyway. -+ if (!state.isAir() && !state.is(Blocks.REDSTONE_WIRE)) { -+ BlockState newState = state.updateShape(dir, neighborState, level, pos, neighborPos); -+ Block.updateOrDestroy(state, newState, level, pos, Block.UPDATE_CLIENTS); -+ } -+ } -+ -+ /** -+ * Queue block updates to nodes around the given wire. -+ */ -+ private void queueNeighbors(WireNode wire) { -+ forEachNeighbor(wire, neighbor -> { -+ queueNeighbor(neighbor, wire); -+ }); -+ } -+ -+ /** -+ * Queue the given node for an update from the given neighboring wire. -+ */ -+ private void queueNeighbor(Node node, WireNode neighborWire) { -+ // Updates to wires are queued when power is transmitted. -+ if (!node.isWire()) { -+ node.neighborWire = neighborWire; -+ updates.offer(node); -+ } -+ } -+ -+ /** -+ * Queue the given wire for a power change. If the wire does not need a power -+ * change (perhaps because its power has already changed), transmit power to -+ * neighboring wires. -+ */ -+ private void queueWire(WireNode wire) { -+ if (needsUpdate(wire)) { -+ updates.offer(wire); -+ } else { -+ findPowerFlow(wire); -+ transmitPower(wire); -+ } -+ } -+ -+ /** -+ * Emit a block update to the given node. -+ */ -+ private void updateBlock(Node node, BlockPos neighborPos, Block neighborBlock) { -+ BlockPos pos = node.pos; -+ BlockState state = level.getBlockState(pos); -+ -+ // While this check makes sure wires in the network are not given block -+ // updates, it also prevents block updates to wires in neighboring networks. -+ // While this should not make a difference in theory, in practice, it is -+ // possible to force a network into an invalid state without updating it, even -+ // if it is relatively obscure. -+ // While I was willing to make this compromise in return for some significant -+ // performance gains in certain setups, if you are not, you can add all the -+ // positions of the network to a set and filter out block updates to wires in -+ // the network that way. -+ if (!state.isAir() && !state.is(Blocks.REDSTONE_WIRE)) { -+ state.handleNeighborChanged(level, pos, neighborBlock, neighborPos, false); -+ } -+ } -+ -+ @FunctionalInterface -+ public static interface NodeProvider { -+ -+ public Node getNeighbor(Node node, int iDir); -+ -+ } -+} -diff --git a/src/main/java/alternate/current/wire/WireNode.java b/src/main/java/alternate/current/wire/WireNode.java -new file mode 100644 -index 0000000000000000000000000000000000000000..33cd90c30c22200a4e1ae64f40a0bf7864546b33 ---- /dev/null -+++ b/src/main/java/alternate/current/wire/WireNode.java -@@ -0,0 +1,122 @@ -+package alternate.current.wire; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.util.Mth; -+import net.minecraft.world.level.block.Block; -+import net.minecraft.world.level.block.Blocks; -+import net.minecraft.world.level.block.RedStoneWireBlock; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.level.redstone.Redstone; -+ -+/** -+ * A WireNode is a Node that represents a wire in the world. It stores all the -+ * information about the wire that the WireHandler needs to calculate power -+ * changes. -+ * -+ * @author Space Walker -+ */ -+public class WireNode extends Node { -+ -+ final WireConnectionManager connections; -+ -+ /** The power level this wire currently holds in the world. */ -+ int currentPower; -+ /** -+ * While calculating power changes for a network, this field is used to keep -+ * track of the power level this wire should have. -+ */ -+ int virtualPower; -+ /** The power level received from non-wire components. */ -+ int externalPower; -+ /** -+ * A 4-bit number that keeps track of the power flow of the wires that give this -+ * wire its power level. -+ */ -+ int flowIn; -+ /** The direction of power flow, based on the incoming flow. */ -+ int iFlowDir; -+ boolean added; -+ boolean removed; -+ boolean shouldBreak; -+ boolean root; -+ boolean discovered; -+ boolean searched; -+ -+ /** The next wire in the simple queue. */ -+ WireNode next_wire; -+ -+ WireNode(ServerLevel level, BlockPos pos, BlockState state) { -+ super(level); -+ -+ this.pos = pos.immutable(); -+ this.state = state; -+ -+ this.connections = new WireConnectionManager(this); -+ -+ this.virtualPower = this.currentPower = this.state.getValue(RedStoneWireBlock.POWER); -+ this.priority = priority(); -+ } -+ -+ @Override -+ Node set(BlockPos pos, BlockState state, boolean clearNeighbors) { -+ throw new UnsupportedOperationException("Cannot update a WireNode!"); -+ } -+ -+ @Override -+ int priority() { -+ return Mth.clamp(virtualPower, Redstone.SIGNAL_MIN, Redstone.SIGNAL_MAX); -+ } -+ -+ @Override -+ public boolean isWire() { -+ return true; -+ } -+ -+ @Override -+ public WireNode asWire() { -+ return this; -+ } -+ -+ boolean offerPower(int power, int iDir) { -+ if (removed || shouldBreak) { -+ return false; -+ } -+ if (power == virtualPower) { -+ flowIn |= (1 << iDir); -+ return false; -+ } -+ if (power > virtualPower) { -+ virtualPower = power; -+ flowIn = (1 << iDir); -+ -+ return true; -+ } -+ -+ return false; -+ } -+ -+ boolean setPower() { -+ if (removed) { -+ return true; -+ } -+ -+ state = level.getBlockState(pos); -+ -+ if (!state.is(Blocks.REDSTONE_WIRE)) { -+ return false; // we should never get here -+ } -+ -+ if (shouldBreak) { -+ Block.dropResources(state, level, pos); -+ level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_CLIENTS); -+ -+ return true; -+ } -+ -+ currentPower = LevelHelper.doRedstoneEvent(level, pos, currentPower, Mth.clamp(virtualPower, Redstone.SIGNAL_MIN, Redstone.SIGNAL_MAX)); -+ state = state.setValue(RedStoneWireBlock.POWER, currentPower); -+ -+ return LevelHelper.setWireState(level, pos, state, added); -+ } -+} -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index aabaaf89f5e344791604a7e05122d9865bb0224e..2bfebbe0bc5a7f72c9ebe60fded260b3f94ac749 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -228,6 +228,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - public final UUID uuid; - public boolean hasPhysicsEvent = true; // Paper - BlockPhysicsEvent - public boolean hasEntityMoveEvent; // Paper - Add EntityMoveEvent -+ private final alternate.current.wire.WireHandler wireHandler = new alternate.current.wire.WireHandler(this); // Paper - optimize redstone (Alternate Current) - - public LevelChunk getChunkIfLoaded(int x, int z) { - return this.chunkSource.getChunkAtIfLoadedImmediately(x, z); // Paper - Use getChunkIfLoadedImmediately -@@ -2566,6 +2567,13 @@ public class ServerLevel extends Level implements WorldGenLevel { - return crashreportsystemdetails; - } - -+ // Paper start - optimize redstone (Alternate Current) -+ @Override -+ public alternate.current.wire.WireHandler getWireHandler() { -+ return wireHandler; -+ } -+ // Paper end - optimize redstone (Alternate Current) -+ - private final class EntityCallbacks implements LevelCallback { - - EntityCallbacks() {} -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index 07281d73fcbca2ea5f8bce25f6bf961d258bf8a0..0a8eeebb2d702ebcefd9f26cc0f41d1eab497902 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -1881,4 +1881,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - return ret; - } - // Paper end -+ // Paper start - optimize redstone (Alternate Current) -+ public alternate.current.wire.WireHandler getWireHandler() { -+ // This method is overridden in ServerLevel. -+ // Since Paper is a server platform there is no risk -+ // of this implementation being called. It is here -+ // only so this method can be called without casting -+ // an instance of Level to ServerLevel. -+ return null; -+ } -+ // Paper end - optimize redstone (Alternate Current) - } -diff --git a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java -index 18ed178223cca85dbba65b1e07741622e266d318..c131734cad123a35456d18f8a161f77a4ac9ac99 100644 ---- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java -@@ -258,7 +258,7 @@ public class RedStoneWireBlock extends Block { - return floor.isFaceSturdy(world, pos, Direction.UP) || floor.is(Blocks.HOPPER); - } - -- // Paper start - Optimize redstone -+ // Paper start - Optimize redstone (Eigencraft) - // The bulk of the new functionality is found in RedstoneWireTurbo.java - com.destroystokyo.paper.util.RedstoneWireTurbo turbo = new com.destroystokyo.paper.util.RedstoneWireTurbo(this); - -@@ -460,7 +460,13 @@ public class RedStoneWireBlock extends Block { - @Override - protected void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) { - if (!oldState.is(state.getBlock()) && !world.isClientSide) { -- this.updateSurroundingRedstone(world, pos, state, null); // Paper - Optimize redstone -+ // Paper start - optimize redstone - replace call to updatePowerStrength -+ if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { -+ world.getWireHandler().onWireAdded(pos); // Alternate Current -+ } else { -+ this.updateSurroundingRedstone(world, pos, state, null); // vanilla/Eigencraft -+ } -+ // Paper end - optimize redstone - Iterator iterator = Direction.Plane.VERTICAL.iterator(); - - while (iterator.hasNext()) { -@@ -487,7 +493,13 @@ public class RedStoneWireBlock extends Block { - world.updateNeighborsAt(pos.relative(enumdirection), this); - } - -- this.updateSurroundingRedstone(world, pos, state, null); // Paper - Optimize redstone -+ // Paper start - optimize redstone - replace call to updatePowerStrength -+ if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { -+ world.getWireHandler().onWireRemoved(pos, state); // Alternate Current -+ } else { -+ this.updateSurroundingRedstone(world, pos, state, null); // vanilla/Eigencraft -+ } -+ // Paper end - optimize redstone - this.updateNeighborsOfNeighboringWires(world, pos); - } - } -@@ -521,8 +533,14 @@ public class RedStoneWireBlock extends Block { - @Override - protected void neighborChanged(BlockState state, Level world, BlockPos pos, Block sourceBlock, BlockPos sourcePos, boolean notify) { - if (!world.isClientSide) { -+ // Paper start - optimize redstone (Alternate Current) -+ // Alternate Current handles breaking of redstone wires in the WireHandler. -+ if (world.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.ALTERNATE_CURRENT) { -+ world.getWireHandler().onWireUpdated(pos); -+ } else -+ // Paper end - optimize redstone (Alternate Current) - if (state.canSurvive(world, pos)) { -- this.updateSurroundingRedstone(world, pos, state, sourcePos); // Paper - Optimize redstone -+ this.updateSurroundingRedstone(world, pos, state, sourcePos); // Paper - Optimize redstone (Eigencraft) - } else { - dropResources(state, world, pos); - world.removeBlock(pos, false); diff --git a/patches/server/1023-optimize-dirt-and-snow-spreading.patch b/patches/server/1023-optimize-dirt-and-snow-spreading.patch new file mode 100644 index 0000000000..49de7fcab9 --- /dev/null +++ b/patches/server/1023-optimize-dirt-and-snow-spreading.patch @@ -0,0 +1,78 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: lukas81298 +Date: Fri, 22 Jan 2021 21:50:18 +0100 +Subject: [PATCH] optimize dirt and snow spreading + + +diff --git a/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java b/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java +index 5a39e8d359dc13383711e49ffb2d1294dad26192..b7165ec19bef1a07f6618fc0429d86cda1b08da4 100644 +--- a/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java ++++ b/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java +@@ -18,8 +18,13 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock { + } + + private static boolean canBeGrass(BlockState state, LevelReader world, BlockPos pos) { ++ // Paper start - Perf: optimize dirt and snow spreading ++ return canBeGrass(world.getChunk(pos), state, world, pos); ++ } ++ private static boolean canBeGrass(net.minecraft.world.level.chunk.ChunkAccess chunk, BlockState state, LevelReader world, BlockPos pos) { ++ // Paper end - Perf: optimize dirt and snow spreading + BlockPos blockposition1 = pos.above(); +- BlockState iblockdata1 = world.getBlockState(blockposition1); ++ BlockState iblockdata1 = chunk.getBlockState(blockposition1); // Paper - Perf: optimize dirt and snow spreading + + if (iblockdata1.is(Blocks.SNOW) && (Integer) iblockdata1.getValue(SnowLayerBlock.LAYERS) == 1) { + return true; +@@ -36,15 +41,27 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock { + protected abstract MapCodec codec(); + + private static boolean canPropagate(BlockState state, LevelReader world, BlockPos pos) { ++ // Paper start - Perf: optimize dirt and snow spreading ++ return canPropagate(world.getChunk(pos), state, world, pos); ++ } ++ ++ private static boolean canPropagate(net.minecraft.world.level.chunk.ChunkAccess chunk, BlockState state, LevelReader world, BlockPos pos) { ++ // Paper end - Perf: optimize dirt and snow spreading + BlockPos blockposition1 = pos.above(); + +- return SpreadingSnowyDirtBlock.canBeGrass(state, world, pos) && !world.getFluidState(blockposition1).is(FluidTags.WATER); ++ return SpreadingSnowyDirtBlock.canBeGrass(chunk, state, world, pos) && !chunk.getFluidState(blockposition1).is(FluidTags.WATER); // Paper - Perf: optimize dirt and snow spreading + } + + @Override + protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { + if (this instanceof GrassBlock && world.paperConfig().tickRates.grassSpread != 1 && (world.paperConfig().tickRates.grassSpread < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % world.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper - Configurable random tick rates for blocks +- if (!SpreadingSnowyDirtBlock.canBeGrass(state, world, pos)) { ++ // Paper start - Perf: optimize dirt and snow spreading ++ final net.minecraft.world.level.chunk.ChunkAccess cachedBlockChunk = world.getChunkIfLoaded(pos); ++ if (cachedBlockChunk == null) { // Is this needed? ++ return; ++ } ++ if (!SpreadingSnowyDirtBlock.canBeGrass(cachedBlockChunk, state, world, pos)) { ++ // Paper end - Perf: optimize dirt and snow spreading + // CraftBukkit start + if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, Blocks.DIRT.defaultBlockState()).isCancelled()) { + return; +@@ -57,9 +74,19 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock { + + for (int i = 0; i < 4; ++i) { + BlockPos blockposition1 = pos.offset(random.nextInt(3) - 1, random.nextInt(5) - 3, random.nextInt(3) - 1); +- +- if (world.getBlockState(blockposition1).is(Blocks.DIRT) && SpreadingSnowyDirtBlock.canPropagate(iblockdata1, world, blockposition1)) { +- org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(world, pos, blockposition1, (BlockState) iblockdata1.setValue(SpreadingSnowyDirtBlock.SNOWY, world.getBlockState(blockposition1.above()).is(Blocks.SNOW))); // CraftBukkit ++ // Paper start - Perf: optimize dirt and snow spreading ++ if (pos.getX() == blockposition1.getX() && pos.getY() == blockposition1.getY() && pos.getZ() == blockposition1.getZ()) { ++ continue; ++ } ++ net.minecraft.world.level.chunk.ChunkAccess access; ++ if (cachedBlockChunk.locX == blockposition1.getX() >> 4 && cachedBlockChunk.locZ == blockposition1.getZ() >> 4) { ++ access = cachedBlockChunk; ++ } else { ++ access = world.getChunkAt(blockposition1); ++ } ++ if (access.getBlockState(blockposition1).is(Blocks.DIRT) && SpreadingSnowyDirtBlock.canPropagate(access, iblockdata1, world, blockposition1)) { ++ org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(world, pos, blockposition1, (BlockState) iblockdata1.setValue(SpreadingSnowyDirtBlock.SNOWY, access.getBlockState(blockposition1.above()).is(Blocks.SNOW))); // CraftBukkit ++ // Paper end - Perf: optimize dirt and snow spreading + } + } + } diff --git a/patches/server/1024-Properly-resend-entities.patch b/patches/server/1024-Properly-resend-entities.patch new file mode 100644 index 0000000000..4ac71e7b7c --- /dev/null +++ b/patches/server/1024-Properly-resend-entities.patch @@ -0,0 +1,232 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> +Date: Wed, 7 Dec 2022 17:25:19 -0500 +Subject: [PATCH] Properly resend entities + +This resolves some issues which caused entities to not be resent correctly. +Entities that are interacted with need to be resent to the client, so we resend all the entity +data to the player whilst making sure not to clear dirty entries from the tracker. This makes +sure that values will be correctly updated to other players. + +This also adds utilities to aid in further preventing entity desyncs. + +This also also fixes the bug causing cancelling PlayerInteractEvent to cause items to continue +to be used despite being cancelled on the server. + +For example, items being consumed but never finishing, shields being put up, etc. +The underlying issue of this is that the client modifies their synced data values, +and so we have to (forcibly) resend them in order for the client to reset their using item state. + +See: https://github.com/PaperMC/Paper/pull/1896 + +== AT == +public net.minecraft.server.level.ChunkMap$TrackedEntity serverEntity + +diff --git a/src/main/java/net/minecraft/network/syncher/SynchedEntityData.java b/src/main/java/net/minecraft/network/syncher/SynchedEntityData.java +index 02bf2705ca1c99023a83a22d92e1962181102297..0f99733660f91280e4c6262cf75b3c9cae86f65a 100644 +--- a/src/main/java/net/minecraft/network/syncher/SynchedEntityData.java ++++ b/src/main/java/net/minecraft/network/syncher/SynchedEntityData.java +@@ -50,7 +50,7 @@ public class SynchedEntityData { + } + } + +- private SynchedEntityData.DataItem getItem(EntityDataAccessor key) { ++ public SynchedEntityData.DataItem getItem(EntityDataAccessor key) { // Paper - public + return (SynchedEntityData.DataItem) this.itemsById[key.id()]; // CraftBukkit - decompile error + } + +@@ -151,6 +151,20 @@ public class SynchedEntityData { + } + } + ++ // Paper start ++ // We need to pack all as we cannot rely on "non default values" or "dirty" ones. ++ // Because these values can possibly be desynced on the client. ++ @Nullable ++ public List> packAll() { ++ final List> list = new ArrayList<>(); ++ for (final DataItem dataItem : this.itemsById) { ++ list.add(dataItem.value()); ++ } ++ ++ return list; ++ } ++ // Paper end ++ + public static class DataItem { + + final EntityDataAccessor accessor; +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +index abb9a86cd42a34cf722a312068134e820ac21956..f168044d36f22080504da171e5ed31a6f02385ba 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +@@ -556,6 +556,7 @@ public class ServerPlayerGameMode { + } + // Paper end - extend Player Interact cancellation + player.getBukkitEntity().updateInventory(); // SPIGOT-2867 ++ this.player.resyncUsingItem(this.player); // Paper - Properly cancel usable items + return (event.useItemInHand() != Event.Result.ALLOW) ? InteractionResult.SUCCESS : InteractionResult.PASS; + } else if (this.gameModeForPlayer == GameType.SPECTATOR) { + MenuProvider itileinventory = iblockdata.getMenuProvider(world, blockposition); +@@ -607,6 +608,11 @@ public class ServerPlayerGameMode { + + return enuminteractionresult; + } else { ++ // Paper start - Properly cancel usable items; Cancel only if cancelled + if the interact result is different from default response ++ if (this.interactResult && this.interactResult != cancelledItem) { ++ this.player.resyncUsingItem(this.player); ++ } ++ // Paper end - Properly cancel usable items + return InteractionResult.PASS; + } + } +diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index ae927c2cdf151b901822636a2543668f85946430..ea7a2db0d71a69a6f67fda032f01c9a126bd3100 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -2004,6 +2004,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + + if (cancelled) { ++ this.player.resyncUsingItem(this.player); // Paper - Properly cancel usable items + this.player.getBukkitEntity().updateInventory(); // SPIGOT-2524 + return; + } +@@ -2788,7 +2789,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + + // Entity in bucket - SPIGOT-4048 and SPIGOT-6859a + if ((entity instanceof Bucketable && entity instanceof LivingEntity && origItem != null && origItem.asItem() == Items.WATER_BUCKET) && (event.isCancelled() || ServerGamePacketListenerImpl.this.player.getInventory().getSelected() == null || ServerGamePacketListenerImpl.this.player.getInventory().getSelected().getItem() != origItem)) { +- ServerGamePacketListenerImpl.this.send(new ClientboundAddEntityPacket(entity)); ++ entity.resendPossiblyDesyncedEntityData(ServerGamePacketListenerImpl.this.player); // Paper - The entire mob gets deleted, so resend it. + ServerGamePacketListenerImpl.this.player.containerMenu.sendAllDataToRemote(); + } + +diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java +index 68446b7532dfbda303293aa9e756644c6fcdffca..a2142930b4d4b05987c90496fb9d733d99040aa0 100644 +--- a/src/main/java/net/minecraft/server/players/PlayerList.java ++++ b/src/main/java/net/minecraft/server/players/PlayerList.java +@@ -393,7 +393,7 @@ public abstract class PlayerList { + ((ServerLevel)player.level()).getChunkSource().chunkMap.addEntity(player); // Paper - Fire PlayerJoinEvent when Player is actually ready; track entity now + // CraftBukkit end + +- player.refreshEntityData(player); // CraftBukkit - BungeeCord#2321, send complete data to self on spawn ++ //player.refreshEntityData(player); // CraftBukkit - BungeeCord#2321, send complete data to self on spawn // Paper - THIS IS NOT NEEDED ANYMORE + + this.sendLevelInfo(player, worldserver1); + +diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java +index 8ad2d17615ff489b2fbbb13480dd0b217a42d805..2bc85351e6e52f90da5fdb29d8d042a06132d742 100644 +--- a/src/main/java/net/minecraft/world/entity/Entity.java ++++ b/src/main/java/net/minecraft/world/entity/Entity.java +@@ -704,13 +704,44 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + + // CraftBukkit start + public void refreshEntityData(ServerPlayer to) { +- List> list = this.getEntityData().getNonDefaultValues(); ++ List> list = this.entityData.packAll(); // Paper - Update EVERYTHING not just not default + +- if (list != null) { ++ if (list != null && to.getBukkitEntity().canSee(this.getBukkitEntity())) { // Paper + to.connection.send(new ClientboundSetEntityDataPacket(this.getId(), list)); + } + } + // CraftBukkit end ++ // Paper start ++ // This method should only be used if the data of an entity could have become desynced ++ // due to interactions on the client. ++ public void resendPossiblyDesyncedEntityData(net.minecraft.server.level.ServerPlayer player) { ++ if (this.tracker == null) { ++ return; ++ } ++ ++ if (player.getBukkitEntity().canSee(this.getBukkitEntity())) { ++ final net.minecraft.server.level.ServerEntity serverEntity = this.tracker.serverEntity; ++ final List> list = new java.util.ArrayList<>(); ++ serverEntity.sendPairingData(player, list::add); ++ player.connection.send(new net.minecraft.network.protocol.game.ClientboundBundlePacket(list)); ++ } ++ } ++ ++ // This method allows you to specifically resend certain data accessor keys to the client ++ public void resendPossiblyDesyncedDataValues(List> keys, ServerPlayer to) { ++ if (!to.getBukkitEntity().canSee(this.getBukkitEntity())) { ++ return; ++ } ++ ++ final List> values = new java.util.ArrayList<>(keys.size()); ++ for (final EntityDataAccessor key : keys) { ++ final SynchedEntityData.DataItem synchedValue = this.entityData.getItem(key); ++ values.add(synchedValue.value()); ++ } ++ ++ to.connection.send(new ClientboundSetEntityDataPacket(this.id, values)); ++ } ++ // Paper end + + public boolean equals(Object object) { + return object instanceof Entity ? ((Entity) object).id == this.id : false; +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index 75e01c1e01e0782fc8af48777bbe4716d37aeafa..2d375fc98e72246f6a640c390cb50d055fb59f8b 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -3828,6 +3828,11 @@ public abstract class LivingEntity extends Entity implements Attackable { + return ((Byte) this.entityData.get(LivingEntity.DATA_LIVING_ENTITY_FLAGS) & 2) > 0 ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND; + } + ++ // Paper start - Properly cancel usable items ++ public void resyncUsingItem(ServerPlayer serverPlayer) { ++ this.resendPossiblyDesyncedDataValues(java.util.List.of(DATA_LIVING_ENTITY_FLAGS), serverPlayer); ++ } ++ // Paper end - Properly cancel usable items + private void updatingUsingItem() { + if (this.isUsingItem()) { + if (ItemStack.isSameItem(this.getItemInHand(this.getUsedItemHand()), this.useItem)) { +diff --git a/src/main/java/net/minecraft/world/entity/animal/Bucketable.java b/src/main/java/net/minecraft/world/entity/animal/Bucketable.java +index cb4a6439e9774bbec07e69b13df8dddd395b9ece..cfe37b4f5e33795ee717d824d86e3a0919129cf5 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/Bucketable.java ++++ b/src/main/java/net/minecraft/world/entity/animal/Bucketable.java +@@ -109,8 +109,7 @@ public interface Bucketable { + itemstack1 = CraftItemStack.asNMSCopy(playerBucketFishEvent.getEntityBucket()); + if (playerBucketFishEvent.isCancelled()) { + ((ServerPlayer) player).containerMenu.sendAllDataToRemote(); // We need to update inventory to resync client's bucket +- ((ServerPlayer) player).connection.send(new ClientboundAddEntityPacket(entity)); // We need to play out these packets as the client assumes the fish is gone +- entity.refreshEntityData((ServerPlayer) player); // Need to send data such as the display name to client ++ entity.resendPossiblyDesyncedEntityData((ServerPlayer) player); // Paper + return Optional.of(InteractionResult.FAIL); + } + entity.playSound(((Bucketable) entity).getPickupSound(), 1.0F, 1.0F); +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +index 3ab04c4bdbe26ff7f6f54eb9cdd58376c592fa05..a2d336ceb52b63db5c03432ee7bc94dc6a742b82 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +@@ -1010,7 +1010,11 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + return; + } + +- entityTracker.broadcast(this.getHandle().getAddEntityPacket()); ++ // Paper start, resend possibly desynced entity instead of add entity packet ++ for (ServerPlayerConnection playerConnection : entityTracker.seenBy) { ++ this.getHandle().resendPossiblyDesyncedEntityData(playerConnection.getPlayer()); ++ } ++ // Paper end + } + + private static PermissibleBase getPermissibleBase() { +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java +index 0801bcdee8fcff0d388d302387e4f1d587e0a283..2fcd9b836d42e3549a3b6b921c57a4c103146dff 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java +@@ -39,9 +39,11 @@ public class CraftItemFrame extends CraftHanging implements ItemFrame { + protected void update() { + super.update(); + ++ // Paper start, don't mark as dirty as this is handled in super.update() + // mark dirty, so that the client gets updated with item and rotation +- this.getHandle().getEntityData().markDirty(net.minecraft.world.entity.decoration.ItemFrame.DATA_ITEM); +- this.getHandle().getEntityData().markDirty(net.minecraft.world.entity.decoration.ItemFrame.DATA_ROTATION); ++ //this.getHandle().getEntityData().markDirty(net.minecraft.world.entity.decoration.ItemFrame.DATA_ITEM); ++ //this.getHandle().getEntityData().markDirty(net.minecraft.world.entity.decoration.ItemFrame.DATA_ROTATION); ++ // Paper end + + // update redstone + if (!this.getHandle().generation) { diff --git a/patches/server/1024-optimize-dirt-and-snow-spreading.patch b/patches/server/1024-optimize-dirt-and-snow-spreading.patch deleted file mode 100644 index 49de7fcab9..0000000000 --- a/patches/server/1024-optimize-dirt-and-snow-spreading.patch +++ /dev/null @@ -1,78 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: lukas81298 -Date: Fri, 22 Jan 2021 21:50:18 +0100 -Subject: [PATCH] optimize dirt and snow spreading - - -diff --git a/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java b/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java -index 5a39e8d359dc13383711e49ffb2d1294dad26192..b7165ec19bef1a07f6618fc0429d86cda1b08da4 100644 ---- a/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java -@@ -18,8 +18,13 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock { - } - - private static boolean canBeGrass(BlockState state, LevelReader world, BlockPos pos) { -+ // Paper start - Perf: optimize dirt and snow spreading -+ return canBeGrass(world.getChunk(pos), state, world, pos); -+ } -+ private static boolean canBeGrass(net.minecraft.world.level.chunk.ChunkAccess chunk, BlockState state, LevelReader world, BlockPos pos) { -+ // Paper end - Perf: optimize dirt and snow spreading - BlockPos blockposition1 = pos.above(); -- BlockState iblockdata1 = world.getBlockState(blockposition1); -+ BlockState iblockdata1 = chunk.getBlockState(blockposition1); // Paper - Perf: optimize dirt and snow spreading - - if (iblockdata1.is(Blocks.SNOW) && (Integer) iblockdata1.getValue(SnowLayerBlock.LAYERS) == 1) { - return true; -@@ -36,15 +41,27 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock { - protected abstract MapCodec codec(); - - private static boolean canPropagate(BlockState state, LevelReader world, BlockPos pos) { -+ // Paper start - Perf: optimize dirt and snow spreading -+ return canPropagate(world.getChunk(pos), state, world, pos); -+ } -+ -+ private static boolean canPropagate(net.minecraft.world.level.chunk.ChunkAccess chunk, BlockState state, LevelReader world, BlockPos pos) { -+ // Paper end - Perf: optimize dirt and snow spreading - BlockPos blockposition1 = pos.above(); - -- return SpreadingSnowyDirtBlock.canBeGrass(state, world, pos) && !world.getFluidState(blockposition1).is(FluidTags.WATER); -+ return SpreadingSnowyDirtBlock.canBeGrass(chunk, state, world, pos) && !chunk.getFluidState(blockposition1).is(FluidTags.WATER); // Paper - Perf: optimize dirt and snow spreading - } - - @Override - protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { - if (this instanceof GrassBlock && world.paperConfig().tickRates.grassSpread != 1 && (world.paperConfig().tickRates.grassSpread < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % world.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper - Configurable random tick rates for blocks -- if (!SpreadingSnowyDirtBlock.canBeGrass(state, world, pos)) { -+ // Paper start - Perf: optimize dirt and snow spreading -+ final net.minecraft.world.level.chunk.ChunkAccess cachedBlockChunk = world.getChunkIfLoaded(pos); -+ if (cachedBlockChunk == null) { // Is this needed? -+ return; -+ } -+ if (!SpreadingSnowyDirtBlock.canBeGrass(cachedBlockChunk, state, world, pos)) { -+ // Paper end - Perf: optimize dirt and snow spreading - // CraftBukkit start - if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, Blocks.DIRT.defaultBlockState()).isCancelled()) { - return; -@@ -57,9 +74,19 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock { - - for (int i = 0; i < 4; ++i) { - BlockPos blockposition1 = pos.offset(random.nextInt(3) - 1, random.nextInt(5) - 3, random.nextInt(3) - 1); -- -- if (world.getBlockState(blockposition1).is(Blocks.DIRT) && SpreadingSnowyDirtBlock.canPropagate(iblockdata1, world, blockposition1)) { -- org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(world, pos, blockposition1, (BlockState) iblockdata1.setValue(SpreadingSnowyDirtBlock.SNOWY, world.getBlockState(blockposition1.above()).is(Blocks.SNOW))); // CraftBukkit -+ // Paper start - Perf: optimize dirt and snow spreading -+ if (pos.getX() == blockposition1.getX() && pos.getY() == blockposition1.getY() && pos.getZ() == blockposition1.getZ()) { -+ continue; -+ } -+ net.minecraft.world.level.chunk.ChunkAccess access; -+ if (cachedBlockChunk.locX == blockposition1.getX() >> 4 && cachedBlockChunk.locZ == blockposition1.getZ() >> 4) { -+ access = cachedBlockChunk; -+ } else { -+ access = world.getChunkAt(blockposition1); -+ } -+ if (access.getBlockState(blockposition1).is(Blocks.DIRT) && SpreadingSnowyDirtBlock.canPropagate(access, iblockdata1, world, blockposition1)) { -+ org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockSpreadEvent(world, pos, blockposition1, (BlockState) iblockdata1.setValue(SpreadingSnowyDirtBlock.SNOWY, access.getBlockState(blockposition1.above()).is(Blocks.SNOW))); // CraftBukkit -+ // Paper end - Perf: optimize dirt and snow spreading - } - } - } diff --git a/patches/server/1025-Optimize-Hoppers.patch b/patches/server/1025-Optimize-Hoppers.patch new file mode 100644 index 0000000000..adf6bca823 --- /dev/null +++ b/patches/server/1025-Optimize-Hoppers.patch @@ -0,0 +1,637 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Wed, 27 Apr 2016 22:09:52 -0400 +Subject: [PATCH] Optimize Hoppers + +* Removes unnecessary extra calls to .update() that are very expensive +* Lots of itemstack cloning removed. Only clone if the item is actually moved +* Return true when a plugin cancels inventory move item event instead of false, as false causes pulls to cycle through all items. + However, pushes do not exhibit the same behavior, so this is not something plugins could of been relying on. +* Add option (Default on) to cooldown hoppers when they fail to move an item due to full inventory +* Skip subsequent InventoryMoveItemEvents if a plugin does not use the item after first event fire for an iteration by tracking changes to the event via an internal event implementation. +* Don't check for Entities with Inventories if the block above us is also occluding (not just Inventoried) +* Remove Streams from Item Suck In and restore restore 1.12 AABB checks which is simpler and no voxel allocations (was doing TWO Item Suck ins) + +diff --git a/src/main/java/io/papermc/paper/event/inventory/PaperInventoryMoveItemEvent.java b/src/main/java/io/papermc/paper/event/inventory/PaperInventoryMoveItemEvent.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5c42823726e70ce6c9d0121d074315488e8b3f60 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/event/inventory/PaperInventoryMoveItemEvent.java +@@ -0,0 +1,31 @@ ++package io.papermc.paper.event.inventory; ++ ++import org.bukkit.event.inventory.InventoryMoveItemEvent; ++import org.bukkit.inventory.Inventory; ++import org.bukkit.inventory.ItemStack; ++import org.checkerframework.checker.nullness.qual.NonNull; ++import org.checkerframework.framework.qual.DefaultQualifier; ++import org.jetbrains.annotations.NotNull; ++ ++@DefaultQualifier(NonNull.class) ++public class PaperInventoryMoveItemEvent extends InventoryMoveItemEvent { ++ ++ public boolean calledSetItem; ++ public boolean calledGetItem; ++ ++ public PaperInventoryMoveItemEvent(final @NotNull Inventory sourceInventory, final @NotNull ItemStack itemStack, final @NotNull Inventory destinationInventory, final boolean didSourceInitiate) { ++ super(sourceInventory, itemStack, destinationInventory, didSourceInitiate); ++ } ++ ++ @Override ++ public ItemStack getItem() { ++ this.calledGetItem = true; ++ return super.getItem(); ++ } ++ ++ @Override ++ public void setItem(final ItemStack itemStack) { ++ super.setItem(itemStack); ++ this.calledSetItem = true; ++ } ++} +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 81d0a0c79958a137ac8bc703eb97f30cd10864e3..b12f69931e5b247c42bb4e56bcf1ef24deddfe2e 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -1756,6 +1756,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper - BlockPhysicsEvent + worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent ++ net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers + + this.profiler.push(() -> { + String s = String.valueOf(worldserver); +diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java +index 2d2b436cae5693051df14e5777d385d38d5f4dd6..3d6ea1a9bd87ee9ce34cf95db8835e5729b1ade0 100644 +--- a/src/main/java/net/minecraft/world/item/ItemStack.java ++++ b/src/main/java/net/minecraft/world/item/ItemStack.java +@@ -795,7 +795,13 @@ public final class ItemStack implements DataComponentHolder { + } + + public ItemStack copy() { +- if (this.isEmpty()) { ++ // Paper start - Perf: Optimize Hoppers ++ return this.copy(false); ++ } ++ ++ public ItemStack copy(boolean originalItem) { ++ if (!originalItem && this.isEmpty()) { ++ // Paper end - Perf: Optimize Hoppers + return ItemStack.EMPTY; + } else { + ItemStack itemstack = new ItemStack(this.getItem(), this.count, this.components.copy()); +diff --git a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java +index a7dfad2696e7145af1355a5db132af14d09a6f30..b0421823684ff6b9474b81675742d2ee3b17edf7 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java +@@ -37,6 +37,7 @@ import co.aikar.timings.MinecraftTimings; // Paper + import co.aikar.timings.Timing; // Paper + + public abstract class BlockEntity { ++ static boolean ignoreTileUpdates; // Paper - Perf: Optimize Hoppers + + public Timing tickTimer = MinecraftTimings.getTileEntityTimings(this); // Paper + // CraftBukkit start - data containers +@@ -210,6 +211,7 @@ public abstract class BlockEntity { + + public void setChanged() { + if (this.level != null) { ++ if (ignoreTileUpdates) return; // Paper - Perf: Optimize Hoppers + BlockEntity.setChanged(this.level, this.worldPosition, this.blockState); + } + +diff --git a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java +index 92086ca118d55ec49cefa5bf18977f8706e3e4b4..f78343b50641a63110b8867ad84298ba9e873c22 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java +@@ -156,6 +156,43 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + + } + ++ // Paper start - Perf: Optimize Hoppers ++ private static final int HOPPER_EMPTY = 0; ++ private static final int HOPPER_HAS_ITEMS = 1; ++ private static final int HOPPER_IS_FULL = 2; ++ ++ private static int getFullState(final HopperBlockEntity tileEntity) { ++ tileEntity.unpackLootTable(null); ++ ++ final List hopperItems = tileEntity.getItems(); ++ ++ boolean empty = true; ++ boolean full = true; ++ ++ for (int i = 0, len = hopperItems.size(); i < len; ++i) { ++ final ItemStack stack = hopperItems.get(i); ++ if (stack.isEmpty()) { ++ full = false; ++ continue; ++ } ++ ++ if (!full) { ++ // can't be full ++ return HOPPER_HAS_ITEMS; ++ } ++ ++ empty = false; ++ ++ if (stack.getCount() != stack.getMaxStackSize()) { ++ // can't be full or empty ++ return HOPPER_HAS_ITEMS; ++ } ++ } ++ ++ return empty ? HOPPER_EMPTY : (full ? HOPPER_IS_FULL : HOPPER_HAS_ITEMS); ++ } ++ // Paper end - Perf: Optimize Hoppers ++ + private static boolean tryMoveItems(Level world, BlockPos pos, BlockState state, HopperBlockEntity blockEntity, BooleanSupplier booleansupplier) { + if (world.isClientSide) { + return false; +@@ -163,11 +200,12 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + if (!blockEntity.isOnCooldown() && (Boolean) state.getValue(HopperBlock.ENABLED)) { + boolean flag = false; + +- if (!blockEntity.isEmpty()) { ++ final int fullState = getFullState(blockEntity); // Paper - Perf: Optimize Hoppers ++ if (fullState != HOPPER_EMPTY) { // Paper - Perf: Optimize Hoppers + flag = HopperBlockEntity.ejectItems(world, pos, blockEntity); + } + +- if (!blockEntity.inventoryFull()) { ++ if (fullState != HOPPER_IS_FULL || flag) { // Paper - Perf: Optimize Hoppers + flag |= booleansupplier.getAsBoolean(); + } + +@@ -198,6 +236,202 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + return false; + } + ++ // Paper start - Perf: Optimize Hoppers ++ private static boolean skipPullModeEventFire; ++ private static boolean skipPushModeEventFire; ++ public static boolean skipHopperEvents; ++ ++ private static boolean hopperPush(final Level level, final Container destination, final Direction direction, final HopperBlockEntity hopper) { ++ skipPushModeEventFire = skipHopperEvents; ++ boolean foundItem = false; ++ for (int i = 0; i < hopper.getContainerSize(); ++i) { ++ final ItemStack item = hopper.getItem(i); ++ if (!item.isEmpty()) { ++ foundItem = true; ++ ItemStack origItemStack = item; ++ ItemStack movedItem = origItemStack; ++ ++ final int originalItemCount = origItemStack.getCount(); ++ final int movedItemCount = Math.min(level.spigotConfig.hopperAmount, originalItemCount); ++ origItemStack.setCount(movedItemCount); ++ ++ // We only need to fire the event once to give protection plugins a chance to cancel this event ++ // Because nothing uses getItem, every event call should end up the same result. ++ if (!skipPushModeEventFire) { ++ movedItem = callPushMoveEvent(destination, movedItem, hopper); ++ if (movedItem == null) { // cancelled ++ origItemStack.setCount(originalItemCount); ++ return false; ++ } ++ } ++ ++ final ItemStack remainingItem = addItem(hopper, destination, movedItem, direction); ++ final int remainingItemCount = remainingItem.getCount(); ++ if (remainingItemCount != movedItemCount) { ++ origItemStack = origItemStack.copy(true); ++ origItemStack.setCount(originalItemCount); ++ if (!origItemStack.isEmpty()) { ++ origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount); ++ } ++ hopper.setItem(i, origItemStack); ++ destination.setChanged(); ++ return true; ++ } ++ origItemStack.setCount(originalItemCount); ++ } ++ } ++ if (foundItem && level.paperConfig().hopper.cooldownWhenFull) { // Inventory was full - cooldown ++ hopper.setCooldown(level.spigotConfig.hopperTransfer); ++ } ++ return false; ++ } ++ ++ private static boolean hopperPull(final Level level, final Hopper hopper, final Container container, ItemStack origItemStack, final int i) { ++ ItemStack movedItem = origItemStack; ++ final int originalItemCount = origItemStack.getCount(); ++ final int movedItemCount = Math.min(level.spigotConfig.hopperAmount, originalItemCount); ++ container.setChanged(); // original logic always marks source inv as changed even if no move happens. ++ movedItem.setCount(movedItemCount); ++ ++ if (!skipPullModeEventFire) { ++ movedItem = callPullMoveEvent(hopper, container, movedItem); ++ if (movedItem == null) { // cancelled ++ origItemStack.setCount(originalItemCount); ++ // Drastically improve performance by returning true. ++ // No plugin could of relied on the behavior of false as the other call ++ // site for IMIE did not exhibit the same behavior ++ return true; ++ } ++ } ++ ++ final ItemStack remainingItem = addItem(container, hopper, movedItem, null); ++ final int remainingItemCount = remainingItem.getCount(); ++ if (remainingItemCount != movedItemCount) { ++ origItemStack = origItemStack.copy(true); ++ origItemStack.setCount(originalItemCount); ++ if (!origItemStack.isEmpty()) { ++ origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount); ++ } ++ ++ ignoreTileUpdates = true; ++ container.setItem(i, origItemStack); ++ ignoreTileUpdates = false; ++ container.setChanged(); ++ return true; ++ } ++ origItemStack.setCount(originalItemCount); ++ ++ if (level.paperConfig().hopper.cooldownWhenFull) { ++ cooldownHopper(hopper); ++ } ++ ++ return false; ++ } ++ ++ @Nullable ++ private static ItemStack callPushMoveEvent(Container iinventory, ItemStack itemstack, HopperBlockEntity hopper) { ++ final Inventory destinationInventory = getInventory(iinventory); ++ final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(hopper.getOwner(false).getInventory(), ++ CraftItemStack.asCraftMirror(itemstack), destinationInventory, true); ++ final boolean result = event.callEvent(); ++ if (!event.calledGetItem && !event.calledSetItem) { ++ skipPushModeEventFire = true; ++ } ++ if (!result) { ++ cooldownHopper(hopper); ++ return null; ++ } ++ ++ if (event.calledSetItem) { ++ return CraftItemStack.asNMSCopy(event.getItem()); ++ } else { ++ return itemstack; ++ } ++ } ++ ++ @Nullable ++ private static ItemStack callPullMoveEvent(final Hopper hopper, final Container container, final ItemStack itemstack) { ++ final Inventory sourceInventory = getInventory(container); ++ final Inventory destination = getInventory(hopper); ++ ++ // Mirror is safe as no plugins ever use this item ++ final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(sourceInventory, CraftItemStack.asCraftMirror(itemstack), destination, false); ++ final boolean result = event.callEvent(); ++ if (!event.calledGetItem && !event.calledSetItem) { ++ skipPullModeEventFire = true; ++ } ++ if (!result) { ++ cooldownHopper(hopper); ++ return null; ++ } ++ ++ if (event.calledSetItem) { ++ return CraftItemStack.asNMSCopy(event.getItem()); ++ } else { ++ return itemstack; ++ } ++ } ++ ++ private static Inventory getInventory(final Container container) { ++ final Inventory sourceInventory; ++ if (container instanceof CompoundContainer compoundContainer) { ++ // Have to special-case large chests as they work oddly ++ sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest(compoundContainer); ++ } else if (container instanceof BlockEntity blockEntity) { ++ sourceInventory = blockEntity.getOwner(false).getInventory(); ++ } else if (container.getOwner() != null) { ++ sourceInventory = container.getOwner().getInventory(); ++ } else { ++ sourceInventory = new CraftInventory(container); ++ } ++ return sourceInventory; ++ } ++ ++ private static void cooldownHopper(final Hopper hopper) { ++ if (hopper instanceof HopperBlockEntity blockEntity && blockEntity.getLevel() != null) { ++ blockEntity.setCooldown(blockEntity.getLevel().spigotConfig.hopperTransfer); ++ } ++ } ++ ++ private static boolean allMatch(Container iinventory, Direction enumdirection, java.util.function.BiPredicate test) { ++ if (iinventory instanceof WorldlyContainer) { ++ for (int i : ((WorldlyContainer) iinventory).getSlotsForFace(enumdirection)) { ++ if (!test.test(iinventory.getItem(i), i)) { ++ return false; ++ } ++ } ++ } else { ++ int size = iinventory.getContainerSize(); ++ for (int i = 0; i < size; i++) { ++ if (!test.test(iinventory.getItem(i), i)) { ++ return false; ++ } ++ } ++ } ++ return true; ++ } ++ ++ private static boolean anyMatch(Container iinventory, Direction enumdirection, java.util.function.BiPredicate test) { ++ if (iinventory instanceof WorldlyContainer) { ++ for (int i : ((WorldlyContainer) iinventory).getSlotsForFace(enumdirection)) { ++ if (test.test(iinventory.getItem(i), i)) { ++ return true; ++ } ++ } ++ } else { ++ int size = iinventory.getContainerSize(); ++ for (int i = 0; i < size; i++) { ++ if (test.test(iinventory.getItem(i), i)) { ++ return true; ++ } ++ } ++ } ++ return true; ++ } ++ private static final java.util.function.BiPredicate STACK_SIZE_TEST = (itemstack, i) -> itemstack.getCount() >= itemstack.getMaxStackSize(); ++ private static final java.util.function.BiPredicate IS_EMPTY_TEST = (itemstack, i) -> itemstack.isEmpty(); ++ // Paper end - Perf: Optimize Hoppers ++ + private static boolean ejectItems(Level world, BlockPos pos, HopperBlockEntity blockEntity) { + Container iinventory = HopperBlockEntity.getAttachedContainer(world, pos, blockEntity); + +@@ -209,47 +443,50 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + if (HopperBlockEntity.isFullContainer(iinventory, enumdirection)) { + return false; + } else { +- for (int i = 0; i < blockEntity.getContainerSize(); ++i) { +- ItemStack itemstack = blockEntity.getItem(i); +- +- if (!itemstack.isEmpty()) { +- int j = itemstack.getCount(); +- // CraftBukkit start - Call event when pushing items into other inventories +- CraftItemStack oitemstack = CraftItemStack.asCraftMirror(blockEntity.removeItem(i, world.spigotConfig.hopperAmount)); // Spigot +- +- Inventory destinationInventory; +- // Have to special case large chests as they work oddly +- if (iinventory instanceof CompoundContainer) { +- destinationInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) iinventory); +- } else if (iinventory.getOwner() != null) { +- destinationInventory = iinventory.getOwner().getInventory(); +- } else { +- destinationInventory = new CraftInventory(iinventory); +- } +- +- InventoryMoveItemEvent event = new InventoryMoveItemEvent(blockEntity.getOwner().getInventory(), oitemstack, destinationInventory, true); +- world.getCraftServer().getPluginManager().callEvent(event); +- if (event.isCancelled()) { +- blockEntity.setItem(i, itemstack); +- blockEntity.setCooldown(world.spigotConfig.hopperTransfer); // Delay hopper checks // Spigot +- return false; +- } +- ItemStack itemstack1 = HopperBlockEntity.addItem(blockEntity, iinventory, CraftItemStack.asNMSCopy(event.getItem()), enumdirection); +- // CraftBukkit end +- +- if (itemstack1.isEmpty()) { +- iinventory.setChanged(); +- return true; +- } +- +- itemstack.setCount(j); +- if (j == 1) { +- blockEntity.setItem(i, itemstack); +- } +- } +- } +- +- return false; ++ // Paper start - Perf: Optimize Hoppers ++ return hopperPush(world, iinventory, enumdirection, blockEntity); ++ //for (int i = 0; i < blockEntity.getContainerSize(); ++i) { ++ // ItemStack itemstack = blockEntity.getItem(i); ++ ++ // if (!itemstack.isEmpty()) { ++ // int j = itemstack.getCount(); ++ // // CraftBukkit start - Call event when pushing items into other inventories ++ // CraftItemStack oitemstack = CraftItemStack.asCraftMirror(blockEntity.removeItem(i, world.spigotConfig.hopperAmount)); // Spigot ++ ++ // Inventory destinationInventory; ++ // // Have to special case large chests as they work oddly ++ // if (iinventory instanceof CompoundContainer) { ++ // destinationInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) iinventory); ++ // } else if (iinventory.getOwner() != null) { ++ // destinationInventory = iinventory.getOwner().getInventory(); ++ // } else { ++ // destinationInventory = new CraftInventory(iinventory); ++ // } ++ ++ // InventoryMoveItemEvent event = new InventoryMoveItemEvent(tileentityhopper.getOwner().getInventory(), oitemstack, destinationInventory, true); ++ // world.getCraftServer().getPluginManager().callEvent(event); ++ // if (event.isCancelled()) { ++ // blockEntity.setItem(i, itemstack); ++ // blockEntity.setCooldown(world.spigotConfig.hopperTransfer); // Delay hopper checks // Spigot ++ // return false; ++ // } ++ // ItemStack itemstack1 = HopperBlockEntity.addItem(blockEntity, iinventory, CraftItemStack.asNMSCopy(event.getItem()), enumdirection); ++ // // CraftBukkit end ++ ++ // if (itemstack1.isEmpty()) { ++ // iinventory.setChanged(); ++ // return true; ++ // } ++ ++ // itemstack.setCount(j); ++ // if (j == 1) { ++ // blockEntity.setItem(i, itemstack); ++ // } ++ // } ++ //} ++ ++ // return false; ++ // Paper end - Perf: Optimize Hoppers + } + } + } +@@ -300,7 +537,6 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + return false; + } + } +- + return true; + } + +@@ -342,48 +578,52 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + } + } + ++ @io.papermc.paper.annotation.DoNotUse // Paper - Optimize hoppers + private static boolean tryTakeInItemFromSlot(Hopper ihopper, Container iinventory, int i, Direction enumdirection, Level world) { // Spigot + ItemStack itemstack = iinventory.getItem(i); + + if (!itemstack.isEmpty() && HopperBlockEntity.canTakeItemFromContainer(ihopper, iinventory, itemstack, i, enumdirection)) { +- int j = itemstack.getCount(); +- // CraftBukkit start - Call event on collection of items from inventories into the hopper +- CraftItemStack oitemstack = CraftItemStack.asCraftMirror(iinventory.removeItem(i, world.spigotConfig.hopperAmount)); // Spigot +- +- Inventory sourceInventory; +- // Have to special case large chests as they work oddly +- if (iinventory instanceof CompoundContainer) { +- sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) iinventory); +- } else if (iinventory.getOwner() != null) { +- sourceInventory = iinventory.getOwner().getInventory(); +- } else { +- sourceInventory = new CraftInventory(iinventory); +- } +- +- InventoryMoveItemEvent event = new InventoryMoveItemEvent(sourceInventory, oitemstack, ihopper.getOwner().getInventory(), false); +- +- Bukkit.getServer().getPluginManager().callEvent(event); +- if (event.isCancelled()) { +- iinventory.setItem(i, itemstack); +- +- if (ihopper instanceof HopperBlockEntity) { +- ((HopperBlockEntity) ihopper).setCooldown(world.spigotConfig.hopperTransfer); // Spigot +- } +- +- return false; +- } +- ItemStack itemstack1 = HopperBlockEntity.addItem(iinventory, ihopper, CraftItemStack.asNMSCopy(event.getItem()), null); +- // CraftBukkit end +- +- if (itemstack1.isEmpty()) { +- iinventory.setChanged(); +- return true; +- } +- +- itemstack.setCount(j); +- if (j == 1) { +- iinventory.setItem(i, itemstack); +- } ++ // Paper start - Perf: Optimize Hoppers ++ return hopperPull(world, ihopper, iinventory, itemstack, i); ++ // int j = itemstack.getCount(); ++ // // CraftBukkit start - Call event on collection of items from inventories into the hopper ++ // CraftItemStack oitemstack = CraftItemStack.asCraftMirror(iinventory.removeItem(i, world.spigotConfig.hopperAmount)); // Spigot ++ ++ // Inventory sourceInventory; ++ // // Have to special case large chests as they work oddly ++ // if (iinventory instanceof CompoundContainer) { ++ // sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) iinventory); ++ // } else if (iinventory.getOwner() != null) { ++ // sourceInventory = iinventory.getOwner().getInventory(); ++ // } else { ++ // sourceInventory = new CraftInventory(iinventory); ++ // } ++ ++ // InventoryMoveItemEvent event = new InventoryMoveItemEvent(sourceInventory, oitemstack, ihopper.getOwner().getInventory(), false); ++ ++ // Bukkit.getServer().getPluginManager().callEvent(event); ++ // if (event.isCancelled()) { ++ // iinventory.setItem(i, itemstack); ++ ++ // if (ihopper instanceof HopperBlockEntity) { ++ // ((HopperBlockEntity) ihopper).setCooldown(world.spigotConfig.hopperTransfer); // Spigot ++ // } ++ ++ // return false; ++ // } ++ // ItemStack itemstack1 = HopperBlockEntity.addItem(iinventory, ihopper, CraftItemStack.asNMSCopy(event.getItem()), null); ++ // // CraftBukkit end ++ ++ // if (itemstack1.isEmpty()) { ++ // iinventory.setChanged(); ++ // return true; ++ // } ++ ++ // itemstack.setCount(j); ++ // if (j == 1) { ++ // iinventory.setItem(i, itemstack); ++ // } ++ // Paper end - Perf: Optimize Hoppers + } + + return false; +@@ -392,12 +632,14 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + public static boolean addItem(Container inventory, ItemEntity itemEntity) { + boolean flag = false; + // CraftBukkit start +- InventoryPickupItemEvent event = new InventoryPickupItemEvent(inventory.getOwner().getInventory(), (org.bukkit.entity.Item) itemEntity.getBukkitEntity()); ++ if (InventoryPickupItemEvent.getHandlerList().getRegisteredListeners().length > 0) { // Paper - optimize hoppers ++ InventoryPickupItemEvent event = new InventoryPickupItemEvent(getInventory(inventory), (org.bukkit.entity.Item) itemEntity.getBukkitEntity()); // Paper - Perf: Optimize Hoppers; use getInventory() to avoid snapshot creation + itemEntity.level().getCraftServer().getPluginManager().callEvent(event); + if (event.isCancelled()) { + return false; + } + // CraftBukkit end ++ } // Paper - Perf: Optimize Hoppers + ItemStack itemstack = itemEntity.getItem().copy(); + ItemStack itemstack1 = HopperBlockEntity.addItem((Container) null, inventory, itemstack, (Direction) null); + +@@ -491,7 +733,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + stack = stack.split(to.getMaxStackSize()); + } + // Spigot end ++ ignoreTileUpdates = true; // Paper - Perf: Optimize Hoppers + to.setItem(slot, stack); ++ ignoreTileUpdates = false; // Paper - Perf: Optimize Hoppers + stack = leftover; // Paper - Make hoppers respect inventory max stack size + flag = true; + } else if (HopperBlockEntity.canMergeItems(itemstack1, stack)) { +@@ -571,14 +815,20 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + + @Nullable + public static Container getContainerAt(Level world, BlockPos pos) { +- return HopperBlockEntity.getContainerAt(world, pos, world.getBlockState(pos), (double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D); ++ return HopperBlockEntity.getContainerAt(world, pos, world.getBlockState(pos), (double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, true); + } + + @Nullable + private static Container getContainerAt(Level world, BlockPos pos, BlockState state, double x, double y, double z) { ++ // Paper start - Perf: Optimize Hoppers ++ return HopperBlockEntity.getContainerAt(world, pos, state, x, y, z, false); ++ } ++ @Nullable ++ private static Container getContainerAt(Level world, BlockPos pos, BlockState state, double x, double y, double z, boolean optimizeEntities) { ++ // Paper end - Perf: Optimize Hoppers + Container iinventory = HopperBlockEntity.getBlockContainer(world, pos, state); + +- if (iinventory == null) { ++ if (iinventory == null && (!optimizeEntities || !world.paperConfig().hopper.ignoreOccludingBlocks || !state.getBukkitMaterial().isOccluding())) { // Paper - Perf: Optimize Hoppers + iinventory = HopperBlockEntity.getEntityContainer(world, x, y, z); + } + +@@ -613,13 +863,13 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + + @Nullable + private static Container getEntityContainer(Level world, double x, double y, double z) { +- List list = world.getEntities((Entity) null, new AABB(x - 0.5D, y - 0.5D, z - 0.5D, x + 0.5D, y + 0.5D, z + 0.5D), EntitySelector.CONTAINER_ENTITY_SELECTOR); ++ List list = world.getEntitiesOfClass((Class) Container.class, new AABB(x - 0.5D, y - 0.5D, z - 0.5D, x + 0.5D, y + 0.5D, z + 0.5D), EntitySelector.CONTAINER_ENTITY_SELECTOR); // Paper - Perf: Optimize hoppers + + return !list.isEmpty() ? (Container) list.get(world.random.nextInt(list.size())) : null; + } + + private static boolean canMergeItems(ItemStack first, ItemStack second) { +- return first.getCount() <= first.getMaxStackSize() && ItemStack.isSameItemSameComponents(first, second); ++ return first.getCount() < first.getMaxStackSize() && ItemStack.isSameItemSameComponents(first, second); // Paper - Perf: Optimize Hoppers; used to return true for full itemstacks?! + } + + @Override +diff --git a/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java +index 5b183f12717008dd6c9863938c17b2e668ebded4..17da915ffa638500a83b67db0940a7b9a9b333a1 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java +@@ -104,7 +104,7 @@ public abstract class RandomizableContainerBlockEntity extends BaseContainerBloc + + @Override + public ItemStack getItem(int slot) { +- this.unpackLootTable(null); ++ if (slot == 0) this.unpackLootTable(null); // Paper - Perf: Optimize Hoppers + return super.getItem(slot); + } + diff --git a/patches/server/1025-Properly-resend-entities.patch b/patches/server/1025-Properly-resend-entities.patch deleted file mode 100644 index 4ac71e7b7c..0000000000 --- a/patches/server/1025-Properly-resend-entities.patch +++ /dev/null @@ -1,232 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> -Date: Wed, 7 Dec 2022 17:25:19 -0500 -Subject: [PATCH] Properly resend entities - -This resolves some issues which caused entities to not be resent correctly. -Entities that are interacted with need to be resent to the client, so we resend all the entity -data to the player whilst making sure not to clear dirty entries from the tracker. This makes -sure that values will be correctly updated to other players. - -This also adds utilities to aid in further preventing entity desyncs. - -This also also fixes the bug causing cancelling PlayerInteractEvent to cause items to continue -to be used despite being cancelled on the server. - -For example, items being consumed but never finishing, shields being put up, etc. -The underlying issue of this is that the client modifies their synced data values, -and so we have to (forcibly) resend them in order for the client to reset their using item state. - -See: https://github.com/PaperMC/Paper/pull/1896 - -== AT == -public net.minecraft.server.level.ChunkMap$TrackedEntity serverEntity - -diff --git a/src/main/java/net/minecraft/network/syncher/SynchedEntityData.java b/src/main/java/net/minecraft/network/syncher/SynchedEntityData.java -index 02bf2705ca1c99023a83a22d92e1962181102297..0f99733660f91280e4c6262cf75b3c9cae86f65a 100644 ---- a/src/main/java/net/minecraft/network/syncher/SynchedEntityData.java -+++ b/src/main/java/net/minecraft/network/syncher/SynchedEntityData.java -@@ -50,7 +50,7 @@ public class SynchedEntityData { - } - } - -- private SynchedEntityData.DataItem getItem(EntityDataAccessor key) { -+ public SynchedEntityData.DataItem getItem(EntityDataAccessor key) { // Paper - public - return (SynchedEntityData.DataItem) this.itemsById[key.id()]; // CraftBukkit - decompile error - } - -@@ -151,6 +151,20 @@ public class SynchedEntityData { - } - } - -+ // Paper start -+ // We need to pack all as we cannot rely on "non default values" or "dirty" ones. -+ // Because these values can possibly be desynced on the client. -+ @Nullable -+ public List> packAll() { -+ final List> list = new ArrayList<>(); -+ for (final DataItem dataItem : this.itemsById) { -+ list.add(dataItem.value()); -+ } -+ -+ return list; -+ } -+ // Paper end -+ - public static class DataItem { - - final EntityDataAccessor accessor; -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -index abb9a86cd42a34cf722a312068134e820ac21956..f168044d36f22080504da171e5ed31a6f02385ba 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -@@ -556,6 +556,7 @@ public class ServerPlayerGameMode { - } - // Paper end - extend Player Interact cancellation - player.getBukkitEntity().updateInventory(); // SPIGOT-2867 -+ this.player.resyncUsingItem(this.player); // Paper - Properly cancel usable items - return (event.useItemInHand() != Event.Result.ALLOW) ? InteractionResult.SUCCESS : InteractionResult.PASS; - } else if (this.gameModeForPlayer == GameType.SPECTATOR) { - MenuProvider itileinventory = iblockdata.getMenuProvider(world, blockposition); -@@ -607,6 +608,11 @@ public class ServerPlayerGameMode { - - return enuminteractionresult; - } else { -+ // Paper start - Properly cancel usable items; Cancel only if cancelled + if the interact result is different from default response -+ if (this.interactResult && this.interactResult != cancelledItem) { -+ this.player.resyncUsingItem(this.player); -+ } -+ // Paper end - Properly cancel usable items - return InteractionResult.PASS; - } - } -diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index ae927c2cdf151b901822636a2543668f85946430..ea7a2db0d71a69a6f67fda032f01c9a126bd3100 100644 ---- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -2004,6 +2004,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - } - - if (cancelled) { -+ this.player.resyncUsingItem(this.player); // Paper - Properly cancel usable items - this.player.getBukkitEntity().updateInventory(); // SPIGOT-2524 - return; - } -@@ -2788,7 +2789,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - - // Entity in bucket - SPIGOT-4048 and SPIGOT-6859a - if ((entity instanceof Bucketable && entity instanceof LivingEntity && origItem != null && origItem.asItem() == Items.WATER_BUCKET) && (event.isCancelled() || ServerGamePacketListenerImpl.this.player.getInventory().getSelected() == null || ServerGamePacketListenerImpl.this.player.getInventory().getSelected().getItem() != origItem)) { -- ServerGamePacketListenerImpl.this.send(new ClientboundAddEntityPacket(entity)); -+ entity.resendPossiblyDesyncedEntityData(ServerGamePacketListenerImpl.this.player); // Paper - The entire mob gets deleted, so resend it. - ServerGamePacketListenerImpl.this.player.containerMenu.sendAllDataToRemote(); - } - -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 68446b7532dfbda303293aa9e756644c6fcdffca..a2142930b4d4b05987c90496fb9d733d99040aa0 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -393,7 +393,7 @@ public abstract class PlayerList { - ((ServerLevel)player.level()).getChunkSource().chunkMap.addEntity(player); // Paper - Fire PlayerJoinEvent when Player is actually ready; track entity now - // CraftBukkit end - -- player.refreshEntityData(player); // CraftBukkit - BungeeCord#2321, send complete data to self on spawn -+ //player.refreshEntityData(player); // CraftBukkit - BungeeCord#2321, send complete data to self on spawn // Paper - THIS IS NOT NEEDED ANYMORE - - this.sendLevelInfo(player, worldserver1); - -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 8ad2d17615ff489b2fbbb13480dd0b217a42d805..2bc85351e6e52f90da5fdb29d8d042a06132d742 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -704,13 +704,44 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - - // CraftBukkit start - public void refreshEntityData(ServerPlayer to) { -- List> list = this.getEntityData().getNonDefaultValues(); -+ List> list = this.entityData.packAll(); // Paper - Update EVERYTHING not just not default - -- if (list != null) { -+ if (list != null && to.getBukkitEntity().canSee(this.getBukkitEntity())) { // Paper - to.connection.send(new ClientboundSetEntityDataPacket(this.getId(), list)); - } - } - // CraftBukkit end -+ // Paper start -+ // This method should only be used if the data of an entity could have become desynced -+ // due to interactions on the client. -+ public void resendPossiblyDesyncedEntityData(net.minecraft.server.level.ServerPlayer player) { -+ if (this.tracker == null) { -+ return; -+ } -+ -+ if (player.getBukkitEntity().canSee(this.getBukkitEntity())) { -+ final net.minecraft.server.level.ServerEntity serverEntity = this.tracker.serverEntity; -+ final List> list = new java.util.ArrayList<>(); -+ serverEntity.sendPairingData(player, list::add); -+ player.connection.send(new net.minecraft.network.protocol.game.ClientboundBundlePacket(list)); -+ } -+ } -+ -+ // This method allows you to specifically resend certain data accessor keys to the client -+ public void resendPossiblyDesyncedDataValues(List> keys, ServerPlayer to) { -+ if (!to.getBukkitEntity().canSee(this.getBukkitEntity())) { -+ return; -+ } -+ -+ final List> values = new java.util.ArrayList<>(keys.size()); -+ for (final EntityDataAccessor key : keys) { -+ final SynchedEntityData.DataItem synchedValue = this.entityData.getItem(key); -+ values.add(synchedValue.value()); -+ } -+ -+ to.connection.send(new ClientboundSetEntityDataPacket(this.id, values)); -+ } -+ // Paper end - - public boolean equals(Object object) { - return object instanceof Entity ? ((Entity) object).id == this.id : false; -diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index 75e01c1e01e0782fc8af48777bbe4716d37aeafa..2d375fc98e72246f6a640c390cb50d055fb59f8b 100644 ---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -3828,6 +3828,11 @@ public abstract class LivingEntity extends Entity implements Attackable { - return ((Byte) this.entityData.get(LivingEntity.DATA_LIVING_ENTITY_FLAGS) & 2) > 0 ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND; - } - -+ // Paper start - Properly cancel usable items -+ public void resyncUsingItem(ServerPlayer serverPlayer) { -+ this.resendPossiblyDesyncedDataValues(java.util.List.of(DATA_LIVING_ENTITY_FLAGS), serverPlayer); -+ } -+ // Paper end - Properly cancel usable items - private void updatingUsingItem() { - if (this.isUsingItem()) { - if (ItemStack.isSameItem(this.getItemInHand(this.getUsedItemHand()), this.useItem)) { -diff --git a/src/main/java/net/minecraft/world/entity/animal/Bucketable.java b/src/main/java/net/minecraft/world/entity/animal/Bucketable.java -index cb4a6439e9774bbec07e69b13df8dddd395b9ece..cfe37b4f5e33795ee717d824d86e3a0919129cf5 100644 ---- a/src/main/java/net/minecraft/world/entity/animal/Bucketable.java -+++ b/src/main/java/net/minecraft/world/entity/animal/Bucketable.java -@@ -109,8 +109,7 @@ public interface Bucketable { - itemstack1 = CraftItemStack.asNMSCopy(playerBucketFishEvent.getEntityBucket()); - if (playerBucketFishEvent.isCancelled()) { - ((ServerPlayer) player).containerMenu.sendAllDataToRemote(); // We need to update inventory to resync client's bucket -- ((ServerPlayer) player).connection.send(new ClientboundAddEntityPacket(entity)); // We need to play out these packets as the client assumes the fish is gone -- entity.refreshEntityData((ServerPlayer) player); // Need to send data such as the display name to client -+ entity.resendPossiblyDesyncedEntityData((ServerPlayer) player); // Paper - return Optional.of(InteractionResult.FAIL); - } - entity.playSound(((Bucketable) entity).getPickupSound(), 1.0F, 1.0F); -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -index 3ab04c4bdbe26ff7f6f54eb9cdd58376c592fa05..a2d336ceb52b63db5c03432ee7bc94dc6a742b82 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -@@ -1010,7 +1010,11 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { - return; - } - -- entityTracker.broadcast(this.getHandle().getAddEntityPacket()); -+ // Paper start, resend possibly desynced entity instead of add entity packet -+ for (ServerPlayerConnection playerConnection : entityTracker.seenBy) { -+ this.getHandle().resendPossiblyDesyncedEntityData(playerConnection.getPlayer()); -+ } -+ // Paper end - } - - private static PermissibleBase getPermissibleBase() { -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java -index 0801bcdee8fcff0d388d302387e4f1d587e0a283..2fcd9b836d42e3549a3b6b921c57a4c103146dff 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java -@@ -39,9 +39,11 @@ public class CraftItemFrame extends CraftHanging implements ItemFrame { - protected void update() { - super.update(); - -+ // Paper start, don't mark as dirty as this is handled in super.update() - // mark dirty, so that the client gets updated with item and rotation -- this.getHandle().getEntityData().markDirty(net.minecraft.world.entity.decoration.ItemFrame.DATA_ITEM); -- this.getHandle().getEntityData().markDirty(net.minecraft.world.entity.decoration.ItemFrame.DATA_ROTATION); -+ //this.getHandle().getEntityData().markDirty(net.minecraft.world.entity.decoration.ItemFrame.DATA_ITEM); -+ //this.getHandle().getEntityData().markDirty(net.minecraft.world.entity.decoration.ItemFrame.DATA_ROTATION); -+ // Paper end - - // update redstone - if (!this.getHandle().generation) { diff --git a/patches/server/1026-Improve-performance-of-mass-crafts.patch b/patches/server/1026-Improve-performance-of-mass-crafts.patch new file mode 100644 index 0000000000..f9839d9135 --- /dev/null +++ b/patches/server/1026-Improve-performance-of-mass-crafts.patch @@ -0,0 +1,74 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Sun, 13 Aug 2023 15:41:52 -0700 +Subject: [PATCH] Improve performance of mass crafts + +When the server crafts all available items in CraftingMenu or InventoryMenu the game +checks either 4 or 9 times for each individual craft for a matching recipe for that container. +This check can be expensive if 64 total crafts are being performed with the recipe matching logic +being run 64 * 9 + 64 times. A breakdown of those times is below. This patch caches the last matching +recipe so that it is checked first and only if it doesn't match does the rest of the matching logic run. + +Shift-click crafts are processed one at a time, so shift clicking on an item in the result of a iron block craft +where all the 9 inputs are full stacks of iron will run 64 iron block crafts. For each of those crafts, the +'remaining' blocks are calculated. This is due to recipes that have leftover items like buckets. This is done +for each craft, and done once to get the full 9 leftover items which are usually air. Then 1 item is removed +from each of the 9 inputs and each time that happens, logic is triggered to update the result itemstack. So +for each craft, that logic is run 9 times (hence the 64 * 9). The + 64 is from the 64 checks for remaining items. + +After this patch, the full iteration over all recipes checking for a match should run once for a full craft to find the +initial recipe match. Then that recipe will be checked first for all future recipe match checks. + +diff --git a/src/main/java/net/minecraft/world/inventory/CraftingMenu.java b/src/main/java/net/minecraft/world/inventory/CraftingMenu.java +index 7a0c1a55a211035bbca7b97293e94b04ae308bae..59220eb9e74877e18b3e015dc0d25c892f45f746 100644 +--- a/src/main/java/net/minecraft/world/inventory/CraftingMenu.java ++++ b/src/main/java/net/minecraft/world/inventory/CraftingMenu.java +@@ -76,7 +76,8 @@ public class CraftingMenu extends RecipeBookMenu { + if (!world.isClientSide) { + ServerPlayer entityplayer = (ServerPlayer) player; + ItemStack itemstack = ItemStack.EMPTY; +- Optional> optional = world.getServer().getRecipeManager().getRecipeFor(RecipeType.CRAFTING, craftingInventory, world); ++ final RecipeHolder currentRecipe = craftingInventory.getCurrentRecipe(); // Paper - Perf: Improve mass crafting; check last recipe used first ++ Optional> optional = currentRecipe == null ? world.getServer().getRecipeManager().getRecipeFor(RecipeType.CRAFTING, craftingInventory, world) : world.getServer().getRecipeManager().getRecipeFor(RecipeType.CRAFTING, craftingInventory, world, currentRecipe.id()); // Paper - Perf: Improve mass crafting; check last recipe used first + + if (optional.isPresent()) { + RecipeHolder recipeholder = (RecipeHolder) optional.get(); +diff --git a/src/main/java/net/minecraft/world/inventory/ResultSlot.java b/src/main/java/net/minecraft/world/inventory/ResultSlot.java +index fef82418358ecf19d367dafbec159bd78ea97494..f17ff5988d826c8fad68f6bf7bac1d06edae01ae 100644 +--- a/src/main/java/net/minecraft/world/inventory/ResultSlot.java ++++ b/src/main/java/net/minecraft/world/inventory/ResultSlot.java +@@ -58,7 +58,7 @@ public class ResultSlot extends Slot { + @Override + public void onTake(Player player, ItemStack stack) { + this.checkTakeAchievements(stack); +- NonNullList nonNullList = player.level().getRecipeManager().getRemainingItemsFor(RecipeType.CRAFTING, this.craftSlots, player.level()); ++ NonNullList nonNullList = player.level().getRecipeManager().getRemainingItemsFor(RecipeType.CRAFTING, this.craftSlots, player.level(), this.craftSlots.getCurrentRecipe() != null ? this.craftSlots.getCurrentRecipe().id() : null); // Paper - Perf: Improve mass crafting; check last recipe used first + + for (int i = 0; i < nonNullList.size(); i++) { + ItemStack itemStack = this.craftSlots.getItem(i); +diff --git a/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java b/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java +index 0126b88f60904dfbf1e29eb3b89a985061d91f91..a31326e24cb68472c81cd781c5e3041772712862 100644 +--- a/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java ++++ b/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java +@@ -116,6 +116,7 @@ public class RecipeManager extends SimpleJsonResourceReloadListener { + RecipeHolder recipeholder = this.byKeyTyped(type, id); + + if (recipeholder != null && recipeholder.value().matches(inventory, world)) { ++ inventory.setCurrentRecipe(recipeholder); // Paper - Perf: Improve mass crafting + return Optional.of(recipeholder); + } + } +@@ -147,7 +148,12 @@ public class RecipeManager extends SimpleJsonResourceReloadListener { + } + + public > NonNullList getRemainingItemsFor(RecipeType type, C inventory, Level world) { +- Optional> optional = this.getRecipeFor(type, inventory, world); ++ // Paper start - Perf: Improve mass crafting;; check last recipe used first ++ return this.getRemainingItemsFor(type, inventory, world, null); ++ } ++ public > NonNullList getRemainingItemsFor(RecipeType type, C inventory, Level world, @Nullable ResourceLocation firstToCheck) { ++ Optional> optional = firstToCheck == null ? this.getRecipeFor(type, inventory, world) : this.getRecipeFor(type, inventory, world, firstToCheck); ++ // Paper end - Perf: Improve mass crafting + + if (optional.isPresent()) { + return ((RecipeHolder) optional.get()).value().getRemainingItems(inventory); diff --git a/patches/server/1026-Optimize-Hoppers.patch b/patches/server/1026-Optimize-Hoppers.patch deleted file mode 100644 index adf6bca823..0000000000 --- a/patches/server/1026-Optimize-Hoppers.patch +++ /dev/null @@ -1,637 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Wed, 27 Apr 2016 22:09:52 -0400 -Subject: [PATCH] Optimize Hoppers - -* Removes unnecessary extra calls to .update() that are very expensive -* Lots of itemstack cloning removed. Only clone if the item is actually moved -* Return true when a plugin cancels inventory move item event instead of false, as false causes pulls to cycle through all items. - However, pushes do not exhibit the same behavior, so this is not something plugins could of been relying on. -* Add option (Default on) to cooldown hoppers when they fail to move an item due to full inventory -* Skip subsequent InventoryMoveItemEvents if a plugin does not use the item after first event fire for an iteration by tracking changes to the event via an internal event implementation. -* Don't check for Entities with Inventories if the block above us is also occluding (not just Inventoried) -* Remove Streams from Item Suck In and restore restore 1.12 AABB checks which is simpler and no voxel allocations (was doing TWO Item Suck ins) - -diff --git a/src/main/java/io/papermc/paper/event/inventory/PaperInventoryMoveItemEvent.java b/src/main/java/io/papermc/paper/event/inventory/PaperInventoryMoveItemEvent.java -new file mode 100644 -index 0000000000000000000000000000000000000000..5c42823726e70ce6c9d0121d074315488e8b3f60 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/event/inventory/PaperInventoryMoveItemEvent.java -@@ -0,0 +1,31 @@ -+package io.papermc.paper.event.inventory; -+ -+import org.bukkit.event.inventory.InventoryMoveItemEvent; -+import org.bukkit.inventory.Inventory; -+import org.bukkit.inventory.ItemStack; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.framework.qual.DefaultQualifier; -+import org.jetbrains.annotations.NotNull; -+ -+@DefaultQualifier(NonNull.class) -+public class PaperInventoryMoveItemEvent extends InventoryMoveItemEvent { -+ -+ public boolean calledSetItem; -+ public boolean calledGetItem; -+ -+ public PaperInventoryMoveItemEvent(final @NotNull Inventory sourceInventory, final @NotNull ItemStack itemStack, final @NotNull Inventory destinationInventory, final boolean didSourceInitiate) { -+ super(sourceInventory, itemStack, destinationInventory, didSourceInitiate); -+ } -+ -+ @Override -+ public ItemStack getItem() { -+ this.calledGetItem = true; -+ return super.getItem(); -+ } -+ -+ @Override -+ public void setItem(final ItemStack itemStack) { -+ super.setItem(itemStack); -+ this.calledSetItem = true; -+ } -+} -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 81d0a0c79958a137ac8bc703eb97f30cd10864e3..b12f69931e5b247c42bb4e56bcf1ef24deddfe2e 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1756,6 +1756,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper - BlockPhysicsEvent - worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent -+ net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers - - this.profiler.push(() -> { - String s = String.valueOf(worldserver); -diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java -index 2d2b436cae5693051df14e5777d385d38d5f4dd6..3d6ea1a9bd87ee9ce34cf95db8835e5729b1ade0 100644 ---- a/src/main/java/net/minecraft/world/item/ItemStack.java -+++ b/src/main/java/net/minecraft/world/item/ItemStack.java -@@ -795,7 +795,13 @@ public final class ItemStack implements DataComponentHolder { - } - - public ItemStack copy() { -- if (this.isEmpty()) { -+ // Paper start - Perf: Optimize Hoppers -+ return this.copy(false); -+ } -+ -+ public ItemStack copy(boolean originalItem) { -+ if (!originalItem && this.isEmpty()) { -+ // Paper end - Perf: Optimize Hoppers - return ItemStack.EMPTY; - } else { - ItemStack itemstack = new ItemStack(this.getItem(), this.count, this.components.copy()); -diff --git a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java -index a7dfad2696e7145af1355a5db132af14d09a6f30..b0421823684ff6b9474b81675742d2ee3b17edf7 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java -@@ -37,6 +37,7 @@ import co.aikar.timings.MinecraftTimings; // Paper - import co.aikar.timings.Timing; // Paper - - public abstract class BlockEntity { -+ static boolean ignoreTileUpdates; // Paper - Perf: Optimize Hoppers - - public Timing tickTimer = MinecraftTimings.getTileEntityTimings(this); // Paper - // CraftBukkit start - data containers -@@ -210,6 +211,7 @@ public abstract class BlockEntity { - - public void setChanged() { - if (this.level != null) { -+ if (ignoreTileUpdates) return; // Paper - Perf: Optimize Hoppers - BlockEntity.setChanged(this.level, this.worldPosition, this.blockState); - } - -diff --git a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java -index 92086ca118d55ec49cefa5bf18977f8706e3e4b4..f78343b50641a63110b8867ad84298ba9e873c22 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java -@@ -156,6 +156,43 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - - } - -+ // Paper start - Perf: Optimize Hoppers -+ private static final int HOPPER_EMPTY = 0; -+ private static final int HOPPER_HAS_ITEMS = 1; -+ private static final int HOPPER_IS_FULL = 2; -+ -+ private static int getFullState(final HopperBlockEntity tileEntity) { -+ tileEntity.unpackLootTable(null); -+ -+ final List hopperItems = tileEntity.getItems(); -+ -+ boolean empty = true; -+ boolean full = true; -+ -+ for (int i = 0, len = hopperItems.size(); i < len; ++i) { -+ final ItemStack stack = hopperItems.get(i); -+ if (stack.isEmpty()) { -+ full = false; -+ continue; -+ } -+ -+ if (!full) { -+ // can't be full -+ return HOPPER_HAS_ITEMS; -+ } -+ -+ empty = false; -+ -+ if (stack.getCount() != stack.getMaxStackSize()) { -+ // can't be full or empty -+ return HOPPER_HAS_ITEMS; -+ } -+ } -+ -+ return empty ? HOPPER_EMPTY : (full ? HOPPER_IS_FULL : HOPPER_HAS_ITEMS); -+ } -+ // Paper end - Perf: Optimize Hoppers -+ - private static boolean tryMoveItems(Level world, BlockPos pos, BlockState state, HopperBlockEntity blockEntity, BooleanSupplier booleansupplier) { - if (world.isClientSide) { - return false; -@@ -163,11 +200,12 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - if (!blockEntity.isOnCooldown() && (Boolean) state.getValue(HopperBlock.ENABLED)) { - boolean flag = false; - -- if (!blockEntity.isEmpty()) { -+ final int fullState = getFullState(blockEntity); // Paper - Perf: Optimize Hoppers -+ if (fullState != HOPPER_EMPTY) { // Paper - Perf: Optimize Hoppers - flag = HopperBlockEntity.ejectItems(world, pos, blockEntity); - } - -- if (!blockEntity.inventoryFull()) { -+ if (fullState != HOPPER_IS_FULL || flag) { // Paper - Perf: Optimize Hoppers - flag |= booleansupplier.getAsBoolean(); - } - -@@ -198,6 +236,202 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - return false; - } - -+ // Paper start - Perf: Optimize Hoppers -+ private static boolean skipPullModeEventFire; -+ private static boolean skipPushModeEventFire; -+ public static boolean skipHopperEvents; -+ -+ private static boolean hopperPush(final Level level, final Container destination, final Direction direction, final HopperBlockEntity hopper) { -+ skipPushModeEventFire = skipHopperEvents; -+ boolean foundItem = false; -+ for (int i = 0; i < hopper.getContainerSize(); ++i) { -+ final ItemStack item = hopper.getItem(i); -+ if (!item.isEmpty()) { -+ foundItem = true; -+ ItemStack origItemStack = item; -+ ItemStack movedItem = origItemStack; -+ -+ final int originalItemCount = origItemStack.getCount(); -+ final int movedItemCount = Math.min(level.spigotConfig.hopperAmount, originalItemCount); -+ origItemStack.setCount(movedItemCount); -+ -+ // We only need to fire the event once to give protection plugins a chance to cancel this event -+ // Because nothing uses getItem, every event call should end up the same result. -+ if (!skipPushModeEventFire) { -+ movedItem = callPushMoveEvent(destination, movedItem, hopper); -+ if (movedItem == null) { // cancelled -+ origItemStack.setCount(originalItemCount); -+ return false; -+ } -+ } -+ -+ final ItemStack remainingItem = addItem(hopper, destination, movedItem, direction); -+ final int remainingItemCount = remainingItem.getCount(); -+ if (remainingItemCount != movedItemCount) { -+ origItemStack = origItemStack.copy(true); -+ origItemStack.setCount(originalItemCount); -+ if (!origItemStack.isEmpty()) { -+ origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount); -+ } -+ hopper.setItem(i, origItemStack); -+ destination.setChanged(); -+ return true; -+ } -+ origItemStack.setCount(originalItemCount); -+ } -+ } -+ if (foundItem && level.paperConfig().hopper.cooldownWhenFull) { // Inventory was full - cooldown -+ hopper.setCooldown(level.spigotConfig.hopperTransfer); -+ } -+ return false; -+ } -+ -+ private static boolean hopperPull(final Level level, final Hopper hopper, final Container container, ItemStack origItemStack, final int i) { -+ ItemStack movedItem = origItemStack; -+ final int originalItemCount = origItemStack.getCount(); -+ final int movedItemCount = Math.min(level.spigotConfig.hopperAmount, originalItemCount); -+ container.setChanged(); // original logic always marks source inv as changed even if no move happens. -+ movedItem.setCount(movedItemCount); -+ -+ if (!skipPullModeEventFire) { -+ movedItem = callPullMoveEvent(hopper, container, movedItem); -+ if (movedItem == null) { // cancelled -+ origItemStack.setCount(originalItemCount); -+ // Drastically improve performance by returning true. -+ // No plugin could of relied on the behavior of false as the other call -+ // site for IMIE did not exhibit the same behavior -+ return true; -+ } -+ } -+ -+ final ItemStack remainingItem = addItem(container, hopper, movedItem, null); -+ final int remainingItemCount = remainingItem.getCount(); -+ if (remainingItemCount != movedItemCount) { -+ origItemStack = origItemStack.copy(true); -+ origItemStack.setCount(originalItemCount); -+ if (!origItemStack.isEmpty()) { -+ origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount); -+ } -+ -+ ignoreTileUpdates = true; -+ container.setItem(i, origItemStack); -+ ignoreTileUpdates = false; -+ container.setChanged(); -+ return true; -+ } -+ origItemStack.setCount(originalItemCount); -+ -+ if (level.paperConfig().hopper.cooldownWhenFull) { -+ cooldownHopper(hopper); -+ } -+ -+ return false; -+ } -+ -+ @Nullable -+ private static ItemStack callPushMoveEvent(Container iinventory, ItemStack itemstack, HopperBlockEntity hopper) { -+ final Inventory destinationInventory = getInventory(iinventory); -+ final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(hopper.getOwner(false).getInventory(), -+ CraftItemStack.asCraftMirror(itemstack), destinationInventory, true); -+ final boolean result = event.callEvent(); -+ if (!event.calledGetItem && !event.calledSetItem) { -+ skipPushModeEventFire = true; -+ } -+ if (!result) { -+ cooldownHopper(hopper); -+ return null; -+ } -+ -+ if (event.calledSetItem) { -+ return CraftItemStack.asNMSCopy(event.getItem()); -+ } else { -+ return itemstack; -+ } -+ } -+ -+ @Nullable -+ private static ItemStack callPullMoveEvent(final Hopper hopper, final Container container, final ItemStack itemstack) { -+ final Inventory sourceInventory = getInventory(container); -+ final Inventory destination = getInventory(hopper); -+ -+ // Mirror is safe as no plugins ever use this item -+ final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(sourceInventory, CraftItemStack.asCraftMirror(itemstack), destination, false); -+ final boolean result = event.callEvent(); -+ if (!event.calledGetItem && !event.calledSetItem) { -+ skipPullModeEventFire = true; -+ } -+ if (!result) { -+ cooldownHopper(hopper); -+ return null; -+ } -+ -+ if (event.calledSetItem) { -+ return CraftItemStack.asNMSCopy(event.getItem()); -+ } else { -+ return itemstack; -+ } -+ } -+ -+ private static Inventory getInventory(final Container container) { -+ final Inventory sourceInventory; -+ if (container instanceof CompoundContainer compoundContainer) { -+ // Have to special-case large chests as they work oddly -+ sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest(compoundContainer); -+ } else if (container instanceof BlockEntity blockEntity) { -+ sourceInventory = blockEntity.getOwner(false).getInventory(); -+ } else if (container.getOwner() != null) { -+ sourceInventory = container.getOwner().getInventory(); -+ } else { -+ sourceInventory = new CraftInventory(container); -+ } -+ return sourceInventory; -+ } -+ -+ private static void cooldownHopper(final Hopper hopper) { -+ if (hopper instanceof HopperBlockEntity blockEntity && blockEntity.getLevel() != null) { -+ blockEntity.setCooldown(blockEntity.getLevel().spigotConfig.hopperTransfer); -+ } -+ } -+ -+ private static boolean allMatch(Container iinventory, Direction enumdirection, java.util.function.BiPredicate test) { -+ if (iinventory instanceof WorldlyContainer) { -+ for (int i : ((WorldlyContainer) iinventory).getSlotsForFace(enumdirection)) { -+ if (!test.test(iinventory.getItem(i), i)) { -+ return false; -+ } -+ } -+ } else { -+ int size = iinventory.getContainerSize(); -+ for (int i = 0; i < size; i++) { -+ if (!test.test(iinventory.getItem(i), i)) { -+ return false; -+ } -+ } -+ } -+ return true; -+ } -+ -+ private static boolean anyMatch(Container iinventory, Direction enumdirection, java.util.function.BiPredicate test) { -+ if (iinventory instanceof WorldlyContainer) { -+ for (int i : ((WorldlyContainer) iinventory).getSlotsForFace(enumdirection)) { -+ if (test.test(iinventory.getItem(i), i)) { -+ return true; -+ } -+ } -+ } else { -+ int size = iinventory.getContainerSize(); -+ for (int i = 0; i < size; i++) { -+ if (test.test(iinventory.getItem(i), i)) { -+ return true; -+ } -+ } -+ } -+ return true; -+ } -+ private static final java.util.function.BiPredicate STACK_SIZE_TEST = (itemstack, i) -> itemstack.getCount() >= itemstack.getMaxStackSize(); -+ private static final java.util.function.BiPredicate IS_EMPTY_TEST = (itemstack, i) -> itemstack.isEmpty(); -+ // Paper end - Perf: Optimize Hoppers -+ - private static boolean ejectItems(Level world, BlockPos pos, HopperBlockEntity blockEntity) { - Container iinventory = HopperBlockEntity.getAttachedContainer(world, pos, blockEntity); - -@@ -209,47 +443,50 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - if (HopperBlockEntity.isFullContainer(iinventory, enumdirection)) { - return false; - } else { -- for (int i = 0; i < blockEntity.getContainerSize(); ++i) { -- ItemStack itemstack = blockEntity.getItem(i); -- -- if (!itemstack.isEmpty()) { -- int j = itemstack.getCount(); -- // CraftBukkit start - Call event when pushing items into other inventories -- CraftItemStack oitemstack = CraftItemStack.asCraftMirror(blockEntity.removeItem(i, world.spigotConfig.hopperAmount)); // Spigot -- -- Inventory destinationInventory; -- // Have to special case large chests as they work oddly -- if (iinventory instanceof CompoundContainer) { -- destinationInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) iinventory); -- } else if (iinventory.getOwner() != null) { -- destinationInventory = iinventory.getOwner().getInventory(); -- } else { -- destinationInventory = new CraftInventory(iinventory); -- } -- -- InventoryMoveItemEvent event = new InventoryMoveItemEvent(blockEntity.getOwner().getInventory(), oitemstack, destinationInventory, true); -- world.getCraftServer().getPluginManager().callEvent(event); -- if (event.isCancelled()) { -- blockEntity.setItem(i, itemstack); -- blockEntity.setCooldown(world.spigotConfig.hopperTransfer); // Delay hopper checks // Spigot -- return false; -- } -- ItemStack itemstack1 = HopperBlockEntity.addItem(blockEntity, iinventory, CraftItemStack.asNMSCopy(event.getItem()), enumdirection); -- // CraftBukkit end -- -- if (itemstack1.isEmpty()) { -- iinventory.setChanged(); -- return true; -- } -- -- itemstack.setCount(j); -- if (j == 1) { -- blockEntity.setItem(i, itemstack); -- } -- } -- } -- -- return false; -+ // Paper start - Perf: Optimize Hoppers -+ return hopperPush(world, iinventory, enumdirection, blockEntity); -+ //for (int i = 0; i < blockEntity.getContainerSize(); ++i) { -+ // ItemStack itemstack = blockEntity.getItem(i); -+ -+ // if (!itemstack.isEmpty()) { -+ // int j = itemstack.getCount(); -+ // // CraftBukkit start - Call event when pushing items into other inventories -+ // CraftItemStack oitemstack = CraftItemStack.asCraftMirror(blockEntity.removeItem(i, world.spigotConfig.hopperAmount)); // Spigot -+ -+ // Inventory destinationInventory; -+ // // Have to special case large chests as they work oddly -+ // if (iinventory instanceof CompoundContainer) { -+ // destinationInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) iinventory); -+ // } else if (iinventory.getOwner() != null) { -+ // destinationInventory = iinventory.getOwner().getInventory(); -+ // } else { -+ // destinationInventory = new CraftInventory(iinventory); -+ // } -+ -+ // InventoryMoveItemEvent event = new InventoryMoveItemEvent(tileentityhopper.getOwner().getInventory(), oitemstack, destinationInventory, true); -+ // world.getCraftServer().getPluginManager().callEvent(event); -+ // if (event.isCancelled()) { -+ // blockEntity.setItem(i, itemstack); -+ // blockEntity.setCooldown(world.spigotConfig.hopperTransfer); // Delay hopper checks // Spigot -+ // return false; -+ // } -+ // ItemStack itemstack1 = HopperBlockEntity.addItem(blockEntity, iinventory, CraftItemStack.asNMSCopy(event.getItem()), enumdirection); -+ // // CraftBukkit end -+ -+ // if (itemstack1.isEmpty()) { -+ // iinventory.setChanged(); -+ // return true; -+ // } -+ -+ // itemstack.setCount(j); -+ // if (j == 1) { -+ // blockEntity.setItem(i, itemstack); -+ // } -+ // } -+ //} -+ -+ // return false; -+ // Paper end - Perf: Optimize Hoppers - } - } - } -@@ -300,7 +537,6 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - return false; - } - } -- - return true; - } - -@@ -342,48 +578,52 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - } - } - -+ @io.papermc.paper.annotation.DoNotUse // Paper - Optimize hoppers - private static boolean tryTakeInItemFromSlot(Hopper ihopper, Container iinventory, int i, Direction enumdirection, Level world) { // Spigot - ItemStack itemstack = iinventory.getItem(i); - - if (!itemstack.isEmpty() && HopperBlockEntity.canTakeItemFromContainer(ihopper, iinventory, itemstack, i, enumdirection)) { -- int j = itemstack.getCount(); -- // CraftBukkit start - Call event on collection of items from inventories into the hopper -- CraftItemStack oitemstack = CraftItemStack.asCraftMirror(iinventory.removeItem(i, world.spigotConfig.hopperAmount)); // Spigot -- -- Inventory sourceInventory; -- // Have to special case large chests as they work oddly -- if (iinventory instanceof CompoundContainer) { -- sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) iinventory); -- } else if (iinventory.getOwner() != null) { -- sourceInventory = iinventory.getOwner().getInventory(); -- } else { -- sourceInventory = new CraftInventory(iinventory); -- } -- -- InventoryMoveItemEvent event = new InventoryMoveItemEvent(sourceInventory, oitemstack, ihopper.getOwner().getInventory(), false); -- -- Bukkit.getServer().getPluginManager().callEvent(event); -- if (event.isCancelled()) { -- iinventory.setItem(i, itemstack); -- -- if (ihopper instanceof HopperBlockEntity) { -- ((HopperBlockEntity) ihopper).setCooldown(world.spigotConfig.hopperTransfer); // Spigot -- } -- -- return false; -- } -- ItemStack itemstack1 = HopperBlockEntity.addItem(iinventory, ihopper, CraftItemStack.asNMSCopy(event.getItem()), null); -- // CraftBukkit end -- -- if (itemstack1.isEmpty()) { -- iinventory.setChanged(); -- return true; -- } -- -- itemstack.setCount(j); -- if (j == 1) { -- iinventory.setItem(i, itemstack); -- } -+ // Paper start - Perf: Optimize Hoppers -+ return hopperPull(world, ihopper, iinventory, itemstack, i); -+ // int j = itemstack.getCount(); -+ // // CraftBukkit start - Call event on collection of items from inventories into the hopper -+ // CraftItemStack oitemstack = CraftItemStack.asCraftMirror(iinventory.removeItem(i, world.spigotConfig.hopperAmount)); // Spigot -+ -+ // Inventory sourceInventory; -+ // // Have to special case large chests as they work oddly -+ // if (iinventory instanceof CompoundContainer) { -+ // sourceInventory = new org.bukkit.craftbukkit.inventory.CraftInventoryDoubleChest((CompoundContainer) iinventory); -+ // } else if (iinventory.getOwner() != null) { -+ // sourceInventory = iinventory.getOwner().getInventory(); -+ // } else { -+ // sourceInventory = new CraftInventory(iinventory); -+ // } -+ -+ // InventoryMoveItemEvent event = new InventoryMoveItemEvent(sourceInventory, oitemstack, ihopper.getOwner().getInventory(), false); -+ -+ // Bukkit.getServer().getPluginManager().callEvent(event); -+ // if (event.isCancelled()) { -+ // iinventory.setItem(i, itemstack); -+ -+ // if (ihopper instanceof HopperBlockEntity) { -+ // ((HopperBlockEntity) ihopper).setCooldown(world.spigotConfig.hopperTransfer); // Spigot -+ // } -+ -+ // return false; -+ // } -+ // ItemStack itemstack1 = HopperBlockEntity.addItem(iinventory, ihopper, CraftItemStack.asNMSCopy(event.getItem()), null); -+ // // CraftBukkit end -+ -+ // if (itemstack1.isEmpty()) { -+ // iinventory.setChanged(); -+ // return true; -+ // } -+ -+ // itemstack.setCount(j); -+ // if (j == 1) { -+ // iinventory.setItem(i, itemstack); -+ // } -+ // Paper end - Perf: Optimize Hoppers - } - - return false; -@@ -392,12 +632,14 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - public static boolean addItem(Container inventory, ItemEntity itemEntity) { - boolean flag = false; - // CraftBukkit start -- InventoryPickupItemEvent event = new InventoryPickupItemEvent(inventory.getOwner().getInventory(), (org.bukkit.entity.Item) itemEntity.getBukkitEntity()); -+ if (InventoryPickupItemEvent.getHandlerList().getRegisteredListeners().length > 0) { // Paper - optimize hoppers -+ InventoryPickupItemEvent event = new InventoryPickupItemEvent(getInventory(inventory), (org.bukkit.entity.Item) itemEntity.getBukkitEntity()); // Paper - Perf: Optimize Hoppers; use getInventory() to avoid snapshot creation - itemEntity.level().getCraftServer().getPluginManager().callEvent(event); - if (event.isCancelled()) { - return false; - } - // CraftBukkit end -+ } // Paper - Perf: Optimize Hoppers - ItemStack itemstack = itemEntity.getItem().copy(); - ItemStack itemstack1 = HopperBlockEntity.addItem((Container) null, inventory, itemstack, (Direction) null); - -@@ -491,7 +733,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - stack = stack.split(to.getMaxStackSize()); - } - // Spigot end -+ ignoreTileUpdates = true; // Paper - Perf: Optimize Hoppers - to.setItem(slot, stack); -+ ignoreTileUpdates = false; // Paper - Perf: Optimize Hoppers - stack = leftover; // Paper - Make hoppers respect inventory max stack size - flag = true; - } else if (HopperBlockEntity.canMergeItems(itemstack1, stack)) { -@@ -571,14 +815,20 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - - @Nullable - public static Container getContainerAt(Level world, BlockPos pos) { -- return HopperBlockEntity.getContainerAt(world, pos, world.getBlockState(pos), (double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D); -+ return HopperBlockEntity.getContainerAt(world, pos, world.getBlockState(pos), (double) pos.getX() + 0.5D, (double) pos.getY() + 0.5D, (double) pos.getZ() + 0.5D, true); - } - - @Nullable - private static Container getContainerAt(Level world, BlockPos pos, BlockState state, double x, double y, double z) { -+ // Paper start - Perf: Optimize Hoppers -+ return HopperBlockEntity.getContainerAt(world, pos, state, x, y, z, false); -+ } -+ @Nullable -+ private static Container getContainerAt(Level world, BlockPos pos, BlockState state, double x, double y, double z, boolean optimizeEntities) { -+ // Paper end - Perf: Optimize Hoppers - Container iinventory = HopperBlockEntity.getBlockContainer(world, pos, state); - -- if (iinventory == null) { -+ if (iinventory == null && (!optimizeEntities || !world.paperConfig().hopper.ignoreOccludingBlocks || !state.getBukkitMaterial().isOccluding())) { // Paper - Perf: Optimize Hoppers - iinventory = HopperBlockEntity.getEntityContainer(world, x, y, z); - } - -@@ -613,13 +863,13 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - - @Nullable - private static Container getEntityContainer(Level world, double x, double y, double z) { -- List list = world.getEntities((Entity) null, new AABB(x - 0.5D, y - 0.5D, z - 0.5D, x + 0.5D, y + 0.5D, z + 0.5D), EntitySelector.CONTAINER_ENTITY_SELECTOR); -+ List list = world.getEntitiesOfClass((Class) Container.class, new AABB(x - 0.5D, y - 0.5D, z - 0.5D, x + 0.5D, y + 0.5D, z + 0.5D), EntitySelector.CONTAINER_ENTITY_SELECTOR); // Paper - Perf: Optimize hoppers - - return !list.isEmpty() ? (Container) list.get(world.random.nextInt(list.size())) : null; - } - - private static boolean canMergeItems(ItemStack first, ItemStack second) { -- return first.getCount() <= first.getMaxStackSize() && ItemStack.isSameItemSameComponents(first, second); -+ return first.getCount() < first.getMaxStackSize() && ItemStack.isSameItemSameComponents(first, second); // Paper - Perf: Optimize Hoppers; used to return true for full itemstacks?! - } - - @Override -diff --git a/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java -index 5b183f12717008dd6c9863938c17b2e668ebded4..17da915ffa638500a83b67db0940a7b9a9b333a1 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java -@@ -104,7 +104,7 @@ public abstract class RandomizableContainerBlockEntity extends BaseContainerBloc - - @Override - public ItemStack getItem(int slot) { -- this.unpackLootTable(null); -+ if (slot == 0) this.unpackLootTable(null); // Paper - Perf: Optimize Hoppers - return super.getItem(slot); - } - diff --git a/patches/server/1027-Actually-optimise-explosions.patch b/patches/server/1027-Actually-optimise-explosions.patch new file mode 100644 index 0000000000..a7db826672 --- /dev/null +++ b/patches/server/1027-Actually-optimise-explosions.patch @@ -0,0 +1,521 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Tue, 12 Sep 2023 06:50:16 -0700 +Subject: [PATCH] Actually optimise explosions + +The vast majority of blocks an explosion of power ~4 tries +to destroy are duplicates. The core of the block destroying +part of this patch is to cache the block state, resistance, and +whether it should explode - as those will not change. + +The other part of this patch is to optimise the visibility +percentage calculation. The new visibility calculation takes +advantage of the block caching already done by the explosion logic. +It continues to update the cache as the visibility calculation +uses many rays which can overlap significantly. + +Effectively, the patch uses a lot of caching to eliminate +redundant operations. + +Performance benchmarking explosions is challenging, as it varies +depending on the power, the number of nearby entities, and the +nearby terrain. This means that no benchmark can cover all the cases. +I decided to test a giant block of TNT, as that's where the optimisations +would be needed the most. + +I tested using a 50x10x50 block of TNT above ground +and determined the following: + +Vanilla time per explosion: 2.27ms +Lithium time per explosion: 1.07ms +This patch time per explosion: 0.45ms + +The results indicate that this logic is 5 times faster than Vanilla +and 2.3 times faster than Lithium. + +diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java +index 6bd6533e314b64d3512cbb46f5174a4956c323b2..46a14da698eff141b7379aa9c159e6a5c5f7d84c 100644 +--- a/src/main/java/net/minecraft/world/level/Explosion.java ++++ b/src/main/java/net/minecraft/world/level/Explosion.java +@@ -112,6 +112,271 @@ public class Explosion { + this.yield = this.blockInteraction == Explosion.BlockInteraction.DESTROY_WITH_DECAY ? 1.0F / this.radius : 1.0F; // CraftBukkit + } + ++ // Paper start - optimise collisions ++ private static final double[] CACHED_RAYS; ++ static { ++ final it.unimi.dsi.fastutil.doubles.DoubleArrayList rayCoords = new it.unimi.dsi.fastutil.doubles.DoubleArrayList(); ++ ++ for (int x = 0; x <= 15; ++x) { ++ for (int y = 0; y <= 15; ++y) { ++ for (int z = 0; z <= 15; ++z) { ++ if ((x == 0 || x == 15) || (y == 0 || y == 15) || (z == 0 || z == 15)) { ++ double xDir = (double)((float)x / 15.0F * 2.0F - 1.0F); ++ double yDir = (double)((float)y / 15.0F * 2.0F - 1.0F); ++ double zDir = (double)((float)z / 15.0F * 2.0F - 1.0F); ++ ++ double mag = Math.sqrt( ++ xDir * xDir + yDir * yDir + zDir * zDir ++ ); ++ ++ rayCoords.add((xDir / mag) * (double)0.3F); ++ rayCoords.add((yDir / mag) * (double)0.3F); ++ rayCoords.add((zDir / mag) * (double)0.3F); ++ } ++ } ++ } ++ } ++ ++ CACHED_RAYS = rayCoords.toDoubleArray(); ++ } ++ ++ private static final int CHUNK_CACHE_SHIFT = 2; ++ private static final int CHUNK_CACHE_MASK = (1 << CHUNK_CACHE_SHIFT) - 1; ++ private static final int CHUNK_CACHE_WIDTH = 1 << CHUNK_CACHE_SHIFT; ++ ++ private static final int BLOCK_EXPLOSION_CACHE_SHIFT = 3; ++ private static final int BLOCK_EXPLOSION_CACHE_MASK = (1 << BLOCK_EXPLOSION_CACHE_SHIFT) - 1; ++ private static final int BLOCK_EXPLOSION_CACHE_WIDTH = 1 << BLOCK_EXPLOSION_CACHE_SHIFT; ++ ++ // resistance = (res + 0.3F) * 0.3F; ++ // so for resistance = 0, we need res = -0.3F ++ private static final Float ZERO_RESISTANCE = Float.valueOf(-0.3f); ++ private it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap blockCache = null; ++ ++ public static final class ExplosionBlockCache { ++ ++ public final long key; ++ public final BlockPos immutablePos; ++ public final BlockState blockState; ++ public final FluidState fluidState; ++ public final float resistance; ++ public final boolean outOfWorld; ++ public Boolean shouldExplode; // null -> not called yet ++ public net.minecraft.world.phys.shapes.VoxelShape cachedCollisionShape; ++ ++ public ExplosionBlockCache(long key, BlockPos immutablePos, BlockState blockState, FluidState fluidState, float resistance, ++ boolean outOfWorld) { ++ this.key = key; ++ this.immutablePos = immutablePos; ++ this.blockState = blockState; ++ this.fluidState = fluidState; ++ this.resistance = resistance; ++ this.outOfWorld = outOfWorld; ++ } ++ } ++ ++ private long[] chunkPosCache = null; ++ private net.minecraft.world.level.chunk.LevelChunk[] chunkCache = null; ++ ++ private ExplosionBlockCache getOrCacheExplosionBlock(final int x, final int y, final int z, ++ final long key, final boolean calculateResistance) { ++ ExplosionBlockCache ret = this.blockCache.get(key); ++ if (ret != null) { ++ return ret; ++ } ++ ++ BlockPos pos = new BlockPos(x, y, z); ++ ++ if (!this.level.isInWorldBounds(pos)) { ++ ret = new ExplosionBlockCache(key, pos, null, null, 0.0f, true); ++ } else { ++ net.minecraft.world.level.chunk.LevelChunk chunk; ++ long chunkKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(x >> 4, z >> 4); ++ int chunkCacheKey = ((x >> 4) & CHUNK_CACHE_MASK) | (((z >> 4) << CHUNK_CACHE_SHIFT) & (CHUNK_CACHE_MASK << CHUNK_CACHE_SHIFT)); ++ if (this.chunkPosCache[chunkCacheKey] == chunkKey) { ++ chunk = this.chunkCache[chunkCacheKey]; ++ } else { ++ this.chunkPosCache[chunkCacheKey] = chunkKey; ++ this.chunkCache[chunkCacheKey] = chunk = this.level.getChunk(x >> 4, z >> 4); ++ } ++ ++ BlockState blockState = chunk.getBlockStateFinal(x, y, z); ++ FluidState fluidState = blockState.getFluidState(); ++ ++ Optional resistance = !calculateResistance ? Optional.empty() : this.damageCalculator.getBlockExplosionResistance((Explosion)(Object)this, this.level, pos, blockState, fluidState); ++ ++ ret = new ExplosionBlockCache( ++ key, pos, blockState, fluidState, ++ (resistance.orElse(ZERO_RESISTANCE).floatValue() + 0.3f) * 0.3f, ++ false ++ ); ++ } ++ ++ this.blockCache.put(key, ret); ++ ++ return ret; ++ } ++ ++ private boolean clipsAnything(final Vec3 from, final Vec3 to, ++ final io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext context, ++ final ExplosionBlockCache[] blockCache, ++ final BlockPos.MutableBlockPos currPos) { ++ // assume that context.delegated = false ++ final double adjX = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.x - to.x); ++ final double adjY = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.y - to.y); ++ final double adjZ = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.z - to.z); ++ ++ if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) { ++ return false; ++ } ++ ++ final double toXAdj = to.x - adjX; ++ final double toYAdj = to.y - adjY; ++ final double toZAdj = to.z - adjZ; ++ final double fromXAdj = from.x + adjX; ++ final double fromYAdj = from.y + adjY; ++ final double fromZAdj = from.z + adjZ; ++ ++ int currX = Mth.floor(fromXAdj); ++ int currY = Mth.floor(fromYAdj); ++ int currZ = Mth.floor(fromZAdj); ++ ++ final double diffX = toXAdj - fromXAdj; ++ final double diffY = toYAdj - fromYAdj; ++ final double diffZ = toZAdj - fromZAdj; ++ ++ final double dxDouble = Math.signum(diffX); ++ final double dyDouble = Math.signum(diffY); ++ final double dzDouble = Math.signum(diffZ); ++ ++ final int dx = (int)dxDouble; ++ final int dy = (int)dyDouble; ++ final int dz = (int)dzDouble; ++ ++ final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX; ++ final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY; ++ final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ; ++ ++ double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj)); ++ double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj)); ++ double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj)); ++ ++ for (;;) { ++ currPos.set(currX, currY, currZ); ++ ++ // ClipContext.Block.COLLIDER -> BlockBehaviour.BlockStateBase::getCollisionShape ++ // ClipContext.Fluid.NONE -> ignore fluids ++ ++ // read block from cache ++ final long key = BlockPos.asLong(currX, currY, currZ); ++ ++ final int cacheKey = ++ (currX & BLOCK_EXPLOSION_CACHE_MASK) | ++ (currY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) | ++ (currZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT); ++ ExplosionBlockCache cachedBlock = blockCache[cacheKey]; ++ if (cachedBlock == null || cachedBlock.key != key) { ++ blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(currX, currY, currZ, key, false); ++ } ++ ++ final BlockState blockState = cachedBlock.blockState; ++ if (blockState != null && !blockState.emptyCollisionShape()) { ++ net.minecraft.world.phys.shapes.VoxelShape collision = cachedBlock.cachedCollisionShape; ++ if (collision == null) { ++ collision = blockState.getConstantCollisionShape(); ++ if (collision == null) { ++ collision = blockState.getCollisionShape(this.level, currPos, context); ++ if (!context.isDelegated()) { ++ // if it was not delegated during this call, assume that for any future ones it will not be delegated ++ // again, and cache the result ++ cachedBlock.cachedCollisionShape = collision; ++ } ++ } else { ++ cachedBlock.cachedCollisionShape = collision; ++ } ++ } ++ ++ if (!collision.isEmpty() && collision.clip(from, to, currPos) != null) { ++ return true; ++ } ++ } ++ ++ if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) { ++ return false; ++ } ++ ++ // inc the smallest normalized coordinate ++ ++ if (normalizedCurrX < normalizedCurrY) { ++ if (normalizedCurrX < normalizedCurrZ) { ++ currX += dx; ++ normalizedCurrX += normalizedDiffX; ++ } else { ++ // x < y && x >= z <--> z < y && z <= x ++ currZ += dz; ++ normalizedCurrZ += normalizedDiffZ; ++ } ++ } else if (normalizedCurrY < normalizedCurrZ) { ++ // y <= x && y < z ++ currY += dy; ++ normalizedCurrY += normalizedDiffY; ++ } else { ++ // y <= x && z <= y <--> z <= y && z <= x ++ currZ += dz; ++ normalizedCurrZ += normalizedDiffZ; ++ } ++ } ++ } ++ ++ private float getSeenFraction(final Vec3 source, final Entity target, ++ final ExplosionBlockCache[] blockCache, ++ final BlockPos.MutableBlockPos blockPos) { ++ final AABB boundingBox = target.getBoundingBox(); ++ final double diffX = boundingBox.maxX - boundingBox.minX; ++ final double diffY = boundingBox.maxY - boundingBox.minY; ++ final double diffZ = boundingBox.maxZ - boundingBox.minZ; ++ ++ final double incX = 1.0 / (diffX * 2.0 + 1.0); ++ final double incY = 1.0 / (diffY * 2.0 + 1.0); ++ final double incZ = 1.0 / (diffZ * 2.0 + 1.0); ++ ++ if (incX < 0.0 || incY < 0.0 || incZ < 0.0) { ++ return 0.0f; ++ } ++ ++ final double offX = (1.0 - Math.floor(1.0 / incX) * incX) * 0.5 + boundingBox.minX; ++ final double offY = boundingBox.minY; ++ final double offZ = (1.0 - Math.floor(1.0 / incZ) * incZ) * 0.5 + boundingBox.minZ; ++ ++ final io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext context = new io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext(target); ++ ++ int totalRays = 0; ++ int missedRays = 0; ++ ++ for (double dx = 0.0; dx <= 1.0; dx += incX) { ++ final double fromX = Math.fma(dx, diffX, offX); ++ for (double dy = 0.0; dy <= 1.0; dy += incY) { ++ final double fromY = Math.fma(dy, diffY, offY); ++ for (double dz = 0.0; dz <= 1.0; dz += incZ) { ++ ++totalRays; ++ ++ final Vec3 from = new Vec3( ++ fromX, ++ fromY, ++ Math.fma(dz, diffZ, offZ) ++ ); ++ ++ if (!this.clipsAnything(from, source, context, blockCache, blockPos)) { ++ ++missedRays; ++ } ++ } ++ } ++ } ++ ++ return (float)missedRays / (float)totalRays; ++ } ++ // Paper end - optimise collisions ++ + private ExplosionDamageCalculator makeDamageCalculator(@Nullable Entity entity) { + return (ExplosionDamageCalculator) (entity == null ? Explosion.EXPLOSION_DAMAGE_CALCULATOR : new EntityBasedExplosionDamageCalculator(entity)); + } +@@ -172,40 +437,88 @@ public class Explosion { + int i; + int j; + +- for (int k = 0; k < 16; ++k) { +- for (i = 0; i < 16; ++i) { +- for (j = 0; j < 16; ++j) { +- if (k == 0 || k == 15 || i == 0 || i == 15 || j == 0 || j == 15) { +- double d0 = (double) ((float) k / 15.0F * 2.0F - 1.0F); +- double d1 = (double) ((float) i / 15.0F * 2.0F - 1.0F); +- double d2 = (double) ((float) j / 15.0F * 2.0F - 1.0F); +- double d3 = Math.sqrt(d0 * d0 + d1 * d1 + d2 * d2); +- +- d0 /= d3; +- d1 /= d3; +- d2 /= d3; ++ // Paper start - optimise explosions ++ this.blockCache = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(); ++ ++ this.chunkPosCache = new long[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH]; ++ java.util.Arrays.fill(this.chunkPosCache, ChunkPos.INVALID_CHUNK_POS); ++ ++ this.chunkCache = new net.minecraft.world.level.chunk.LevelChunk[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH]; ++ ++ final ExplosionBlockCache[] blockCache = new ExplosionBlockCache[BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH]; ++ // use initial cache value that is most likely to be used: the source position ++ final ExplosionBlockCache initialCache; ++ { ++ final int blockX = Mth.floor(this.x); ++ final int blockY = Mth.floor(this.y); ++ final int blockZ = Mth.floor(this.z); ++ ++ final long key = BlockPos.asLong(blockX, blockY, blockZ); ++ ++ initialCache = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true); ++ } ++ // only ~1/3rd of the loop iterations in vanilla will result in a ray, as it is iterating the perimeter of ++ // a 16x16x16 cube ++ // we can cache the rays and their normals as well, so that we eliminate the excess iterations / checks and ++ // calculations in one go ++ // additional aggressive caching of block retrieval is very significant, as at low power (i.e tnt) most ++ // block retrievals are not unique ++ for (int ray = 0, len = CACHED_RAYS.length; ray < len;) { ++ { ++ { ++ { ++ ExplosionBlockCache cachedBlock = initialCache; ++ ++ double d0 = CACHED_RAYS[ray]; ++ double d1 = CACHED_RAYS[ray + 1]; ++ double d2 = CACHED_RAYS[ray + 2]; ++ ray += 3; ++ // Paper end - optimise explosions + float f = this.radius * (0.7F + this.level.random.nextFloat() * 0.6F); + double d4 = this.x; + double d5 = this.y; + double d6 = this.z; + + for (float f1 = 0.3F; f > 0.0F; f -= 0.22500001F) { +- BlockPos blockposition = BlockPos.containing(d4, d5, d6); +- BlockState iblockdata = this.level.getBlockState(blockposition); +- if (!iblockdata.isDestroyable()) continue; // Paper - Protect Bedrock and End Portal/Frames from being destroyed +- FluidState fluid = iblockdata.getFluidState(); // Paper - Perf: Optimize call to getFluid for explosions ++ // Paper start - optimise explosions ++ final int blockX = Mth.floor(d4); ++ final int blockY = Mth.floor(d5); ++ final int blockZ = Mth.floor(d6); ++ ++ final long key = BlockPos.asLong(blockX, blockY, blockZ); ++ ++ if (cachedBlock.key != key) { ++ final int cacheKey = ++ (blockX & BLOCK_EXPLOSION_CACHE_MASK) | ++ (blockY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) | ++ (blockZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT); ++ cachedBlock = blockCache[cacheKey]; ++ if (cachedBlock == null || cachedBlock.key != key) { ++ blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true); ++ } ++ } + +- if (!this.level.isInWorldBounds(blockposition)) { ++ if (cachedBlock.outOfWorld) { + break; + } + +- Optional optional = this.damageCalculator.getBlockExplosionResistance(this, this.level, blockposition, iblockdata, fluid); ++ BlockPos blockposition = cachedBlock.immutablePos; ++ BlockState iblockdata = cachedBlock.blockState; ++ // Paper end - optimise explosions + +- if (optional.isPresent()) { +- f -= ((Float) optional.get() + 0.3F) * 0.3F; +- } ++ if (!iblockdata.isDestroyable()) continue; // Paper ++ // Paper - optimise explosions + +- if (f > 0.0F && this.damageCalculator.shouldBlockExplode(this, this.level, blockposition, iblockdata, f)) { ++ f -= cachedBlock.resistance; // Paper - optimise explosions ++ ++ if (f > 0.0F && cachedBlock.shouldExplode == null) { // Paper - optimise explosions ++ // Paper start - optimise explosions ++ // note: we expect shouldBlockExplode to be pure with respect to power, as Vanilla currently is. ++ // basically, it is unused, which allows us to cache the result ++ final boolean shouldExplode = this.damageCalculator.shouldBlockExplode(this, this.level, cachedBlock.immutablePos, cachedBlock.blockState, f); ++ cachedBlock.shouldExplode = shouldExplode ? Boolean.TRUE : Boolean.FALSE; ++ if (shouldExplode && (this.fire || !cachedBlock.blockState.isAir())) { ++ // Paper end - optimise explosions + set.add(blockposition); + // Paper start - prevent headless pistons from forming + if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && iblockdata.getBlock() == Blocks.MOVING_PISTON) { +@@ -216,11 +529,12 @@ public class Explosion { + } + } + // Paper end - prevent headless pistons from forming ++ } // Paper - optimise explosions + } + +- d4 += d0 * 0.30000001192092896D; +- d5 += d1 * 0.30000001192092896D; +- d6 += d2 * 0.30000001192092896D; ++ d4 += d0; // Paper - optimise explosions ++ d5 += d1; // Paper - optimise explosions ++ d6 += d2; // Paper - optimise explosions + } + } + } +@@ -240,6 +554,8 @@ public class Explosion { + Vec3 vec3d = new Vec3(this.x, this.y, this.z); + Iterator iterator = list.iterator(); + ++ final BlockPos.MutableBlockPos blockPos = new BlockPos.MutableBlockPos(); // Paper - optimise explosions ++ + while (iterator.hasNext()) { + Entity entity = (Entity) iterator.next(); + +@@ -275,11 +591,11 @@ public class Explosion { + for (EnderDragonPart entityComplexPart : ((EnderDragon) entity).subEntities) { + // Calculate damage separately for each EntityComplexPart + if (list.contains(entityComplexPart)) { +- entityComplexPart.hurt(this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity)); ++ entityComplexPart.hurt(this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entityComplexPart, getSeenFraction(vec3d, entityComplexPart, blockCache, blockPos))); // Paper - actually optimise explosions and use the right entity to calculate the damage + } + } + } else { +- entity.hurt(this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity)); ++ entity.hurt(this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity, getSeenFraction(vec3d, entity, blockCache, blockPos))); // Paper - actually optimise explosions + } + + if (entity.lastDamageCancelled) { // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Skip entity if damage event was cancelled +@@ -288,7 +604,7 @@ public class Explosion { + // CraftBukkit end + } + +- double d12 = (1.0D - d7) * this.getBlockDensity(vec3d, entity) * (double) this.damageCalculator.getKnockbackMultiplier(entity); // Paper - Optimize explosions ++ double d12 = (1.0D - d7) * this.getBlockDensity(vec3d, entity, blockCache, blockPos) * (double) this.damageCalculator.getKnockbackMultiplier(entity); // Paper - Optimize explosions + double d13; + + if (entity instanceof LivingEntity) { +@@ -337,6 +653,9 @@ public class Explosion { + } + } + ++ this.blockCache = null; // Paper - optimise explosions ++ this.chunkPosCache = null; // Paper - optimise explosions ++ this.chunkCache = null; // Paper - optimise explosions + } + + public void finalizeExplosion(boolean particles) { +@@ -550,14 +869,14 @@ public class Explosion { + private BlockInteraction() {} + } + // Paper start - Optimize explosions +- private float getBlockDensity(Vec3 vec3d, Entity entity) { ++ private float getBlockDensity(Vec3 vec3d, Entity entity, ExplosionBlockCache[] blockCache, BlockPos.MutableBlockPos blockPos) { // Paper - optimise explosions + if (!this.level.paperConfig().environment.optimizeExplosions) { +- return getSeenPercent(vec3d, entity); ++ return this.getSeenFraction(vec3d, entity, blockCache, blockPos); // Paper - optimise explosions + } + CacheKey key = new CacheKey(this, entity.getBoundingBox()); + Float blockDensity = this.level.explosionDensityCache.get(key); + if (blockDensity == null) { +- blockDensity = getSeenPercent(vec3d, entity); ++ blockDensity = this.getSeenFraction(vec3d, entity, blockCache, blockPos); // Paper - optimise explosions; + this.level.explosionDensityCache.put(key, blockDensity); + } + +diff --git a/src/main/java/net/minecraft/world/level/ExplosionDamageCalculator.java b/src/main/java/net/minecraft/world/level/ExplosionDamageCalculator.java +index 0ef9b402d129b072134688c06719a56328581158..1eb259b48bcab6172c15546744eea410c6a3e1fe 100644 +--- a/src/main/java/net/minecraft/world/level/ExplosionDamageCalculator.java ++++ b/src/main/java/net/minecraft/world/level/ExplosionDamageCalculator.java +@@ -26,11 +26,17 @@ public class ExplosionDamageCalculator { + return 1.0F; + } + ++ @io.papermc.paper.annotation.DoNotUse @Deprecated // Paper + public float getEntityDamageAmount(Explosion explosion, Entity entity) { ++ // Paper start - actually optimise explosions ++ return this.getEntityDamageAmount(explosion, entity, Explosion.getSeenPercent(explosion.center(), entity)); ++ } ++ public float getEntityDamageAmount(Explosion explosion, Entity entity, double seenPercent) { ++ // Paper end - actually optimise explosions + float f = explosion.radius() * 2.0F; + Vec3 vec3 = explosion.center(); + double d = Math.sqrt(entity.distanceToSqr(vec3)) / (double)f; +- double e = (1.0 - d) * (double)Explosion.getSeenPercent(vec3, entity); ++ double e = (1.0 - d) * seenPercent; // Paper - actually optimise explosions + return (float)((e * e + e) / 2.0 * 7.0 * (double)f + 1.0); + } + } diff --git a/patches/server/1027-Improve-performance-of-mass-crafts.patch b/patches/server/1027-Improve-performance-of-mass-crafts.patch deleted file mode 100644 index f9839d9135..0000000000 --- a/patches/server/1027-Improve-performance-of-mass-crafts.patch +++ /dev/null @@ -1,74 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Jake Potrebic -Date: Sun, 13 Aug 2023 15:41:52 -0700 -Subject: [PATCH] Improve performance of mass crafts - -When the server crafts all available items in CraftingMenu or InventoryMenu the game -checks either 4 or 9 times for each individual craft for a matching recipe for that container. -This check can be expensive if 64 total crafts are being performed with the recipe matching logic -being run 64 * 9 + 64 times. A breakdown of those times is below. This patch caches the last matching -recipe so that it is checked first and only if it doesn't match does the rest of the matching logic run. - -Shift-click crafts are processed one at a time, so shift clicking on an item in the result of a iron block craft -where all the 9 inputs are full stacks of iron will run 64 iron block crafts. For each of those crafts, the -'remaining' blocks are calculated. This is due to recipes that have leftover items like buckets. This is done -for each craft, and done once to get the full 9 leftover items which are usually air. Then 1 item is removed -from each of the 9 inputs and each time that happens, logic is triggered to update the result itemstack. So -for each craft, that logic is run 9 times (hence the 64 * 9). The + 64 is from the 64 checks for remaining items. - -After this patch, the full iteration over all recipes checking for a match should run once for a full craft to find the -initial recipe match. Then that recipe will be checked first for all future recipe match checks. - -diff --git a/src/main/java/net/minecraft/world/inventory/CraftingMenu.java b/src/main/java/net/minecraft/world/inventory/CraftingMenu.java -index 7a0c1a55a211035bbca7b97293e94b04ae308bae..59220eb9e74877e18b3e015dc0d25c892f45f746 100644 ---- a/src/main/java/net/minecraft/world/inventory/CraftingMenu.java -+++ b/src/main/java/net/minecraft/world/inventory/CraftingMenu.java -@@ -76,7 +76,8 @@ public class CraftingMenu extends RecipeBookMenu { - if (!world.isClientSide) { - ServerPlayer entityplayer = (ServerPlayer) player; - ItemStack itemstack = ItemStack.EMPTY; -- Optional> optional = world.getServer().getRecipeManager().getRecipeFor(RecipeType.CRAFTING, craftingInventory, world); -+ final RecipeHolder currentRecipe = craftingInventory.getCurrentRecipe(); // Paper - Perf: Improve mass crafting; check last recipe used first -+ Optional> optional = currentRecipe == null ? world.getServer().getRecipeManager().getRecipeFor(RecipeType.CRAFTING, craftingInventory, world) : world.getServer().getRecipeManager().getRecipeFor(RecipeType.CRAFTING, craftingInventory, world, currentRecipe.id()); // Paper - Perf: Improve mass crafting; check last recipe used first - - if (optional.isPresent()) { - RecipeHolder recipeholder = (RecipeHolder) optional.get(); -diff --git a/src/main/java/net/minecraft/world/inventory/ResultSlot.java b/src/main/java/net/minecraft/world/inventory/ResultSlot.java -index fef82418358ecf19d367dafbec159bd78ea97494..f17ff5988d826c8fad68f6bf7bac1d06edae01ae 100644 ---- a/src/main/java/net/minecraft/world/inventory/ResultSlot.java -+++ b/src/main/java/net/minecraft/world/inventory/ResultSlot.java -@@ -58,7 +58,7 @@ public class ResultSlot extends Slot { - @Override - public void onTake(Player player, ItemStack stack) { - this.checkTakeAchievements(stack); -- NonNullList nonNullList = player.level().getRecipeManager().getRemainingItemsFor(RecipeType.CRAFTING, this.craftSlots, player.level()); -+ NonNullList nonNullList = player.level().getRecipeManager().getRemainingItemsFor(RecipeType.CRAFTING, this.craftSlots, player.level(), this.craftSlots.getCurrentRecipe() != null ? this.craftSlots.getCurrentRecipe().id() : null); // Paper - Perf: Improve mass crafting; check last recipe used first - - for (int i = 0; i < nonNullList.size(); i++) { - ItemStack itemStack = this.craftSlots.getItem(i); -diff --git a/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java b/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java -index 0126b88f60904dfbf1e29eb3b89a985061d91f91..a31326e24cb68472c81cd781c5e3041772712862 100644 ---- a/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java -+++ b/src/main/java/net/minecraft/world/item/crafting/RecipeManager.java -@@ -116,6 +116,7 @@ public class RecipeManager extends SimpleJsonResourceReloadListener { - RecipeHolder recipeholder = this.byKeyTyped(type, id); - - if (recipeholder != null && recipeholder.value().matches(inventory, world)) { -+ inventory.setCurrentRecipe(recipeholder); // Paper - Perf: Improve mass crafting - return Optional.of(recipeholder); - } - } -@@ -147,7 +148,12 @@ public class RecipeManager extends SimpleJsonResourceReloadListener { - } - - public > NonNullList getRemainingItemsFor(RecipeType type, C inventory, Level world) { -- Optional> optional = this.getRecipeFor(type, inventory, world); -+ // Paper start - Perf: Improve mass crafting;; check last recipe used first -+ return this.getRemainingItemsFor(type, inventory, world, null); -+ } -+ public > NonNullList getRemainingItemsFor(RecipeType type, C inventory, Level world, @Nullable ResourceLocation firstToCheck) { -+ Optional> optional = firstToCheck == null ? this.getRecipeFor(type, inventory, world) : this.getRecipeFor(type, inventory, world, firstToCheck); -+ // Paper end - Perf: Improve mass crafting - - if (optional.isPresent()) { - return ((RecipeHolder) optional.get()).value().getRemainingItems(inventory); diff --git a/patches/server/1028-Actually-optimise-explosions.patch b/patches/server/1028-Actually-optimise-explosions.patch deleted file mode 100644 index a7db826672..0000000000 --- a/patches/server/1028-Actually-optimise-explosions.patch +++ /dev/null @@ -1,521 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Tue, 12 Sep 2023 06:50:16 -0700 -Subject: [PATCH] Actually optimise explosions - -The vast majority of blocks an explosion of power ~4 tries -to destroy are duplicates. The core of the block destroying -part of this patch is to cache the block state, resistance, and -whether it should explode - as those will not change. - -The other part of this patch is to optimise the visibility -percentage calculation. The new visibility calculation takes -advantage of the block caching already done by the explosion logic. -It continues to update the cache as the visibility calculation -uses many rays which can overlap significantly. - -Effectively, the patch uses a lot of caching to eliminate -redundant operations. - -Performance benchmarking explosions is challenging, as it varies -depending on the power, the number of nearby entities, and the -nearby terrain. This means that no benchmark can cover all the cases. -I decided to test a giant block of TNT, as that's where the optimisations -would be needed the most. - -I tested using a 50x10x50 block of TNT above ground -and determined the following: - -Vanilla time per explosion: 2.27ms -Lithium time per explosion: 1.07ms -This patch time per explosion: 0.45ms - -The results indicate that this logic is 5 times faster than Vanilla -and 2.3 times faster than Lithium. - -diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java -index 6bd6533e314b64d3512cbb46f5174a4956c323b2..46a14da698eff141b7379aa9c159e6a5c5f7d84c 100644 ---- a/src/main/java/net/minecraft/world/level/Explosion.java -+++ b/src/main/java/net/minecraft/world/level/Explosion.java -@@ -112,6 +112,271 @@ public class Explosion { - this.yield = this.blockInteraction == Explosion.BlockInteraction.DESTROY_WITH_DECAY ? 1.0F / this.radius : 1.0F; // CraftBukkit - } - -+ // Paper start - optimise collisions -+ private static final double[] CACHED_RAYS; -+ static { -+ final it.unimi.dsi.fastutil.doubles.DoubleArrayList rayCoords = new it.unimi.dsi.fastutil.doubles.DoubleArrayList(); -+ -+ for (int x = 0; x <= 15; ++x) { -+ for (int y = 0; y <= 15; ++y) { -+ for (int z = 0; z <= 15; ++z) { -+ if ((x == 0 || x == 15) || (y == 0 || y == 15) || (z == 0 || z == 15)) { -+ double xDir = (double)((float)x / 15.0F * 2.0F - 1.0F); -+ double yDir = (double)((float)y / 15.0F * 2.0F - 1.0F); -+ double zDir = (double)((float)z / 15.0F * 2.0F - 1.0F); -+ -+ double mag = Math.sqrt( -+ xDir * xDir + yDir * yDir + zDir * zDir -+ ); -+ -+ rayCoords.add((xDir / mag) * (double)0.3F); -+ rayCoords.add((yDir / mag) * (double)0.3F); -+ rayCoords.add((zDir / mag) * (double)0.3F); -+ } -+ } -+ } -+ } -+ -+ CACHED_RAYS = rayCoords.toDoubleArray(); -+ } -+ -+ private static final int CHUNK_CACHE_SHIFT = 2; -+ private static final int CHUNK_CACHE_MASK = (1 << CHUNK_CACHE_SHIFT) - 1; -+ private static final int CHUNK_CACHE_WIDTH = 1 << CHUNK_CACHE_SHIFT; -+ -+ private static final int BLOCK_EXPLOSION_CACHE_SHIFT = 3; -+ private static final int BLOCK_EXPLOSION_CACHE_MASK = (1 << BLOCK_EXPLOSION_CACHE_SHIFT) - 1; -+ private static final int BLOCK_EXPLOSION_CACHE_WIDTH = 1 << BLOCK_EXPLOSION_CACHE_SHIFT; -+ -+ // resistance = (res + 0.3F) * 0.3F; -+ // so for resistance = 0, we need res = -0.3F -+ private static final Float ZERO_RESISTANCE = Float.valueOf(-0.3f); -+ private it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap blockCache = null; -+ -+ public static final class ExplosionBlockCache { -+ -+ public final long key; -+ public final BlockPos immutablePos; -+ public final BlockState blockState; -+ public final FluidState fluidState; -+ public final float resistance; -+ public final boolean outOfWorld; -+ public Boolean shouldExplode; // null -> not called yet -+ public net.minecraft.world.phys.shapes.VoxelShape cachedCollisionShape; -+ -+ public ExplosionBlockCache(long key, BlockPos immutablePos, BlockState blockState, FluidState fluidState, float resistance, -+ boolean outOfWorld) { -+ this.key = key; -+ this.immutablePos = immutablePos; -+ this.blockState = blockState; -+ this.fluidState = fluidState; -+ this.resistance = resistance; -+ this.outOfWorld = outOfWorld; -+ } -+ } -+ -+ private long[] chunkPosCache = null; -+ private net.minecraft.world.level.chunk.LevelChunk[] chunkCache = null; -+ -+ private ExplosionBlockCache getOrCacheExplosionBlock(final int x, final int y, final int z, -+ final long key, final boolean calculateResistance) { -+ ExplosionBlockCache ret = this.blockCache.get(key); -+ if (ret != null) { -+ return ret; -+ } -+ -+ BlockPos pos = new BlockPos(x, y, z); -+ -+ if (!this.level.isInWorldBounds(pos)) { -+ ret = new ExplosionBlockCache(key, pos, null, null, 0.0f, true); -+ } else { -+ net.minecraft.world.level.chunk.LevelChunk chunk; -+ long chunkKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(x >> 4, z >> 4); -+ int chunkCacheKey = ((x >> 4) & CHUNK_CACHE_MASK) | (((z >> 4) << CHUNK_CACHE_SHIFT) & (CHUNK_CACHE_MASK << CHUNK_CACHE_SHIFT)); -+ if (this.chunkPosCache[chunkCacheKey] == chunkKey) { -+ chunk = this.chunkCache[chunkCacheKey]; -+ } else { -+ this.chunkPosCache[chunkCacheKey] = chunkKey; -+ this.chunkCache[chunkCacheKey] = chunk = this.level.getChunk(x >> 4, z >> 4); -+ } -+ -+ BlockState blockState = chunk.getBlockStateFinal(x, y, z); -+ FluidState fluidState = blockState.getFluidState(); -+ -+ Optional resistance = !calculateResistance ? Optional.empty() : this.damageCalculator.getBlockExplosionResistance((Explosion)(Object)this, this.level, pos, blockState, fluidState); -+ -+ ret = new ExplosionBlockCache( -+ key, pos, blockState, fluidState, -+ (resistance.orElse(ZERO_RESISTANCE).floatValue() + 0.3f) * 0.3f, -+ false -+ ); -+ } -+ -+ this.blockCache.put(key, ret); -+ -+ return ret; -+ } -+ -+ private boolean clipsAnything(final Vec3 from, final Vec3 to, -+ final io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext context, -+ final ExplosionBlockCache[] blockCache, -+ final BlockPos.MutableBlockPos currPos) { -+ // assume that context.delegated = false -+ final double adjX = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.x - to.x); -+ final double adjY = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.y - to.y); -+ final double adjZ = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.z - to.z); -+ -+ if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) { -+ return false; -+ } -+ -+ final double toXAdj = to.x - adjX; -+ final double toYAdj = to.y - adjY; -+ final double toZAdj = to.z - adjZ; -+ final double fromXAdj = from.x + adjX; -+ final double fromYAdj = from.y + adjY; -+ final double fromZAdj = from.z + adjZ; -+ -+ int currX = Mth.floor(fromXAdj); -+ int currY = Mth.floor(fromYAdj); -+ int currZ = Mth.floor(fromZAdj); -+ -+ final double diffX = toXAdj - fromXAdj; -+ final double diffY = toYAdj - fromYAdj; -+ final double diffZ = toZAdj - fromZAdj; -+ -+ final double dxDouble = Math.signum(diffX); -+ final double dyDouble = Math.signum(diffY); -+ final double dzDouble = Math.signum(diffZ); -+ -+ final int dx = (int)dxDouble; -+ final int dy = (int)dyDouble; -+ final int dz = (int)dzDouble; -+ -+ final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX; -+ final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY; -+ final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ; -+ -+ double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj)); -+ double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj)); -+ double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj)); -+ -+ for (;;) { -+ currPos.set(currX, currY, currZ); -+ -+ // ClipContext.Block.COLLIDER -> BlockBehaviour.BlockStateBase::getCollisionShape -+ // ClipContext.Fluid.NONE -> ignore fluids -+ -+ // read block from cache -+ final long key = BlockPos.asLong(currX, currY, currZ); -+ -+ final int cacheKey = -+ (currX & BLOCK_EXPLOSION_CACHE_MASK) | -+ (currY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) | -+ (currZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT); -+ ExplosionBlockCache cachedBlock = blockCache[cacheKey]; -+ if (cachedBlock == null || cachedBlock.key != key) { -+ blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(currX, currY, currZ, key, false); -+ } -+ -+ final BlockState blockState = cachedBlock.blockState; -+ if (blockState != null && !blockState.emptyCollisionShape()) { -+ net.minecraft.world.phys.shapes.VoxelShape collision = cachedBlock.cachedCollisionShape; -+ if (collision == null) { -+ collision = blockState.getConstantCollisionShape(); -+ if (collision == null) { -+ collision = blockState.getCollisionShape(this.level, currPos, context); -+ if (!context.isDelegated()) { -+ // if it was not delegated during this call, assume that for any future ones it will not be delegated -+ // again, and cache the result -+ cachedBlock.cachedCollisionShape = collision; -+ } -+ } else { -+ cachedBlock.cachedCollisionShape = collision; -+ } -+ } -+ -+ if (!collision.isEmpty() && collision.clip(from, to, currPos) != null) { -+ return true; -+ } -+ } -+ -+ if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) { -+ return false; -+ } -+ -+ // inc the smallest normalized coordinate -+ -+ if (normalizedCurrX < normalizedCurrY) { -+ if (normalizedCurrX < normalizedCurrZ) { -+ currX += dx; -+ normalizedCurrX += normalizedDiffX; -+ } else { -+ // x < y && x >= z <--> z < y && z <= x -+ currZ += dz; -+ normalizedCurrZ += normalizedDiffZ; -+ } -+ } else if (normalizedCurrY < normalizedCurrZ) { -+ // y <= x && y < z -+ currY += dy; -+ normalizedCurrY += normalizedDiffY; -+ } else { -+ // y <= x && z <= y <--> z <= y && z <= x -+ currZ += dz; -+ normalizedCurrZ += normalizedDiffZ; -+ } -+ } -+ } -+ -+ private float getSeenFraction(final Vec3 source, final Entity target, -+ final ExplosionBlockCache[] blockCache, -+ final BlockPos.MutableBlockPos blockPos) { -+ final AABB boundingBox = target.getBoundingBox(); -+ final double diffX = boundingBox.maxX - boundingBox.minX; -+ final double diffY = boundingBox.maxY - boundingBox.minY; -+ final double diffZ = boundingBox.maxZ - boundingBox.minZ; -+ -+ final double incX = 1.0 / (diffX * 2.0 + 1.0); -+ final double incY = 1.0 / (diffY * 2.0 + 1.0); -+ final double incZ = 1.0 / (diffZ * 2.0 + 1.0); -+ -+ if (incX < 0.0 || incY < 0.0 || incZ < 0.0) { -+ return 0.0f; -+ } -+ -+ final double offX = (1.0 - Math.floor(1.0 / incX) * incX) * 0.5 + boundingBox.minX; -+ final double offY = boundingBox.minY; -+ final double offZ = (1.0 - Math.floor(1.0 / incZ) * incZ) * 0.5 + boundingBox.minZ; -+ -+ final io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext context = new io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext(target); -+ -+ int totalRays = 0; -+ int missedRays = 0; -+ -+ for (double dx = 0.0; dx <= 1.0; dx += incX) { -+ final double fromX = Math.fma(dx, diffX, offX); -+ for (double dy = 0.0; dy <= 1.0; dy += incY) { -+ final double fromY = Math.fma(dy, diffY, offY); -+ for (double dz = 0.0; dz <= 1.0; dz += incZ) { -+ ++totalRays; -+ -+ final Vec3 from = new Vec3( -+ fromX, -+ fromY, -+ Math.fma(dz, diffZ, offZ) -+ ); -+ -+ if (!this.clipsAnything(from, source, context, blockCache, blockPos)) { -+ ++missedRays; -+ } -+ } -+ } -+ } -+ -+ return (float)missedRays / (float)totalRays; -+ } -+ // Paper end - optimise collisions -+ - private ExplosionDamageCalculator makeDamageCalculator(@Nullable Entity entity) { - return (ExplosionDamageCalculator) (entity == null ? Explosion.EXPLOSION_DAMAGE_CALCULATOR : new EntityBasedExplosionDamageCalculator(entity)); - } -@@ -172,40 +437,88 @@ public class Explosion { - int i; - int j; - -- for (int k = 0; k < 16; ++k) { -- for (i = 0; i < 16; ++i) { -- for (j = 0; j < 16; ++j) { -- if (k == 0 || k == 15 || i == 0 || i == 15 || j == 0 || j == 15) { -- double d0 = (double) ((float) k / 15.0F * 2.0F - 1.0F); -- double d1 = (double) ((float) i / 15.0F * 2.0F - 1.0F); -- double d2 = (double) ((float) j / 15.0F * 2.0F - 1.0F); -- double d3 = Math.sqrt(d0 * d0 + d1 * d1 + d2 * d2); -- -- d0 /= d3; -- d1 /= d3; -- d2 /= d3; -+ // Paper start - optimise explosions -+ this.blockCache = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(); -+ -+ this.chunkPosCache = new long[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH]; -+ java.util.Arrays.fill(this.chunkPosCache, ChunkPos.INVALID_CHUNK_POS); -+ -+ this.chunkCache = new net.minecraft.world.level.chunk.LevelChunk[CHUNK_CACHE_WIDTH * CHUNK_CACHE_WIDTH]; -+ -+ final ExplosionBlockCache[] blockCache = new ExplosionBlockCache[BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH * BLOCK_EXPLOSION_CACHE_WIDTH]; -+ // use initial cache value that is most likely to be used: the source position -+ final ExplosionBlockCache initialCache; -+ { -+ final int blockX = Mth.floor(this.x); -+ final int blockY = Mth.floor(this.y); -+ final int blockZ = Mth.floor(this.z); -+ -+ final long key = BlockPos.asLong(blockX, blockY, blockZ); -+ -+ initialCache = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true); -+ } -+ // only ~1/3rd of the loop iterations in vanilla will result in a ray, as it is iterating the perimeter of -+ // a 16x16x16 cube -+ // we can cache the rays and their normals as well, so that we eliminate the excess iterations / checks and -+ // calculations in one go -+ // additional aggressive caching of block retrieval is very significant, as at low power (i.e tnt) most -+ // block retrievals are not unique -+ for (int ray = 0, len = CACHED_RAYS.length; ray < len;) { -+ { -+ { -+ { -+ ExplosionBlockCache cachedBlock = initialCache; -+ -+ double d0 = CACHED_RAYS[ray]; -+ double d1 = CACHED_RAYS[ray + 1]; -+ double d2 = CACHED_RAYS[ray + 2]; -+ ray += 3; -+ // Paper end - optimise explosions - float f = this.radius * (0.7F + this.level.random.nextFloat() * 0.6F); - double d4 = this.x; - double d5 = this.y; - double d6 = this.z; - - for (float f1 = 0.3F; f > 0.0F; f -= 0.22500001F) { -- BlockPos blockposition = BlockPos.containing(d4, d5, d6); -- BlockState iblockdata = this.level.getBlockState(blockposition); -- if (!iblockdata.isDestroyable()) continue; // Paper - Protect Bedrock and End Portal/Frames from being destroyed -- FluidState fluid = iblockdata.getFluidState(); // Paper - Perf: Optimize call to getFluid for explosions -+ // Paper start - optimise explosions -+ final int blockX = Mth.floor(d4); -+ final int blockY = Mth.floor(d5); -+ final int blockZ = Mth.floor(d6); -+ -+ final long key = BlockPos.asLong(blockX, blockY, blockZ); -+ -+ if (cachedBlock.key != key) { -+ final int cacheKey = -+ (blockX & BLOCK_EXPLOSION_CACHE_MASK) | -+ (blockY & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT) | -+ (blockZ & BLOCK_EXPLOSION_CACHE_MASK) << (BLOCK_EXPLOSION_CACHE_SHIFT + BLOCK_EXPLOSION_CACHE_SHIFT); -+ cachedBlock = blockCache[cacheKey]; -+ if (cachedBlock == null || cachedBlock.key != key) { -+ blockCache[cacheKey] = cachedBlock = this.getOrCacheExplosionBlock(blockX, blockY, blockZ, key, true); -+ } -+ } - -- if (!this.level.isInWorldBounds(blockposition)) { -+ if (cachedBlock.outOfWorld) { - break; - } - -- Optional optional = this.damageCalculator.getBlockExplosionResistance(this, this.level, blockposition, iblockdata, fluid); -+ BlockPos blockposition = cachedBlock.immutablePos; -+ BlockState iblockdata = cachedBlock.blockState; -+ // Paper end - optimise explosions - -- if (optional.isPresent()) { -- f -= ((Float) optional.get() + 0.3F) * 0.3F; -- } -+ if (!iblockdata.isDestroyable()) continue; // Paper -+ // Paper - optimise explosions - -- if (f > 0.0F && this.damageCalculator.shouldBlockExplode(this, this.level, blockposition, iblockdata, f)) { -+ f -= cachedBlock.resistance; // Paper - optimise explosions -+ -+ if (f > 0.0F && cachedBlock.shouldExplode == null) { // Paper - optimise explosions -+ // Paper start - optimise explosions -+ // note: we expect shouldBlockExplode to be pure with respect to power, as Vanilla currently is. -+ // basically, it is unused, which allows us to cache the result -+ final boolean shouldExplode = this.damageCalculator.shouldBlockExplode(this, this.level, cachedBlock.immutablePos, cachedBlock.blockState, f); -+ cachedBlock.shouldExplode = shouldExplode ? Boolean.TRUE : Boolean.FALSE; -+ if (shouldExplode && (this.fire || !cachedBlock.blockState.isAir())) { -+ // Paper end - optimise explosions - set.add(blockposition); - // Paper start - prevent headless pistons from forming - if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons && iblockdata.getBlock() == Blocks.MOVING_PISTON) { -@@ -216,11 +529,12 @@ public class Explosion { - } - } - // Paper end - prevent headless pistons from forming -+ } // Paper - optimise explosions - } - -- d4 += d0 * 0.30000001192092896D; -- d5 += d1 * 0.30000001192092896D; -- d6 += d2 * 0.30000001192092896D; -+ d4 += d0; // Paper - optimise explosions -+ d5 += d1; // Paper - optimise explosions -+ d6 += d2; // Paper - optimise explosions - } - } - } -@@ -240,6 +554,8 @@ public class Explosion { - Vec3 vec3d = new Vec3(this.x, this.y, this.z); - Iterator iterator = list.iterator(); - -+ final BlockPos.MutableBlockPos blockPos = new BlockPos.MutableBlockPos(); // Paper - optimise explosions -+ - while (iterator.hasNext()) { - Entity entity = (Entity) iterator.next(); - -@@ -275,11 +591,11 @@ public class Explosion { - for (EnderDragonPart entityComplexPart : ((EnderDragon) entity).subEntities) { - // Calculate damage separately for each EntityComplexPart - if (list.contains(entityComplexPart)) { -- entityComplexPart.hurt(this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity)); -+ entityComplexPart.hurt(this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entityComplexPart, getSeenFraction(vec3d, entityComplexPart, blockCache, blockPos))); // Paper - actually optimise explosions and use the right entity to calculate the damage - } - } - } else { -- entity.hurt(this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity)); -+ entity.hurt(this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity, getSeenFraction(vec3d, entity, blockCache, blockPos))); // Paper - actually optimise explosions - } - - if (entity.lastDamageCancelled) { // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Skip entity if damage event was cancelled -@@ -288,7 +604,7 @@ public class Explosion { - // CraftBukkit end - } - -- double d12 = (1.0D - d7) * this.getBlockDensity(vec3d, entity) * (double) this.damageCalculator.getKnockbackMultiplier(entity); // Paper - Optimize explosions -+ double d12 = (1.0D - d7) * this.getBlockDensity(vec3d, entity, blockCache, blockPos) * (double) this.damageCalculator.getKnockbackMultiplier(entity); // Paper - Optimize explosions - double d13; - - if (entity instanceof LivingEntity) { -@@ -337,6 +653,9 @@ public class Explosion { - } - } - -+ this.blockCache = null; // Paper - optimise explosions -+ this.chunkPosCache = null; // Paper - optimise explosions -+ this.chunkCache = null; // Paper - optimise explosions - } - - public void finalizeExplosion(boolean particles) { -@@ -550,14 +869,14 @@ public class Explosion { - private BlockInteraction() {} - } - // Paper start - Optimize explosions -- private float getBlockDensity(Vec3 vec3d, Entity entity) { -+ private float getBlockDensity(Vec3 vec3d, Entity entity, ExplosionBlockCache[] blockCache, BlockPos.MutableBlockPos blockPos) { // Paper - optimise explosions - if (!this.level.paperConfig().environment.optimizeExplosions) { -- return getSeenPercent(vec3d, entity); -+ return this.getSeenFraction(vec3d, entity, blockCache, blockPos); // Paper - optimise explosions - } - CacheKey key = new CacheKey(this, entity.getBoundingBox()); - Float blockDensity = this.level.explosionDensityCache.get(key); - if (blockDensity == null) { -- blockDensity = getSeenPercent(vec3d, entity); -+ blockDensity = this.getSeenFraction(vec3d, entity, blockCache, blockPos); // Paper - optimise explosions; - this.level.explosionDensityCache.put(key, blockDensity); - } - -diff --git a/src/main/java/net/minecraft/world/level/ExplosionDamageCalculator.java b/src/main/java/net/minecraft/world/level/ExplosionDamageCalculator.java -index 0ef9b402d129b072134688c06719a56328581158..1eb259b48bcab6172c15546744eea410c6a3e1fe 100644 ---- a/src/main/java/net/minecraft/world/level/ExplosionDamageCalculator.java -+++ b/src/main/java/net/minecraft/world/level/ExplosionDamageCalculator.java -@@ -26,11 +26,17 @@ public class ExplosionDamageCalculator { - return 1.0F; - } - -+ @io.papermc.paper.annotation.DoNotUse @Deprecated // Paper - public float getEntityDamageAmount(Explosion explosion, Entity entity) { -+ // Paper start - actually optimise explosions -+ return this.getEntityDamageAmount(explosion, entity, Explosion.getSeenPercent(explosion.center(), entity)); -+ } -+ public float getEntityDamageAmount(Explosion explosion, Entity entity, double seenPercent) { -+ // Paper end - actually optimise explosions - float f = explosion.radius() * 2.0F; - Vec3 vec3 = explosion.center(); - double d = Math.sqrt(entity.distanceToSqr(vec3)) / (double)f; -- double e = (1.0 - d) * (double)Explosion.getSeenPercent(vec3, entity); -+ double e = (1.0 - d) * seenPercent; // Paper - actually optimise explosions - return (float)((e * e + e) / 2.0 * 7.0 * (double)f + 1.0); - } - } diff --git a/patches/server/1028-Optimise-chunk-tick-iteration.patch b/patches/server/1028-Optimise-chunk-tick-iteration.patch new file mode 100644 index 0000000000..1452dcd9c5 --- /dev/null +++ b/patches/server/1028-Optimise-chunk-tick-iteration.patch @@ -0,0 +1,380 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sat, 23 Sep 2023 21:36:36 -0700 +Subject: [PATCH] Optimise chunk tick iteration + +When per-player mob spawning is enabled we do not need to randomly +shuffle the chunk list. Additionally, we can use the NearbyPlayers +class to quickly retrieve nearby players instead of possible +searching all players on the server. + +diff --git a/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java b/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java +index c3ce8a42dddd76b7189ad5685b23f9d9f8ccadb3..f164256d59b761264876ca0c85f812d101bfd5de 100644 +--- a/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java ++++ b/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java +@@ -17,7 +17,8 @@ public final class NearbyPlayers { + GENERAL_SMALL, + GENERAL_REALLY_SMALL, + TICK_VIEW_DISTANCE, +- VIEW_DISTANCE; ++ VIEW_DISTANCE, // Paper - optimise chunk iteration ++ SPAWN_RANGE, // Paper - optimise chunk iteration + } + + private static final NearbyMapType[] MOB_TYPES = NearbyMapType.values(); +@@ -26,10 +27,12 @@ public final class NearbyPlayers { + private static final int GENERAL_AREA_VIEW_DISTANCE = 33; + private static final int GENERAL_SMALL_VIEW_DISTANCE = 10; + private static final int GENERAL_REALLY_SMALL_VIEW_DISTANCE = 3; ++ private static final int SPAWN_RANGE_VIEW_DISTANCE = net.minecraft.server.level.DistanceManager.MOB_SPAWN_RANGE; // Paper - optimise chunk iteration + + public static final int GENERAL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_AREA_VIEW_DISTANCE << 4); + public static final int GENERAL_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_SMALL_VIEW_DISTANCE << 4); + public static final int GENERAL_REALLY_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_REALLY_SMALL_VIEW_DISTANCE << 4); ++ public static final int SPAWN_RANGE_VIEW_DISTANCE_BLOCKS = (SPAWN_RANGE_VIEW_DISTANCE << 4); // Paper - optimise chunk iteration + + private final ServerLevel world; + private final Reference2ReferenceOpenHashMap players = new Reference2ReferenceOpenHashMap<>(); +@@ -80,6 +83,7 @@ public final class NearbyPlayers { + players[NearbyMapType.GENERAL_REALLY_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_REALLY_SMALL_VIEW_DISTANCE); + players[NearbyMapType.TICK_VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getTickViewDistance(player)); + players[NearbyMapType.VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getLoadViewDistance(player)); ++ players[NearbyMapType.SPAWN_RANGE.ordinal()].update(chunk.x, chunk.z, SPAWN_RANGE_VIEW_DISTANCE); // Paper - optimise chunk iteration + } + + public TrackedChunk getChunk(final ChunkPos pos) { +diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java +index 13d15a135dd0373bef4a5ac9ffb56dbbf53353a0..472b9494f8a34a8ba90d6a2936b0db7530a229ad 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java ++++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java +@@ -77,11 +77,19 @@ public class ChunkHolder { + + // Paper start + public void onChunkAdd() { +- ++ // Paper start - optimise chunk tick iteration ++ if (this.needsBroadcastChanges()) { ++ this.chunkMap.needsChangeBroadcasting.add(this); ++ } ++ // Paper end - optimise chunk tick iteration + } + + public void onChunkRemove() { +- ++ // Paper start - optimise chunk tick iteration ++ if (this.needsBroadcastChanges()) { ++ this.chunkMap.needsChangeBroadcasting.remove(this); ++ } ++ // Paper end - optimise chunk tick iteration + } + // Paper end + +@@ -216,7 +224,7 @@ public class ChunkHolder { + + if (i < 0 || i >= this.changedBlocksPerSection.length) return; // CraftBukkit - SPIGOT-6086, SPIGOT-6296 + if (this.changedBlocksPerSection[i] == null) { +- this.hasChangedSections = true; ++ this.hasChangedSections = true; this.addToBroadcastMap(); // Paper - optimise chunk tick iteration + this.changedBlocksPerSection[i] = new ShortOpenHashSet(); + } + +@@ -236,6 +244,7 @@ public class ChunkHolder { + int k = this.lightEngine.getMaxLightSection(); + + if (y >= j && y <= k) { ++ this.addToBroadcastMap(); // Paper - optimise chunk tick iteration + int l = y - j; + + if (lightType == LightLayer.SKY) { +@@ -254,9 +263,19 @@ public class ChunkHolder { + this.broadcast(this.getPlayers(onChunkViewEdge), packet); // Paper - rewrite chunk system + } + // Paper end - starlight ++ // Paper start - optimise chunk tick iteration ++ public final boolean needsBroadcastChanges() { ++ return this.hasChangedSections || !this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty(); ++ } ++ ++ private void addToBroadcastMap() { ++ io.papermc.paper.util.TickThread.ensureTickThread(this.chunkMap.level, this.pos, "Asynchronous ChunkHolder update is not allowed"); ++ this.chunkMap.needsChangeBroadcasting.add(this); ++ } ++ // Paper end - optimise chunk tick iteration + + public void broadcastChanges(LevelChunk chunk) { +- if (this.hasChangedSections || !this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty()) { ++ if (this.needsBroadcastChanges()) { // Paper - optimise chunk tick iteration; moved into above, other logic needs to call + Level world = chunk.getLevel(); + List list; + +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index 48f7997e8a20f5a5a77516cbde990d0aacc2078a..dbe9df1e1973db133f7c8516256697ef7c968137 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -194,6 +194,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.playerEntityTrackerTrackMaps[i].remove(player); + } + // Paper end - use distance map to optimise tracker ++ this.playerMobSpawnMap.remove(player); // Paper - optimise chunk tick iteration + } + + void updateMaps(ServerPlayer player) { +@@ -243,6 +244,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + public final io.papermc.paper.util.player.NearbyPlayers nearbyPlayers; + // Paper end ++ // Paper start - optimise chunk tick iteration ++ public final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet needsChangeBroadcasting = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); ++ public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets); ++ // Paper end - optimise chunk tick iteration + + public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory, int viewDistance, boolean dsync) { + super(new RegionStorageInfo(session.getLevelId(), world.dimension(), "chunk"), session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync); +@@ -411,7 +416,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + // Paper end - Optional per player mob spawns + +- private static double euclideanDistanceSquared(ChunkPos pos, Entity entity) { ++ public static double euclideanDistanceSquared(ChunkPos pos, Entity entity) { // Paper - optimise chunk iteration; public + double d0 = (double) SectionPos.sectionToBlockCoord(pos.x, 8); + double d1 = (double) SectionPos.sectionToBlockCoord(pos.z, 8); + double d2 = d0 - entity.getX(); +diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java +index ed5154e41ca858f4d6b4d1c276c66831c038d2a6..cdb3c2cde5d9133ef60cf96d91762e6a7c8aeb4a 100644 +--- a/src/main/java/net/minecraft/server/level/DistanceManager.java ++++ b/src/main/java/net/minecraft/server/level/DistanceManager.java +@@ -49,7 +49,7 @@ public abstract class DistanceManager { + private static final int INITIAL_TICKET_LIST_CAPACITY = 4; + final Long2ObjectMap> playersPerChunk = new Long2ObjectOpenHashMap(); + // Paper - rewrite chunk system +- private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8); ++ public static final int MOB_SPAWN_RANGE = 8; //private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8); // Paper - optimise chunk tick iteration + // Paper - rewrite chunk system + private final ChunkMap chunkMap; // Paper + +@@ -135,7 +135,7 @@ public abstract class DistanceManager { + long i = chunkcoordintpair.toLong(); + + // Paper - no longer used +- this.naturalSpawnChunkCounter.update(i, 0, true); ++ //this.naturalSpawnChunkCounter.update(i, 0, true); // Paper - optimise chunk tick iteration + //this.playerTicketManager.update(i, 0, true); // Paper - no longer used + //this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used + } +@@ -149,7 +149,7 @@ public abstract class DistanceManager { + if (objectset != null) objectset.remove(player); // Paper - some state corruption happens here, don't crash, clean up gracefully + if (objectset == null || objectset.isEmpty()) { // Paper + this.playersPerChunk.remove(i); +- this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false); ++ //this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false); // Paper - optimise chunk tick iteration + //this.playerTicketManager.update(i, Integer.MAX_VALUE, false); // Paper - no longer used + //this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used + } +@@ -191,13 +191,11 @@ public abstract class DistanceManager { + } + + public int getNaturalSpawnChunkCount() { +- this.naturalSpawnChunkCounter.runAllUpdates(); +- return this.naturalSpawnChunkCounter.chunks.size(); ++ return this.chunkMap.playerMobSpawnMap.size(); // Paper - optimise chunk tick iteration + } + + public boolean hasPlayersNearby(long chunkPos) { +- this.naturalSpawnChunkCounter.runAllUpdates(); +- return this.naturalSpawnChunkCounter.chunks.containsKey(chunkPos); ++ return this.chunkMap.playerMobSpawnMap.getObjectsInRange(chunkPos) != null; // Paper - optimise chunk tick iteration + } + + public String getDebugStatus() { +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index 18640c8681f6a3b2276123d19e3e8f0a8c630b41..b99f50604bafecbc68835974c9ed0caa91911a40 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -506,18 +506,10 @@ public class ServerChunkCache extends ChunkSource { + + gameprofilerfiller.push("pollingChunks"); + gameprofilerfiller.push("filteringLoadedChunks"); +- List list = Lists.newArrayListWithCapacity(this.chunkMap.size()); +- Iterator iterator = this.chunkMap.getChunks().iterator(); ++ // Paper - optimise chunk tick iteration + if (this.level.getServer().tickRateManager().runsNormally()) this.level.timings.chunkTicks.startTiming(); // Paper + +- while (iterator.hasNext()) { +- ChunkHolder playerchunk = (ChunkHolder) iterator.next(); +- LevelChunk chunk = playerchunk.getTickingChunk(); +- +- if (chunk != null) { +- list.add(new ServerChunkCache.ChunkAndHolder(chunk, playerchunk)); +- } +- } ++ // Paper - optimise chunk tick iteration + + if (this.level.tickRateManager().runsNormally()) { + gameprofilerfiller.popPush("naturalSpawnCount"); +@@ -552,38 +544,109 @@ public class ServerChunkCache extends ChunkSource { + gameprofilerfiller.popPush("spawnAndTick"); + boolean flag = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit + +- Util.shuffle(list, this.level.random); +- // Paper start - PlayerNaturallySpawnCreaturesEvent +- int chunkRange = level.spigotConfig.mobSpawnRange; +- chunkRange = (chunkRange > level.spigotConfig.viewDistance) ? (byte) level.spigotConfig.viewDistance : chunkRange; +- chunkRange = Math.min(chunkRange, 8); +- for (ServerPlayer entityPlayer : this.level.players()) { +- entityPlayer.playerNaturallySpawnedEvent = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(entityPlayer.getBukkitEntity(), (byte) chunkRange); +- entityPlayer.playerNaturallySpawnedEvent.callEvent(); ++ // Paper start - optimise chunk tick iteration ++ ChunkMap playerChunkMap = this.chunkMap; ++ for (ServerPlayer player : this.level.players) { ++ if (!player.affectsSpawning || player.isSpectator()) { ++ playerChunkMap.playerMobSpawnMap.remove(player); ++ player.playerNaturallySpawnedEvent = null; ++ player.lastEntitySpawnRadiusSquared = -1.0; ++ continue; ++ } ++ ++ int viewDistance = io.papermc.paper.chunk.system.ChunkSystem.getTickViewDistance(player); ++ ++ // copied and modified from isOutisdeRange ++ int chunkRange = (int)level.spigotConfig.mobSpawnRange; ++ chunkRange = (chunkRange > viewDistance) ? viewDistance : chunkRange; ++ chunkRange = (chunkRange > DistanceManager.MOB_SPAWN_RANGE) ? DistanceManager.MOB_SPAWN_RANGE : chunkRange; ++ ++ com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent event = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(player.getBukkitEntity(), (byte)chunkRange); ++ event.callEvent(); ++ if (event.isCancelled() || event.getSpawnRadius() < 0) { ++ playerChunkMap.playerMobSpawnMap.remove(player); ++ player.playerNaturallySpawnedEvent = null; ++ player.lastEntitySpawnRadiusSquared = -1.0; ++ continue; ++ } ++ ++ int range = Math.min(event.getSpawnRadius(), DistanceManager.MOB_SPAWN_RANGE); // limit to max spawn range ++ int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkCoordinate(player.getX()); ++ int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkCoordinate(player.getZ()); ++ ++ playerChunkMap.playerMobSpawnMap.addOrUpdate(player, chunkX, chunkZ, range); ++ player.lastEntitySpawnRadiusSquared = (double)((range << 4) * (range << 4)); // used in anyPlayerCloseEnoughForSpawning ++ player.playerNaturallySpawnedEvent = event; + } +- // Paper end - PlayerNaturallySpawnCreaturesEvent ++ // Paper end - optimise chunk tick iteration + int l = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING); + boolean flag1 = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getLevelData().getGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit +- Iterator iterator1 = list.iterator(); ++ // Paper - optimise chunk tick iteration + + int chunksTicked = 0; // Paper +- while (iterator1.hasNext()) { +- ServerChunkCache.ChunkAndHolder chunkproviderserver_a = (ServerChunkCache.ChunkAndHolder) iterator1.next(); +- LevelChunk chunk1 = chunkproviderserver_a.chunk; ++ // Paper start - optimise chunk tick iteration ++ io.papermc.paper.util.player.NearbyPlayers nearbyPlayers = this.chunkMap.getNearbyPlayers(); // Paper - optimise chunk tick iteration ++ Iterator chunkIterator; ++ if (this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { ++ chunkIterator = this.tickingChunks.iterator(); ++ } else { ++ chunkIterator = this.tickingChunks.unsafeIterator(); ++ List shuffled = Lists.newArrayListWithCapacity(this.tickingChunks.size()); ++ while (chunkIterator.hasNext()) { ++ shuffled.add(chunkIterator.next()); ++ } ++ Util.shuffle(shuffled, this.level.random); ++ chunkIterator = shuffled.iterator(); ++ } ++ try { ++ // Paper end - optimise chunk tick iteration ++ while (chunkIterator.hasNext()) { ++ LevelChunk chunk1 = chunkIterator.next(); ++ // Paper end - optimise chunk tick iteration + ChunkPos chunkcoordintpair = chunk1.getPos(); + +- if (this.level.isNaturalSpawningAllowed(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(chunkcoordintpair)) { ++ // Paper start - optimise chunk tick iteration ++ com.destroystokyo.paper.util.maplist.ReferenceList playersNearby ++ = nearbyPlayers.getPlayers(chunkcoordintpair, io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.SPAWN_RANGE); ++ if (playersNearby == null) { ++ continue; ++ } ++ Object[] rawData = playersNearby.getRawData(); ++ boolean spawn = false; ++ boolean tick = false; ++ for (int itr = 0, len = playersNearby.size(); itr < len; ++itr) { ++ ServerPlayer player = (ServerPlayer)rawData[itr]; ++ if (player.isSpectator()) { ++ continue; ++ } ++ ++ double distance = ChunkMap.euclideanDistanceSquared(chunkcoordintpair, player); ++ spawn |= player.lastEntitySpawnRadiusSquared >= distance; ++ tick |= ((double)io.papermc.paper.util.player.NearbyPlayers.SPAWN_RANGE_VIEW_DISTANCE_BLOCKS) * ((double)io.papermc.paper.util.player.NearbyPlayers.SPAWN_RANGE_VIEW_DISTANCE_BLOCKS) >= distance; ++ if (spawn & tick) { ++ break; ++ } ++ } ++ if (tick && chunk1.chunkStatus.isOrAfter(net.minecraft.server.level.FullChunkStatus.ENTITY_TICKING)) { ++ // Paper end - optimise chunk tick iteration + chunk1.incrementInhabitedTime(j); +- if (flag && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(chunkcoordintpair, true)) { // Spigot ++ if (spawn && flag && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair)) { // Spigot // Paper - optimise chunk tick iteration + NaturalSpawner.spawnForChunk(this.level, chunk1, spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag1); + } + +- if (this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { ++ if (true || this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { // Paper - optimise chunk tick iteration + this.level.tickChunk(chunk1, l); + if ((chunksTicked++ & 1) == 0) net.minecraft.server.MinecraftServer.getServer().executeMidTickTasks(); // Paper + } + } + } ++ // Paper start - optimise chunk tick iteration ++ } finally { ++ if (chunkIterator instanceof io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.Iterator safeIterator) { ++ safeIterator.finishedIterating(); ++ } ++ } ++ // Paper end - optimise chunk tick iteration + this.level.timings.chunkTicks.stopTiming(); // Paper + + gameprofilerfiller.popPush("customSpawners"); +@@ -595,11 +658,23 @@ public class ServerChunkCache extends ChunkSource { + } + + gameprofilerfiller.popPush("broadcast"); +- list.forEach((chunkproviderserver_a1) -> { ++ // Paper - optimise chunk tick iteration + this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timing +- chunkproviderserver_a1.holder.broadcastChanges(chunkproviderserver_a1.chunk); ++ // Paper start - optimise chunk tick iteration ++ if (!this.chunkMap.needsChangeBroadcasting.isEmpty()) { ++ it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet copy = this.chunkMap.needsChangeBroadcasting.clone(); ++ this.chunkMap.needsChangeBroadcasting.clear(); ++ for (ChunkHolder holder : copy) { ++ holder.broadcastChanges(holder.getFullChunkNowUnchecked()); // LevelChunks are NEVER unloaded ++ if (holder.needsBroadcastChanges()) { ++ // I DON'T want to KNOW what DUMB plugins might be doing. ++ this.chunkMap.needsChangeBroadcasting.add(holder); ++ } ++ } ++ } ++ // Paper end - optimise chunk tick iteration + this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timing +- }); ++ // Paper - optimise chunk tick iteration + gameprofilerfiller.pop(); + gameprofilerfiller.pop(); + } +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index ed42a78aebeda86b77f27cba4fcefbc6c17f86fc..49d612c1b84fe4f2335c9cf91cce5f60b6779ff6 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -341,6 +341,9 @@ public class ServerPlayer extends Player { + }); + } + // Paper end - replace player chunk loader ++ // Paper start - optimise chunk tick iteration ++ public double lastEntitySpawnRadiusSquared = -1.0; ++ // Paper end - optimise chunk tick iteration + + public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) { + super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile); diff --git a/patches/server/1029-Lag-compensation-ticks.patch b/patches/server/1029-Lag-compensation-ticks.patch new file mode 100644 index 0000000000..07b5e78339 --- /dev/null +++ b/patches/server/1029-Lag-compensation-ticks.patch @@ -0,0 +1,129 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sat, 23 Sep 2023 22:05:35 -0700 +Subject: [PATCH] Lag compensation ticks + +Areas affected by lag comepnsation: + - Block breaking and destroying + - Eating food items + +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index b12f69931e5b247c42bb4e56bcf1ef24deddfe2e..40551f2bb4e5511d1feefe87244905f1ee84b511 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -318,6 +318,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { + AtomicReference atomicreference = new AtomicReference(); +@@ -1757,6 +1758,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper - BlockPhysicsEvent + worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent + net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers ++ worldserver.updateLagCompensationTick(); // Paper - lag compensation + + this.profiler.push(() -> { + String s = String.valueOf(worldserver); +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 2bfebbe0bc5a7f72c9ebe60fded260b3f94ac749..89ecaaa774e59ad01eec0ce2fe546026a8b47ae8 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -570,6 +570,17 @@ public class ServerLevel extends Level implements WorldGenLevel { + return player != null && player.level() == this ? player : null; + } + // Paper end - optimise getPlayerByUUID ++ // Paper start - lag compensation ++ private long lagCompensationTick = net.minecraft.server.MinecraftServer.SERVER_INIT; ++ ++ public long getLagCompensationTick() { ++ return this.lagCompensationTick; ++ } ++ ++ public void updateLagCompensationTick() { ++ this.lagCompensationTick = (System.nanoTime() - net.minecraft.server.MinecraftServer.SERVER_INIT) / (java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(50L)); ++ } ++ // Paper end - lag compensation + + // Add env and gen to constructor, IWorldDataServer -> WorldDataServer + public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +index f168044d36f22080504da171e5ed31a6f02385ba..5cedce1f432f6b809b25269242a16477682c824f 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +@@ -127,7 +127,7 @@ public class ServerPlayerGameMode { + } + + public void tick() { +- this.gameTicks = MinecraftServer.currentTick; // CraftBukkit; ++ this.gameTicks = (int)this.level.getLagCompensationTick(); // CraftBukkit; // Paper - lag compensation + BlockState iblockdata; + + if (this.hasDelayedDestroy) { +diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java +index 2d375fc98e72246f6a640c390cb50d055fb59f8b..bad6a69246f6b0a4ada222b820f16469f91b0534 100644 +--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java ++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java +@@ -3833,6 +3833,10 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.resendPossiblyDesyncedDataValues(java.util.List.of(DATA_LIVING_ENTITY_FLAGS), serverPlayer); + } + // Paper end - Properly cancel usable items ++ // Paper start - lag compensate eating ++ protected long eatStartTime; ++ protected int totalEatTimeTicks; ++ // Paper end - lag compensate eating + private void updatingUsingItem() { + if (this.isUsingItem()) { + if (ItemStack.isSameItem(this.getItemInHand(this.getUsedItemHand()), this.useItem)) { +@@ -3851,7 +3855,12 @@ public abstract class LivingEntity extends Entity implements Attackable { + this.triggerItemUseEffects(stack, 5); + } + +- if (--this.useItemRemaining == 0 && !this.level().isClientSide && !stack.useOnRelease()) { ++ // Paper start - lag compensate eating ++ // we add 1 to the expected time to avoid lag compensating when we should not ++ boolean shouldLagCompensate = this.useItem.getItem().components().has(DataComponents.FOOD) && this.eatStartTime != -1 && (System.nanoTime() - this.eatStartTime) > ((1 + this.totalEatTimeTicks) * 50 * (1000 * 1000)); ++ if ((--this.useItemRemaining == 0 || shouldLagCompensate) && !this.level().isClientSide && !stack.useOnRelease()) { ++ this.useItemRemaining = 0; ++ // Paper end - lag compensate eating + this.completeUsingItem(); + } + +@@ -3897,7 +3906,10 @@ public abstract class LivingEntity extends Entity implements Attackable { + + if (!itemstack.isEmpty() && !this.isUsingItem() || forceUpdate) { // Paper - Prevent consuming the wrong itemstack + this.useItem = itemstack; +- this.useItemRemaining = itemstack.getUseDuration(); ++ // Paper start - lag compensate eating ++ this.useItemRemaining = this.totalEatTimeTicks = itemstack.getUseDuration(); ++ this.eatStartTime = System.nanoTime(); ++ // Paper end - lag compensate eating + if (!this.level().isClientSide) { + this.setLivingEntityFlag(1, true); + this.setLivingEntityFlag(2, hand == InteractionHand.OFF_HAND); +@@ -3922,7 +3934,10 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + } else if (!this.isUsingItem() && !this.useItem.isEmpty()) { + this.useItem = ItemStack.EMPTY; +- this.useItemRemaining = 0; ++ // Paper start - lag compensate eating ++ this.useItemRemaining = this.totalEatTimeTicks = 0; ++ this.eatStartTime = -1L; ++ // Paper end - lag compensate eating + } + } + +@@ -4057,7 +4072,10 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + this.useItem = ItemStack.EMPTY; +- this.useItemRemaining = 0; ++ // Paper start - lag compensate eating ++ this.useItemRemaining = this.totalEatTimeTicks = 0; ++ this.eatStartTime = -1L; ++ // Paper end - lag compensate eating + } + + public boolean isBlocking() { diff --git a/patches/server/1029-Optimise-chunk-tick-iteration.patch b/patches/server/1029-Optimise-chunk-tick-iteration.patch deleted file mode 100644 index 1452dcd9c5..0000000000 --- a/patches/server/1029-Optimise-chunk-tick-iteration.patch +++ /dev/null @@ -1,380 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sat, 23 Sep 2023 21:36:36 -0700 -Subject: [PATCH] Optimise chunk tick iteration - -When per-player mob spawning is enabled we do not need to randomly -shuffle the chunk list. Additionally, we can use the NearbyPlayers -class to quickly retrieve nearby players instead of possible -searching all players on the server. - -diff --git a/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java b/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java -index c3ce8a42dddd76b7189ad5685b23f9d9f8ccadb3..f164256d59b761264876ca0c85f812d101bfd5de 100644 ---- a/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java -+++ b/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java -@@ -17,7 +17,8 @@ public final class NearbyPlayers { - GENERAL_SMALL, - GENERAL_REALLY_SMALL, - TICK_VIEW_DISTANCE, -- VIEW_DISTANCE; -+ VIEW_DISTANCE, // Paper - optimise chunk iteration -+ SPAWN_RANGE, // Paper - optimise chunk iteration - } - - private static final NearbyMapType[] MOB_TYPES = NearbyMapType.values(); -@@ -26,10 +27,12 @@ public final class NearbyPlayers { - private static final int GENERAL_AREA_VIEW_DISTANCE = 33; - private static final int GENERAL_SMALL_VIEW_DISTANCE = 10; - private static final int GENERAL_REALLY_SMALL_VIEW_DISTANCE = 3; -+ private static final int SPAWN_RANGE_VIEW_DISTANCE = net.minecraft.server.level.DistanceManager.MOB_SPAWN_RANGE; // Paper - optimise chunk iteration - - public static final int GENERAL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_AREA_VIEW_DISTANCE << 4); - public static final int GENERAL_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_SMALL_VIEW_DISTANCE << 4); - public static final int GENERAL_REALLY_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_REALLY_SMALL_VIEW_DISTANCE << 4); -+ public static final int SPAWN_RANGE_VIEW_DISTANCE_BLOCKS = (SPAWN_RANGE_VIEW_DISTANCE << 4); // Paper - optimise chunk iteration - - private final ServerLevel world; - private final Reference2ReferenceOpenHashMap players = new Reference2ReferenceOpenHashMap<>(); -@@ -80,6 +83,7 @@ public final class NearbyPlayers { - players[NearbyMapType.GENERAL_REALLY_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_REALLY_SMALL_VIEW_DISTANCE); - players[NearbyMapType.TICK_VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getTickViewDistance(player)); - players[NearbyMapType.VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getLoadViewDistance(player)); -+ players[NearbyMapType.SPAWN_RANGE.ordinal()].update(chunk.x, chunk.z, SPAWN_RANGE_VIEW_DISTANCE); // Paper - optimise chunk iteration - } - - public TrackedChunk getChunk(final ChunkPos pos) { -diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java -index 13d15a135dd0373bef4a5ac9ffb56dbbf53353a0..472b9494f8a34a8ba90d6a2936b0db7530a229ad 100644 ---- a/src/main/java/net/minecraft/server/level/ChunkHolder.java -+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java -@@ -77,11 +77,19 @@ public class ChunkHolder { - - // Paper start - public void onChunkAdd() { -- -+ // Paper start - optimise chunk tick iteration -+ if (this.needsBroadcastChanges()) { -+ this.chunkMap.needsChangeBroadcasting.add(this); -+ } -+ // Paper end - optimise chunk tick iteration - } - - public void onChunkRemove() { -- -+ // Paper start - optimise chunk tick iteration -+ if (this.needsBroadcastChanges()) { -+ this.chunkMap.needsChangeBroadcasting.remove(this); -+ } -+ // Paper end - optimise chunk tick iteration - } - // Paper end - -@@ -216,7 +224,7 @@ public class ChunkHolder { - - if (i < 0 || i >= this.changedBlocksPerSection.length) return; // CraftBukkit - SPIGOT-6086, SPIGOT-6296 - if (this.changedBlocksPerSection[i] == null) { -- this.hasChangedSections = true; -+ this.hasChangedSections = true; this.addToBroadcastMap(); // Paper - optimise chunk tick iteration - this.changedBlocksPerSection[i] = new ShortOpenHashSet(); - } - -@@ -236,6 +244,7 @@ public class ChunkHolder { - int k = this.lightEngine.getMaxLightSection(); - - if (y >= j && y <= k) { -+ this.addToBroadcastMap(); // Paper - optimise chunk tick iteration - int l = y - j; - - if (lightType == LightLayer.SKY) { -@@ -254,9 +263,19 @@ public class ChunkHolder { - this.broadcast(this.getPlayers(onChunkViewEdge), packet); // Paper - rewrite chunk system - } - // Paper end - starlight -+ // Paper start - optimise chunk tick iteration -+ public final boolean needsBroadcastChanges() { -+ return this.hasChangedSections || !this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty(); -+ } -+ -+ private void addToBroadcastMap() { -+ io.papermc.paper.util.TickThread.ensureTickThread(this.chunkMap.level, this.pos, "Asynchronous ChunkHolder update is not allowed"); -+ this.chunkMap.needsChangeBroadcasting.add(this); -+ } -+ // Paper end - optimise chunk tick iteration - - public void broadcastChanges(LevelChunk chunk) { -- if (this.hasChangedSections || !this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty()) { -+ if (this.needsBroadcastChanges()) { // Paper - optimise chunk tick iteration; moved into above, other logic needs to call - Level world = chunk.getLevel(); - List list; - -diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java -index 48f7997e8a20f5a5a77516cbde990d0aacc2078a..dbe9df1e1973db133f7c8516256697ef7c968137 100644 ---- a/src/main/java/net/minecraft/server/level/ChunkMap.java -+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java -@@ -194,6 +194,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - this.playerEntityTrackerTrackMaps[i].remove(player); - } - // Paper end - use distance map to optimise tracker -+ this.playerMobSpawnMap.remove(player); // Paper - optimise chunk tick iteration - } - - void updateMaps(ServerPlayer player) { -@@ -243,6 +244,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - public final io.papermc.paper.util.player.NearbyPlayers nearbyPlayers; - // Paper end -+ // Paper start - optimise chunk tick iteration -+ public final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet needsChangeBroadcasting = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); -+ public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets); -+ // Paper end - optimise chunk tick iteration - - public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory, int viewDistance, boolean dsync) { - super(new RegionStorageInfo(session.getLevelId(), world.dimension(), "chunk"), session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync); -@@ -411,7 +416,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - // Paper end - Optional per player mob spawns - -- private static double euclideanDistanceSquared(ChunkPos pos, Entity entity) { -+ public static double euclideanDistanceSquared(ChunkPos pos, Entity entity) { // Paper - optimise chunk iteration; public - double d0 = (double) SectionPos.sectionToBlockCoord(pos.x, 8); - double d1 = (double) SectionPos.sectionToBlockCoord(pos.z, 8); - double d2 = d0 - entity.getX(); -diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java -index ed5154e41ca858f4d6b4d1c276c66831c038d2a6..cdb3c2cde5d9133ef60cf96d91762e6a7c8aeb4a 100644 ---- a/src/main/java/net/minecraft/server/level/DistanceManager.java -+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java -@@ -49,7 +49,7 @@ public abstract class DistanceManager { - private static final int INITIAL_TICKET_LIST_CAPACITY = 4; - final Long2ObjectMap> playersPerChunk = new Long2ObjectOpenHashMap(); - // Paper - rewrite chunk system -- private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8); -+ public static final int MOB_SPAWN_RANGE = 8; //private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8); // Paper - optimise chunk tick iteration - // Paper - rewrite chunk system - private final ChunkMap chunkMap; // Paper - -@@ -135,7 +135,7 @@ public abstract class DistanceManager { - long i = chunkcoordintpair.toLong(); - - // Paper - no longer used -- this.naturalSpawnChunkCounter.update(i, 0, true); -+ //this.naturalSpawnChunkCounter.update(i, 0, true); // Paper - optimise chunk tick iteration - //this.playerTicketManager.update(i, 0, true); // Paper - no longer used - //this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used - } -@@ -149,7 +149,7 @@ public abstract class DistanceManager { - if (objectset != null) objectset.remove(player); // Paper - some state corruption happens here, don't crash, clean up gracefully - if (objectset == null || objectset.isEmpty()) { // Paper - this.playersPerChunk.remove(i); -- this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false); -+ //this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false); // Paper - optimise chunk tick iteration - //this.playerTicketManager.update(i, Integer.MAX_VALUE, false); // Paper - no longer used - //this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used - } -@@ -191,13 +191,11 @@ public abstract class DistanceManager { - } - - public int getNaturalSpawnChunkCount() { -- this.naturalSpawnChunkCounter.runAllUpdates(); -- return this.naturalSpawnChunkCounter.chunks.size(); -+ return this.chunkMap.playerMobSpawnMap.size(); // Paper - optimise chunk tick iteration - } - - public boolean hasPlayersNearby(long chunkPos) { -- this.naturalSpawnChunkCounter.runAllUpdates(); -- return this.naturalSpawnChunkCounter.chunks.containsKey(chunkPos); -+ return this.chunkMap.playerMobSpawnMap.getObjectsInRange(chunkPos) != null; // Paper - optimise chunk tick iteration - } - - public String getDebugStatus() { -diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -index 18640c8681f6a3b2276123d19e3e8f0a8c630b41..b99f50604bafecbc68835974c9ed0caa91911a40 100644 ---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java -+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -@@ -506,18 +506,10 @@ public class ServerChunkCache extends ChunkSource { - - gameprofilerfiller.push("pollingChunks"); - gameprofilerfiller.push("filteringLoadedChunks"); -- List list = Lists.newArrayListWithCapacity(this.chunkMap.size()); -- Iterator iterator = this.chunkMap.getChunks().iterator(); -+ // Paper - optimise chunk tick iteration - if (this.level.getServer().tickRateManager().runsNormally()) this.level.timings.chunkTicks.startTiming(); // Paper - -- while (iterator.hasNext()) { -- ChunkHolder playerchunk = (ChunkHolder) iterator.next(); -- LevelChunk chunk = playerchunk.getTickingChunk(); -- -- if (chunk != null) { -- list.add(new ServerChunkCache.ChunkAndHolder(chunk, playerchunk)); -- } -- } -+ // Paper - optimise chunk tick iteration - - if (this.level.tickRateManager().runsNormally()) { - gameprofilerfiller.popPush("naturalSpawnCount"); -@@ -552,38 +544,109 @@ public class ServerChunkCache extends ChunkSource { - gameprofilerfiller.popPush("spawnAndTick"); - boolean flag = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit - -- Util.shuffle(list, this.level.random); -- // Paper start - PlayerNaturallySpawnCreaturesEvent -- int chunkRange = level.spigotConfig.mobSpawnRange; -- chunkRange = (chunkRange > level.spigotConfig.viewDistance) ? (byte) level.spigotConfig.viewDistance : chunkRange; -- chunkRange = Math.min(chunkRange, 8); -- for (ServerPlayer entityPlayer : this.level.players()) { -- entityPlayer.playerNaturallySpawnedEvent = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(entityPlayer.getBukkitEntity(), (byte) chunkRange); -- entityPlayer.playerNaturallySpawnedEvent.callEvent(); -+ // Paper start - optimise chunk tick iteration -+ ChunkMap playerChunkMap = this.chunkMap; -+ for (ServerPlayer player : this.level.players) { -+ if (!player.affectsSpawning || player.isSpectator()) { -+ playerChunkMap.playerMobSpawnMap.remove(player); -+ player.playerNaturallySpawnedEvent = null; -+ player.lastEntitySpawnRadiusSquared = -1.0; -+ continue; -+ } -+ -+ int viewDistance = io.papermc.paper.chunk.system.ChunkSystem.getTickViewDistance(player); -+ -+ // copied and modified from isOutisdeRange -+ int chunkRange = (int)level.spigotConfig.mobSpawnRange; -+ chunkRange = (chunkRange > viewDistance) ? viewDistance : chunkRange; -+ chunkRange = (chunkRange > DistanceManager.MOB_SPAWN_RANGE) ? DistanceManager.MOB_SPAWN_RANGE : chunkRange; -+ -+ com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent event = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(player.getBukkitEntity(), (byte)chunkRange); -+ event.callEvent(); -+ if (event.isCancelled() || event.getSpawnRadius() < 0) { -+ playerChunkMap.playerMobSpawnMap.remove(player); -+ player.playerNaturallySpawnedEvent = null; -+ player.lastEntitySpawnRadiusSquared = -1.0; -+ continue; -+ } -+ -+ int range = Math.min(event.getSpawnRadius(), DistanceManager.MOB_SPAWN_RANGE); // limit to max spawn range -+ int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkCoordinate(player.getX()); -+ int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkCoordinate(player.getZ()); -+ -+ playerChunkMap.playerMobSpawnMap.addOrUpdate(player, chunkX, chunkZ, range); -+ player.lastEntitySpawnRadiusSquared = (double)((range << 4) * (range << 4)); // used in anyPlayerCloseEnoughForSpawning -+ player.playerNaturallySpawnedEvent = event; - } -- // Paper end - PlayerNaturallySpawnCreaturesEvent -+ // Paper end - optimise chunk tick iteration - int l = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING); - boolean flag1 = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getLevelData().getGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit -- Iterator iterator1 = list.iterator(); -+ // Paper - optimise chunk tick iteration - - int chunksTicked = 0; // Paper -- while (iterator1.hasNext()) { -- ServerChunkCache.ChunkAndHolder chunkproviderserver_a = (ServerChunkCache.ChunkAndHolder) iterator1.next(); -- LevelChunk chunk1 = chunkproviderserver_a.chunk; -+ // Paper start - optimise chunk tick iteration -+ io.papermc.paper.util.player.NearbyPlayers nearbyPlayers = this.chunkMap.getNearbyPlayers(); // Paper - optimise chunk tick iteration -+ Iterator chunkIterator; -+ if (this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { -+ chunkIterator = this.tickingChunks.iterator(); -+ } else { -+ chunkIterator = this.tickingChunks.unsafeIterator(); -+ List shuffled = Lists.newArrayListWithCapacity(this.tickingChunks.size()); -+ while (chunkIterator.hasNext()) { -+ shuffled.add(chunkIterator.next()); -+ } -+ Util.shuffle(shuffled, this.level.random); -+ chunkIterator = shuffled.iterator(); -+ } -+ try { -+ // Paper end - optimise chunk tick iteration -+ while (chunkIterator.hasNext()) { -+ LevelChunk chunk1 = chunkIterator.next(); -+ // Paper end - optimise chunk tick iteration - ChunkPos chunkcoordintpair = chunk1.getPos(); - -- if (this.level.isNaturalSpawningAllowed(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(chunkcoordintpair)) { -+ // Paper start - optimise chunk tick iteration -+ com.destroystokyo.paper.util.maplist.ReferenceList playersNearby -+ = nearbyPlayers.getPlayers(chunkcoordintpair, io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.SPAWN_RANGE); -+ if (playersNearby == null) { -+ continue; -+ } -+ Object[] rawData = playersNearby.getRawData(); -+ boolean spawn = false; -+ boolean tick = false; -+ for (int itr = 0, len = playersNearby.size(); itr < len; ++itr) { -+ ServerPlayer player = (ServerPlayer)rawData[itr]; -+ if (player.isSpectator()) { -+ continue; -+ } -+ -+ double distance = ChunkMap.euclideanDistanceSquared(chunkcoordintpair, player); -+ spawn |= player.lastEntitySpawnRadiusSquared >= distance; -+ tick |= ((double)io.papermc.paper.util.player.NearbyPlayers.SPAWN_RANGE_VIEW_DISTANCE_BLOCKS) * ((double)io.papermc.paper.util.player.NearbyPlayers.SPAWN_RANGE_VIEW_DISTANCE_BLOCKS) >= distance; -+ if (spawn & tick) { -+ break; -+ } -+ } -+ if (tick && chunk1.chunkStatus.isOrAfter(net.minecraft.server.level.FullChunkStatus.ENTITY_TICKING)) { -+ // Paper end - optimise chunk tick iteration - chunk1.incrementInhabitedTime(j); -- if (flag && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(chunkcoordintpair, true)) { // Spigot -+ if (spawn && flag && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair)) { // Spigot // Paper - optimise chunk tick iteration - NaturalSpawner.spawnForChunk(this.level, chunk1, spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag1); - } - -- if (this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { -+ if (true || this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { // Paper - optimise chunk tick iteration - this.level.tickChunk(chunk1, l); - if ((chunksTicked++ & 1) == 0) net.minecraft.server.MinecraftServer.getServer().executeMidTickTasks(); // Paper - } - } - } -+ // Paper start - optimise chunk tick iteration -+ } finally { -+ if (chunkIterator instanceof io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.Iterator safeIterator) { -+ safeIterator.finishedIterating(); -+ } -+ } -+ // Paper end - optimise chunk tick iteration - this.level.timings.chunkTicks.stopTiming(); // Paper - - gameprofilerfiller.popPush("customSpawners"); -@@ -595,11 +658,23 @@ public class ServerChunkCache extends ChunkSource { - } - - gameprofilerfiller.popPush("broadcast"); -- list.forEach((chunkproviderserver_a1) -> { -+ // Paper - optimise chunk tick iteration - this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timing -- chunkproviderserver_a1.holder.broadcastChanges(chunkproviderserver_a1.chunk); -+ // Paper start - optimise chunk tick iteration -+ if (!this.chunkMap.needsChangeBroadcasting.isEmpty()) { -+ it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet copy = this.chunkMap.needsChangeBroadcasting.clone(); -+ this.chunkMap.needsChangeBroadcasting.clear(); -+ for (ChunkHolder holder : copy) { -+ holder.broadcastChanges(holder.getFullChunkNowUnchecked()); // LevelChunks are NEVER unloaded -+ if (holder.needsBroadcastChanges()) { -+ // I DON'T want to KNOW what DUMB plugins might be doing. -+ this.chunkMap.needsChangeBroadcasting.add(holder); -+ } -+ } -+ } -+ // Paper end - optimise chunk tick iteration - this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timing -- }); -+ // Paper - optimise chunk tick iteration - gameprofilerfiller.pop(); - gameprofilerfiller.pop(); - } -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index ed42a78aebeda86b77f27cba4fcefbc6c17f86fc..49d612c1b84fe4f2335c9cf91cce5f60b6779ff6 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -341,6 +341,9 @@ public class ServerPlayer extends Player { - }); - } - // Paper end - replace player chunk loader -+ // Paper start - optimise chunk tick iteration -+ public double lastEntitySpawnRadiusSquared = -1.0; -+ // Paper end - optimise chunk tick iteration - - public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) { - super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile); diff --git a/patches/server/1030-Lag-compensation-ticks.patch b/patches/server/1030-Lag-compensation-ticks.patch deleted file mode 100644 index 07b5e78339..0000000000 --- a/patches/server/1030-Lag-compensation-ticks.patch +++ /dev/null @@ -1,129 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sat, 23 Sep 2023 22:05:35 -0700 -Subject: [PATCH] Lag compensation ticks - -Areas affected by lag comepnsation: - - Block breaking and destroying - - Eating food items - -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index b12f69931e5b247c42bb4e56bcf1ef24deddfe2e..40551f2bb4e5511d1feefe87244905f1ee84b511 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -318,6 +318,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { - AtomicReference atomicreference = new AtomicReference(); -@@ -1757,6 +1758,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper - BlockPhysicsEvent - worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent - net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers -+ worldserver.updateLagCompensationTick(); // Paper - lag compensation - - this.profiler.push(() -> { - String s = String.valueOf(worldserver); -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 2bfebbe0bc5a7f72c9ebe60fded260b3f94ac749..89ecaaa774e59ad01eec0ce2fe546026a8b47ae8 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -570,6 +570,17 @@ public class ServerLevel extends Level implements WorldGenLevel { - return player != null && player.level() == this ? player : null; - } - // Paper end - optimise getPlayerByUUID -+ // Paper start - lag compensation -+ private long lagCompensationTick = net.minecraft.server.MinecraftServer.SERVER_INIT; -+ -+ public long getLagCompensationTick() { -+ return this.lagCompensationTick; -+ } -+ -+ public void updateLagCompensationTick() { -+ this.lagCompensationTick = (System.nanoTime() - net.minecraft.server.MinecraftServer.SERVER_INIT) / (java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(50L)); -+ } -+ // Paper end - lag compensation - - // Add env and gen to constructor, IWorldDataServer -> WorldDataServer - public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -index f168044d36f22080504da171e5ed31a6f02385ba..5cedce1f432f6b809b25269242a16477682c824f 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -@@ -127,7 +127,7 @@ public class ServerPlayerGameMode { - } - - public void tick() { -- this.gameTicks = MinecraftServer.currentTick; // CraftBukkit; -+ this.gameTicks = (int)this.level.getLagCompensationTick(); // CraftBukkit; // Paper - lag compensation - BlockState iblockdata; - - if (this.hasDelayedDestroy) { -diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index 2d375fc98e72246f6a640c390cb50d055fb59f8b..bad6a69246f6b0a4ada222b820f16469f91b0534 100644 ---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -3833,6 +3833,10 @@ public abstract class LivingEntity extends Entity implements Attackable { - this.resendPossiblyDesyncedDataValues(java.util.List.of(DATA_LIVING_ENTITY_FLAGS), serverPlayer); - } - // Paper end - Properly cancel usable items -+ // Paper start - lag compensate eating -+ protected long eatStartTime; -+ protected int totalEatTimeTicks; -+ // Paper end - lag compensate eating - private void updatingUsingItem() { - if (this.isUsingItem()) { - if (ItemStack.isSameItem(this.getItemInHand(this.getUsedItemHand()), this.useItem)) { -@@ -3851,7 +3855,12 @@ public abstract class LivingEntity extends Entity implements Attackable { - this.triggerItemUseEffects(stack, 5); - } - -- if (--this.useItemRemaining == 0 && !this.level().isClientSide && !stack.useOnRelease()) { -+ // Paper start - lag compensate eating -+ // we add 1 to the expected time to avoid lag compensating when we should not -+ boolean shouldLagCompensate = this.useItem.getItem().components().has(DataComponents.FOOD) && this.eatStartTime != -1 && (System.nanoTime() - this.eatStartTime) > ((1 + this.totalEatTimeTicks) * 50 * (1000 * 1000)); -+ if ((--this.useItemRemaining == 0 || shouldLagCompensate) && !this.level().isClientSide && !stack.useOnRelease()) { -+ this.useItemRemaining = 0; -+ // Paper end - lag compensate eating - this.completeUsingItem(); - } - -@@ -3897,7 +3906,10 @@ public abstract class LivingEntity extends Entity implements Attackable { - - if (!itemstack.isEmpty() && !this.isUsingItem() || forceUpdate) { // Paper - Prevent consuming the wrong itemstack - this.useItem = itemstack; -- this.useItemRemaining = itemstack.getUseDuration(); -+ // Paper start - lag compensate eating -+ this.useItemRemaining = this.totalEatTimeTicks = itemstack.getUseDuration(); -+ this.eatStartTime = System.nanoTime(); -+ // Paper end - lag compensate eating - if (!this.level().isClientSide) { - this.setLivingEntityFlag(1, true); - this.setLivingEntityFlag(2, hand == InteractionHand.OFF_HAND); -@@ -3922,7 +3934,10 @@ public abstract class LivingEntity extends Entity implements Attackable { - } - } else if (!this.isUsingItem() && !this.useItem.isEmpty()) { - this.useItem = ItemStack.EMPTY; -- this.useItemRemaining = 0; -+ // Paper start - lag compensate eating -+ this.useItemRemaining = this.totalEatTimeTicks = 0; -+ this.eatStartTime = -1L; -+ // Paper end - lag compensate eating - } - } - -@@ -4057,7 +4072,10 @@ public abstract class LivingEntity extends Entity implements Attackable { - } - - this.useItem = ItemStack.EMPTY; -- this.useItemRemaining = 0; -+ // Paper start - lag compensate eating -+ this.useItemRemaining = this.totalEatTimeTicks = 0; -+ this.eatStartTime = -1L; -+ // Paper end - lag compensate eating - } - - public boolean isBlocking() { diff --git a/patches/server/1030-Optimise-nearby-player-retrieval.patch b/patches/server/1030-Optimise-nearby-player-retrieval.patch new file mode 100644 index 0000000000..080816f839 --- /dev/null +++ b/patches/server/1030-Optimise-nearby-player-retrieval.patch @@ -0,0 +1,228 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sat, 23 Sep 2023 23:15:52 -0700 +Subject: [PATCH] Optimise nearby player retrieval + +Instead of searching/testing every player online on the server, +we can instead use the nearby player tracking system to reduce +the number of tests per search. + +diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java +index 89ecaaa774e59ad01eec0ce2fe546026a8b47ae8..12a9342634ffa7d6aaa9a7fdd65a45bccec8dd13 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -581,6 +581,115 @@ public class ServerLevel extends Level implements WorldGenLevel { + this.lagCompensationTick = (System.nanoTime() - net.minecraft.server.MinecraftServer.SERVER_INIT) / (java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(50L)); + } + // Paper end - lag compensation ++ // Paper start - optimise nearby player retrieval ++ @Override ++ public java.util.List getNearbyPlayers(net.minecraft.world.entity.ai.targeting.TargetingConditions targetPredicate, ++ net.minecraft.world.entity.LivingEntity entity, ++ net.minecraft.world.phys.AABB box) { ++ return this.getNearbyEntities(Player.class, targetPredicate, entity, box); ++ } ++ ++ @Override ++ public Player getNearestPlayer(double x, double y, double z, double maxDistance, @Nullable Predicate targetPredicate) { ++ if (maxDistance > 0.0D) { ++ io.papermc.paper.util.player.NearbyPlayers players = this.chunkSource.chunkMap.getNearbyPlayers(); ++ ++ com.destroystokyo.paper.util.maplist.ReferenceList nearby = players.getPlayersByBlock( ++ io.papermc.paper.util.CoordinateUtils.getBlockCoordinate(x), ++ io.papermc.paper.util.CoordinateUtils.getBlockCoordinate(z), ++ io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.GENERAL ++ ); ++ ++ if (nearby == null) { ++ return null; ++ } ++ ++ ServerPlayer nearest = null; ++ double nearestDist = maxDistance * maxDistance; ++ Object[] rawData = nearby.getRawData(); ++ for (int i = 0, len = nearby.size(); i < len; ++i) { ++ ServerPlayer player = (ServerPlayer)rawData[i]; ++ double dist = player.distanceToSqr(x, y, z); ++ if (dist >= nearestDist) { ++ continue; ++ } ++ ++ if (targetPredicate == null || targetPredicate.test(player)) { ++ nearest = player; ++ nearestDist = dist; ++ } ++ } ++ ++ return nearest; ++ } else { ++ ServerPlayer nearest = null; ++ double nearestDist = Double.MAX_VALUE; ++ ++ for (ServerPlayer player : this.players()) { ++ double dist = player.distanceToSqr(x, y, z); ++ if (dist >= nearestDist) { ++ continue; ++ } ++ ++ if (targetPredicate == null || targetPredicate.test(player)) { ++ nearest = player; ++ nearestDist = dist; ++ } ++ } ++ ++ return nearest; ++ } ++ } ++ ++ @Override ++ public Player getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions targetPredicate, LivingEntity entity) { ++ return this.getNearestPlayer(targetPredicate, entity, entity.getX(), entity.getY(), entity.getZ()); ++ } ++ ++ @Override ++ public Player getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions targetPredicate, LivingEntity entity, ++ double x, double y, double z) { ++ double range = targetPredicate.range; ++ if (range > 0.0D) { ++ io.papermc.paper.util.player.NearbyPlayers players = this.chunkSource.chunkMap.getNearbyPlayers(); ++ ++ com.destroystokyo.paper.util.maplist.ReferenceList nearby = players.getPlayersByBlock( ++ io.papermc.paper.util.CoordinateUtils.getBlockCoordinate(x), ++ io.papermc.paper.util.CoordinateUtils.getBlockCoordinate(z), ++ io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.GENERAL ++ ); ++ ++ if (nearby == null) { ++ return null; ++ } ++ ++ ServerPlayer nearest = null; ++ double nearestDist = Double.MAX_VALUE; ++ Object[] rawData = nearby.getRawData(); ++ for (int i = 0, len = nearby.size(); i < len; ++i) { ++ ServerPlayer player = (ServerPlayer)rawData[i]; ++ double dist = player.distanceToSqr(x, y, z); ++ if (dist >= nearestDist) { ++ continue; ++ } ++ ++ if (targetPredicate.test(entity, player)) { ++ nearest = player; ++ nearestDist = dist; ++ } ++ } ++ ++ return nearest; ++ } else { ++ return this.getNearestEntity(this.players(), targetPredicate, entity, x, y, z); ++ } ++ } ++ ++ @Nullable ++ public Player getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions targetPredicate, double x, double y, double z) { ++ return this.getNearestPlayer(targetPredicate, null, x, y, z); ++ } ++ // Paper end - optimise nearby player retrieval + + // Add env and gen to constructor, IWorldDataServer -> WorldDataServer + public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { +diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/PlayerSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/PlayerSensor.java +index 2e887e426dcd79e2dda401f127d0e01ca537e80e..65cd42ce9f553e0aa5bf248bdbf902f9d1f55460 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/sensing/PlayerSensor.java ++++ b/src/main/java/net/minecraft/world/entity/ai/sensing/PlayerSensor.java +@@ -21,17 +21,50 @@ public class PlayerSensor extends Sensor { + + @Override + protected void doTick(ServerLevel world, LivingEntity entity) { +- List list = world.players() +- .stream() +- .filter(EntitySelector.NO_SPECTATORS) +- .filter(player -> entity.closerThan(player, 16.0)) +- .sorted(Comparator.comparingDouble(entity::distanceToSqr)) +- .collect(Collectors.toList()); ++ // Paper start - Perf: optimise nearby player retrieval & remove streams from hot code ++ io.papermc.paper.util.player.NearbyPlayers nearbyPlayers = world.chunkSource.chunkMap.getNearbyPlayers(); ++ net.minecraft.world.phys.Vec3 entityPos = entity.position(); ++ com.destroystokyo.paper.util.maplist.ReferenceList nearby = nearbyPlayers.getPlayersByChunk( ++ entity.chunkPosition().x, ++ entity.chunkPosition().z, ++ io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.GENERAL_REALLY_SMALL ++ ); ++ ++ List players = new java.util.ArrayList<>(nearby == null ? 0 : nearby.size()); ++ if (nearby != null) { ++ Object[] rawData = nearby.getRawData(); ++ for (int index = 0, len = nearby.size(); index < len; ++index) { ++ net.minecraft.server.level.ServerPlayer player = (net.minecraft.server.level.ServerPlayer) rawData[index]; ++ if (player.isSpectator()) { ++ continue; ++ } ++ if (player.distanceToSqr(entityPos.x, entityPos.y, entityPos.z) >= (16.0 * 16.0)) { ++ continue; ++ } ++ players.add(player); ++ } ++ } ++ players.sort(Comparator.comparingDouble(entity::distanceToSqr)); + Brain brain = entity.getBrain(); +- brain.setMemory(MemoryModuleType.NEAREST_PLAYERS, list); +- List list2 = list.stream().filter(player -> isEntityTargetable(entity, player)).collect(Collectors.toList()); +- brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_PLAYER, list2.isEmpty() ? null : list2.get(0)); +- Optional optional = list2.stream().filter(player -> isEntityAttackable(entity, player)).findFirst(); +- brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_ATTACKABLE_PLAYER, optional); ++ ++ brain.setMemory(MemoryModuleType.NEAREST_PLAYERS, players); ++ ++ Player firstTargetable = null; ++ Player firstAttackable = null; ++ for (Player player : players) { ++ if (firstTargetable == null && Sensor.isEntityTargetable(entity, player)) { ++ firstTargetable = player; ++ } ++ if (firstAttackable == null && Sensor.isEntityAttackable(entity, player)) { ++ firstAttackable = player; ++ } ++ ++ if (firstAttackable != null && firstTargetable != null) { ++ break; ++ } ++ } ++ brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_PLAYER, firstTargetable); ++ brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_ATTACKABLE_PLAYER, Optional.ofNullable(firstAttackable)); ++ // Paper end - Perf: optimise nearby player retrieval & remove streams from hot code + } + } +diff --git a/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java b/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java +index aecb0ad814586bfc5e56755ee14379a69388b38c..d2f0c3b26d4beedb49d86e0242d843590d469d02 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java ++++ b/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java +@@ -10,7 +10,7 @@ public class TargetingConditions { + public static final TargetingConditions DEFAULT = forCombat(); + private static final double MIN_VISIBILITY_DISTANCE_FOR_INVISIBLE_TARGET = 2.0; + private final boolean isCombat; +- private double range = -1.0; ++ public double range = -1.0; // Paper - public + private boolean checkLineOfSight = true; + private boolean testInvisible = true; + @Nullable +diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java +index 21843501355a0c0c8d594e3e5312e97861c9a777..ea0aee88c7d901034427db201c1b2430f8a1d522 100644 +--- a/src/main/java/net/minecraft/world/level/EntityGetter.java ++++ b/src/main/java/net/minecraft/world/level/EntityGetter.java +@@ -233,9 +233,13 @@ public interface EntityGetter { + T livingEntity = null; + + for (T livingEntity2 : entityList) { ++ // Paper start - optimise nearby player retrieval; move up ++ // don't check entities outside closest range ++ double e = livingEntity2.distanceToSqr(x, y, z); ++ if (d == -1.0 || e < d) { ++ // Paper end - move up + if (targetPredicate.test(entity, livingEntity2)) { +- double e = livingEntity2.distanceToSqr(x, y, z); +- if (d == -1.0 || e < d) { ++ // Paper - optimise nearby player retrieval; move up + d = e; + livingEntity = livingEntity2; + } diff --git a/patches/server/1031-Distance-manager-tick-timings.patch b/patches/server/1031-Distance-manager-tick-timings.patch new file mode 100644 index 0000000000..f946d92596 --- /dev/null +++ b/patches/server/1031-Distance-manager-tick-timings.patch @@ -0,0 +1,34 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Sat, 18 Jul 2020 16:03:57 -0700 +Subject: [PATCH] Distance manager tick timings + +Recently this has been taking up more time, so add a timings to +really figure out how much. + +diff --git a/src/main/java/co/aikar/timings/MinecraftTimings.java b/src/main/java/co/aikar/timings/MinecraftTimings.java +index 46449728f69ee7d4f78470f8da23c055acd53a3b..4b467f1af93452d13829f756d55dee18b8889d40 100644 +--- a/src/main/java/co/aikar/timings/MinecraftTimings.java ++++ b/src/main/java/co/aikar/timings/MinecraftTimings.java +@@ -47,6 +47,7 @@ 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"); // Paper - add timings for scoreboard search ++ public static final Timing distanceManagerTick = Timings.ofSafe("Distance Manager Tick"); // Paper - add timings for distance manager + + public static final Timing midTickChunkTasks = Timings.ofSafe("Mid Tick Chunk Tasks"); + +diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java +index 5b446e6ac151f99f64f0c442d0b40b5e251bc4c4..6bc7c6f16a1649fc9e24e7cf90fca401e5bd4875 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java ++++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java +@@ -1316,7 +1316,9 @@ public final class ChunkHolderManager { + } + + public boolean processTicketUpdates() { ++ co.aikar.timings.MinecraftTimings.distanceManagerTick.startTiming(); try { // Paper - add timings for distance manager + return this.processTicketUpdates(true, true, null); ++ } finally { co.aikar.timings.MinecraftTimings.distanceManagerTick.stopTiming(); } // Paper - add timings for distance manager + } + + private static final ThreadLocal> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>(); diff --git a/patches/server/1031-Optimise-nearby-player-retrieval.patch b/patches/server/1031-Optimise-nearby-player-retrieval.patch deleted file mode 100644 index 080816f839..0000000000 --- a/patches/server/1031-Optimise-nearby-player-retrieval.patch +++ /dev/null @@ -1,228 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sat, 23 Sep 2023 23:15:52 -0700 -Subject: [PATCH] Optimise nearby player retrieval - -Instead of searching/testing every player online on the server, -we can instead use the nearby player tracking system to reduce -the number of tests per search. - -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 89ecaaa774e59ad01eec0ce2fe546026a8b47ae8..12a9342634ffa7d6aaa9a7fdd65a45bccec8dd13 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -581,6 +581,115 @@ public class ServerLevel extends Level implements WorldGenLevel { - this.lagCompensationTick = (System.nanoTime() - net.minecraft.server.MinecraftServer.SERVER_INIT) / (java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(50L)); - } - // Paper end - lag compensation -+ // Paper start - optimise nearby player retrieval -+ @Override -+ public java.util.List getNearbyPlayers(net.minecraft.world.entity.ai.targeting.TargetingConditions targetPredicate, -+ net.minecraft.world.entity.LivingEntity entity, -+ net.minecraft.world.phys.AABB box) { -+ return this.getNearbyEntities(Player.class, targetPredicate, entity, box); -+ } -+ -+ @Override -+ public Player getNearestPlayer(double x, double y, double z, double maxDistance, @Nullable Predicate targetPredicate) { -+ if (maxDistance > 0.0D) { -+ io.papermc.paper.util.player.NearbyPlayers players = this.chunkSource.chunkMap.getNearbyPlayers(); -+ -+ com.destroystokyo.paper.util.maplist.ReferenceList nearby = players.getPlayersByBlock( -+ io.papermc.paper.util.CoordinateUtils.getBlockCoordinate(x), -+ io.papermc.paper.util.CoordinateUtils.getBlockCoordinate(z), -+ io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.GENERAL -+ ); -+ -+ if (nearby == null) { -+ return null; -+ } -+ -+ ServerPlayer nearest = null; -+ double nearestDist = maxDistance * maxDistance; -+ Object[] rawData = nearby.getRawData(); -+ for (int i = 0, len = nearby.size(); i < len; ++i) { -+ ServerPlayer player = (ServerPlayer)rawData[i]; -+ double dist = player.distanceToSqr(x, y, z); -+ if (dist >= nearestDist) { -+ continue; -+ } -+ -+ if (targetPredicate == null || targetPredicate.test(player)) { -+ nearest = player; -+ nearestDist = dist; -+ } -+ } -+ -+ return nearest; -+ } else { -+ ServerPlayer nearest = null; -+ double nearestDist = Double.MAX_VALUE; -+ -+ for (ServerPlayer player : this.players()) { -+ double dist = player.distanceToSqr(x, y, z); -+ if (dist >= nearestDist) { -+ continue; -+ } -+ -+ if (targetPredicate == null || targetPredicate.test(player)) { -+ nearest = player; -+ nearestDist = dist; -+ } -+ } -+ -+ return nearest; -+ } -+ } -+ -+ @Override -+ public Player getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions targetPredicate, LivingEntity entity) { -+ return this.getNearestPlayer(targetPredicate, entity, entity.getX(), entity.getY(), entity.getZ()); -+ } -+ -+ @Override -+ public Player getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions targetPredicate, LivingEntity entity, -+ double x, double y, double z) { -+ double range = targetPredicate.range; -+ if (range > 0.0D) { -+ io.papermc.paper.util.player.NearbyPlayers players = this.chunkSource.chunkMap.getNearbyPlayers(); -+ -+ com.destroystokyo.paper.util.maplist.ReferenceList nearby = players.getPlayersByBlock( -+ io.papermc.paper.util.CoordinateUtils.getBlockCoordinate(x), -+ io.papermc.paper.util.CoordinateUtils.getBlockCoordinate(z), -+ io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.GENERAL -+ ); -+ -+ if (nearby == null) { -+ return null; -+ } -+ -+ ServerPlayer nearest = null; -+ double nearestDist = Double.MAX_VALUE; -+ Object[] rawData = nearby.getRawData(); -+ for (int i = 0, len = nearby.size(); i < len; ++i) { -+ ServerPlayer player = (ServerPlayer)rawData[i]; -+ double dist = player.distanceToSqr(x, y, z); -+ if (dist >= nearestDist) { -+ continue; -+ } -+ -+ if (targetPredicate.test(entity, player)) { -+ nearest = player; -+ nearestDist = dist; -+ } -+ } -+ -+ return nearest; -+ } else { -+ return this.getNearestEntity(this.players(), targetPredicate, entity, x, y, z); -+ } -+ } -+ -+ @Nullable -+ public Player getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions targetPredicate, double x, double y, double z) { -+ return this.getNearestPlayer(targetPredicate, null, x, y, z); -+ } -+ // Paper end - optimise nearby player retrieval - - // Add env and gen to constructor, IWorldDataServer -> WorldDataServer - public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { -diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/PlayerSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/PlayerSensor.java -index 2e887e426dcd79e2dda401f127d0e01ca537e80e..65cd42ce9f553e0aa5bf248bdbf902f9d1f55460 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/sensing/PlayerSensor.java -+++ b/src/main/java/net/minecraft/world/entity/ai/sensing/PlayerSensor.java -@@ -21,17 +21,50 @@ public class PlayerSensor extends Sensor { - - @Override - protected void doTick(ServerLevel world, LivingEntity entity) { -- List list = world.players() -- .stream() -- .filter(EntitySelector.NO_SPECTATORS) -- .filter(player -> entity.closerThan(player, 16.0)) -- .sorted(Comparator.comparingDouble(entity::distanceToSqr)) -- .collect(Collectors.toList()); -+ // Paper start - Perf: optimise nearby player retrieval & remove streams from hot code -+ io.papermc.paper.util.player.NearbyPlayers nearbyPlayers = world.chunkSource.chunkMap.getNearbyPlayers(); -+ net.minecraft.world.phys.Vec3 entityPos = entity.position(); -+ com.destroystokyo.paper.util.maplist.ReferenceList nearby = nearbyPlayers.getPlayersByChunk( -+ entity.chunkPosition().x, -+ entity.chunkPosition().z, -+ io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.GENERAL_REALLY_SMALL -+ ); -+ -+ List players = new java.util.ArrayList<>(nearby == null ? 0 : nearby.size()); -+ if (nearby != null) { -+ Object[] rawData = nearby.getRawData(); -+ for (int index = 0, len = nearby.size(); index < len; ++index) { -+ net.minecraft.server.level.ServerPlayer player = (net.minecraft.server.level.ServerPlayer) rawData[index]; -+ if (player.isSpectator()) { -+ continue; -+ } -+ if (player.distanceToSqr(entityPos.x, entityPos.y, entityPos.z) >= (16.0 * 16.0)) { -+ continue; -+ } -+ players.add(player); -+ } -+ } -+ players.sort(Comparator.comparingDouble(entity::distanceToSqr)); - Brain brain = entity.getBrain(); -- brain.setMemory(MemoryModuleType.NEAREST_PLAYERS, list); -- List list2 = list.stream().filter(player -> isEntityTargetable(entity, player)).collect(Collectors.toList()); -- brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_PLAYER, list2.isEmpty() ? null : list2.get(0)); -- Optional optional = list2.stream().filter(player -> isEntityAttackable(entity, player)).findFirst(); -- brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_ATTACKABLE_PLAYER, optional); -+ -+ brain.setMemory(MemoryModuleType.NEAREST_PLAYERS, players); -+ -+ Player firstTargetable = null; -+ Player firstAttackable = null; -+ for (Player player : players) { -+ if (firstTargetable == null && Sensor.isEntityTargetable(entity, player)) { -+ firstTargetable = player; -+ } -+ if (firstAttackable == null && Sensor.isEntityAttackable(entity, player)) { -+ firstAttackable = player; -+ } -+ -+ if (firstAttackable != null && firstTargetable != null) { -+ break; -+ } -+ } -+ brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_PLAYER, firstTargetable); -+ brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_ATTACKABLE_PLAYER, Optional.ofNullable(firstAttackable)); -+ // Paper end - Perf: optimise nearby player retrieval & remove streams from hot code - } - } -diff --git a/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java b/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java -index aecb0ad814586bfc5e56755ee14379a69388b38c..d2f0c3b26d4beedb49d86e0242d843590d469d02 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java -+++ b/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java -@@ -10,7 +10,7 @@ public class TargetingConditions { - public static final TargetingConditions DEFAULT = forCombat(); - private static final double MIN_VISIBILITY_DISTANCE_FOR_INVISIBLE_TARGET = 2.0; - private final boolean isCombat; -- private double range = -1.0; -+ public double range = -1.0; // Paper - public - private boolean checkLineOfSight = true; - private boolean testInvisible = true; - @Nullable -diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java -index 21843501355a0c0c8d594e3e5312e97861c9a777..ea0aee88c7d901034427db201c1b2430f8a1d522 100644 ---- a/src/main/java/net/minecraft/world/level/EntityGetter.java -+++ b/src/main/java/net/minecraft/world/level/EntityGetter.java -@@ -233,9 +233,13 @@ public interface EntityGetter { - T livingEntity = null; - - for (T livingEntity2 : entityList) { -+ // Paper start - optimise nearby player retrieval; move up -+ // don't check entities outside closest range -+ double e = livingEntity2.distanceToSqr(x, y, z); -+ if (d == -1.0 || e < d) { -+ // Paper end - move up - if (targetPredicate.test(entity, livingEntity2)) { -- double e = livingEntity2.distanceToSqr(x, y, z); -- if (d == -1.0 || e < d) { -+ // Paper - optimise nearby player retrieval; move up - d = e; - livingEntity = livingEntity2; - } diff --git a/patches/server/1032-Distance-manager-tick-timings.patch b/patches/server/1032-Distance-manager-tick-timings.patch deleted file mode 100644 index f946d92596..0000000000 --- a/patches/server/1032-Distance-manager-tick-timings.patch +++ /dev/null @@ -1,34 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sat, 18 Jul 2020 16:03:57 -0700 -Subject: [PATCH] Distance manager tick timings - -Recently this has been taking up more time, so add a timings to -really figure out how much. - -diff --git a/src/main/java/co/aikar/timings/MinecraftTimings.java b/src/main/java/co/aikar/timings/MinecraftTimings.java -index 46449728f69ee7d4f78470f8da23c055acd53a3b..4b467f1af93452d13829f756d55dee18b8889d40 100644 ---- a/src/main/java/co/aikar/timings/MinecraftTimings.java -+++ b/src/main/java/co/aikar/timings/MinecraftTimings.java -@@ -47,6 +47,7 @@ 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"); // Paper - add timings for scoreboard search -+ public static final Timing distanceManagerTick = Timings.ofSafe("Distance Manager Tick"); // Paper - add timings for distance manager - - public static final Timing midTickChunkTasks = Timings.ofSafe("Mid Tick Chunk Tasks"); - -diff --git a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java -index 5b446e6ac151f99f64f0c442d0b40b5e251bc4c4..6bc7c6f16a1649fc9e24e7cf90fca401e5bd4875 100644 ---- a/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java -+++ b/src/main/java/io/papermc/paper/chunk/system/scheduling/ChunkHolderManager.java -@@ -1316,7 +1316,9 @@ public final class ChunkHolderManager { - } - - public boolean processTicketUpdates() { -+ co.aikar.timings.MinecraftTimings.distanceManagerTick.startTiming(); try { // Paper - add timings for distance manager - return this.processTicketUpdates(true, true, null); -+ } finally { co.aikar.timings.MinecraftTimings.distanceManagerTick.stopTiming(); } // Paper - add timings for distance manager - } - - private static final ThreadLocal> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>(); diff --git a/patches/server/1032-Handle-Oversized-block-entities-in-chunks.patch b/patches/server/1032-Handle-Oversized-block-entities-in-chunks.patch new file mode 100644 index 0000000000..b9ea4832bd --- /dev/null +++ b/patches/server/1032-Handle-Oversized-block-entities-in-chunks.patch @@ -0,0 +1,64 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar +Date: Wed, 6 May 2020 05:00:57 -0400 +Subject: [PATCH] Handle Oversized block entities in chunks + +Splits out Extra Packets if too many TE's are encountered to prevent +creating too large of a packet to sed. + +Co-authored-by: Spottedleaf + +diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java +index 05e29a703f022b5047262bae8caef33d9dfb9035..2501fbaf497d226051800c53d60a39bbc80db91c 100644 +--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java ++++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java +@@ -27,6 +27,14 @@ public class ClientboundLevelChunkPacketData { + private final CompoundTag heightmaps; + private final byte[] buffer; + private final List blockEntitiesData; ++ // Paper start - Handle oversized block entities in chunks ++ private final java.util.List> extraPackets = new java.util.ArrayList<>(); ++ private static final int TE_LIMIT = Integer.getInteger("Paper.excessiveTELimit", 750); ++ ++ public List> getExtraPackets() { ++ return this.extraPackets; ++ } ++ // Paper end - Handle oversized block entities in chunks + + // Paper start - Anti-Xray - Add chunk packet info + @Deprecated @io.papermc.paper.annotation.DoNotUse public ClientboundLevelChunkPacketData(LevelChunk chunk) { this(chunk, null); } +@@ -50,8 +58,18 @@ public class ClientboundLevelChunkPacketData { + extractChunkData(new FriendlyByteBuf(this.getWriteBuffer()), chunk, chunkPacketInfo); + // Paper end + this.blockEntitiesData = Lists.newArrayList(); ++ int totalTileEntities = 0; // Paper - Handle oversized block entities in chunks + + for (Entry entry2 : chunk.getBlockEntities().entrySet()) { ++ // Paper start - Handle oversized block entities in chunks ++ if (++totalTileEntities > TE_LIMIT) { ++ var packet = entry2.getValue().getUpdatePacket(); ++ if (packet != null) { ++ this.extraPackets.add(packet); ++ continue; ++ } ++ } ++ // Paper end - Handle oversized block entities in chunks + this.blockEntitiesData.add(ClientboundLevelChunkPacketData.BlockEntityInfo.create(entry2.getValue())); + } + } +diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java +index 8ead66c134688b11dca15f6509147e726f182e6a..cfcac0fdc130120cb1f8d97c6353d93db7ddf81b 100644 +--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java ++++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java +@@ -83,4 +83,11 @@ public class ClientboundLevelChunkWithLightPacket implements Packet> getExtraPackets() { ++ return this.chunkData.getExtraPackets(); ++ } ++ // Paper end - Handle oversized block entities in chunks + } diff --git a/patches/server/1033-Handle-Oversized-block-entities-in-chunks.patch b/patches/server/1033-Handle-Oversized-block-entities-in-chunks.patch deleted file mode 100644 index b9ea4832bd..0000000000 --- a/patches/server/1033-Handle-Oversized-block-entities-in-chunks.patch +++ /dev/null @@ -1,64 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Aikar -Date: Wed, 6 May 2020 05:00:57 -0400 -Subject: [PATCH] Handle Oversized block entities in chunks - -Splits out Extra Packets if too many TE's are encountered to prevent -creating too large of a packet to sed. - -Co-authored-by: Spottedleaf - -diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java -index 05e29a703f022b5047262bae8caef33d9dfb9035..2501fbaf497d226051800c53d60a39bbc80db91c 100644 ---- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java -+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java -@@ -27,6 +27,14 @@ public class ClientboundLevelChunkPacketData { - private final CompoundTag heightmaps; - private final byte[] buffer; - private final List blockEntitiesData; -+ // Paper start - Handle oversized block entities in chunks -+ private final java.util.List> extraPackets = new java.util.ArrayList<>(); -+ private static final int TE_LIMIT = Integer.getInteger("Paper.excessiveTELimit", 750); -+ -+ public List> getExtraPackets() { -+ return this.extraPackets; -+ } -+ // Paper end - Handle oversized block entities in chunks - - // Paper start - Anti-Xray - Add chunk packet info - @Deprecated @io.papermc.paper.annotation.DoNotUse public ClientboundLevelChunkPacketData(LevelChunk chunk) { this(chunk, null); } -@@ -50,8 +58,18 @@ public class ClientboundLevelChunkPacketData { - extractChunkData(new FriendlyByteBuf(this.getWriteBuffer()), chunk, chunkPacketInfo); - // Paper end - this.blockEntitiesData = Lists.newArrayList(); -+ int totalTileEntities = 0; // Paper - Handle oversized block entities in chunks - - for (Entry entry2 : chunk.getBlockEntities().entrySet()) { -+ // Paper start - Handle oversized block entities in chunks -+ if (++totalTileEntities > TE_LIMIT) { -+ var packet = entry2.getValue().getUpdatePacket(); -+ if (packet != null) { -+ this.extraPackets.add(packet); -+ continue; -+ } -+ } -+ // Paper end - Handle oversized block entities in chunks - this.blockEntitiesData.add(ClientboundLevelChunkPacketData.BlockEntityInfo.create(entry2.getValue())); - } - } -diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java -index 8ead66c134688b11dca15f6509147e726f182e6a..cfcac0fdc130120cb1f8d97c6353d93db7ddf81b 100644 ---- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java -+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java -@@ -83,4 +83,11 @@ public class ClientboundLevelChunkWithLightPacket implements Packet> getExtraPackets() { -+ return this.chunkData.getExtraPackets(); -+ } -+ // Paper end - Handle oversized block entities in chunks - } diff --git a/patches/server/1033-Send-full-pos-packets-for-hard-colliding-entities.patch b/patches/server/1033-Send-full-pos-packets-for-hard-colliding-entities.patch new file mode 100644 index 0000000000..6a98c63b52 --- /dev/null +++ b/patches/server/1033-Send-full-pos-packets-for-hard-colliding-entities.patch @@ -0,0 +1,23 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Tue, 16 Feb 2021 00:16:56 -0800 +Subject: [PATCH] Send full pos packets for hard colliding entities + +Prevent collision problems due to desync (i.e boats) + +Configurable under +`send-full-pos-for-hard-colliding-entities` + +diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java +index 22eec853588ded2d255ab69d408f8e987832abe2..4f103f731623a8570238a6867fda1c5f83fca4e4 100644 +--- a/src/main/java/net/minecraft/server/level/ServerEntity.java ++++ b/src/main/java/net/minecraft/server/level/ServerEntity.java +@@ -183,7 +183,7 @@ public class ServerEntity { + long i1 = this.positionCodec.encodeZ(vec3d); + boolean flag6 = k < -32768L || k > 32767L || l < -32768L || l > 32767L || i1 < -32768L || i1 > 32767L; + +- if (!flag6 && this.teleportDelay <= 400 && !this.wasRiding && this.wasOnGround == this.entity.onGround()) { ++ if (!flag6 && this.teleportDelay <= 400 && !this.wasRiding && this.wasOnGround == this.entity.onGround()&& !(io.papermc.paper.configuration.GlobalConfiguration.get().collisions.sendFullPosForHardCollidingEntities && this.entity.hardCollides())) { // Paper - send full pos for hard colliding entities to prevent collision problems due to desync + if ((!flag2 || !flag3) && !(this.entity instanceof AbstractArrow)) { + if (flag2) { + packet1 = new ClientboundMoveEntityPacket.Pos(this.entity.getId(), (short) ((int) k), (short) ((int) l), (short) ((int) i1), this.entity.onGround()); diff --git a/patches/server/1034-Fix-and-optimise-world-force-upgrading.patch b/patches/server/1034-Fix-and-optimise-world-force-upgrading.patch new file mode 100644 index 0000000000..8afbafd466 --- /dev/null +++ b/patches/server/1034-Fix-and-optimise-world-force-upgrading.patch @@ -0,0 +1,395 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Spottedleaf +Date: Thu, 20 May 2021 07:02:22 -0700 +Subject: [PATCH] Fix and optimise world force upgrading + +The WorldUpgrader class was incorrectly modified by +CB. It will store an IChunkLoader instance for all +dimension types in the world, but obviously with how +CB shifts around worlds only one dimension type exists +per world. But this would be OK if CB did this +change correctly. All IChunkLoader instances +will point to the same regionfiles. And all +IChunkLoader instances are going to be read from. + +This problem hasn't really been reported because +it relies on the persistent legacy data to be converted +as well to cause corruption. Why? Because the legacy +data is also shared, it will result in different +outputs from conversion (as once conversion for legacy +persistent data takes place, it is REMOVED - so the next +convert will _not_ have the data). Which means different +sizes on disk. Which means different regionfile sector +allocations. Which means there are 3 different possible +regionfile sector allocations in memory, and none of them +are going to be correct. + +I've fixed this by writing a world upgrader suited to +CB's changes to world folder format. It was brain dead +easy to add threading, so I did. + +== AT == +public net.minecraft.util.worldupdate.WorldUpgrader REGEX + +diff --git a/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java b/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java +new file mode 100644 +index 0000000000000000000000000000000000000000..e049fbe4038aaea896f45b11ce9ce8f05922c898 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java +@@ -0,0 +1,222 @@ ++package io.papermc.paper.world; ++ ++import com.mojang.datafixers.DataFixer; ++import com.mojang.serialization.MapCodec; ++import net.minecraft.SharedConstants; ++import net.minecraft.core.RegistryAccess; ++import net.minecraft.core.registries.Registries; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.resources.ResourceKey; ++import net.minecraft.util.worldupdate.WorldUpgrader; ++import net.minecraft.world.level.ChunkPos; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.chunk.ChunkGenerator; ++import net.minecraft.world.level.chunk.storage.ChunkStorage; ++import net.minecraft.world.level.chunk.storage.RegionFileStorage; ++import net.minecraft.world.level.chunk.storage.RegionStorageInfo; ++import net.minecraft.world.level.dimension.LevelStem; ++import net.minecraft.world.level.storage.DimensionDataStorage; ++import net.minecraft.world.level.storage.LevelStorageSource; ++import org.apache.logging.log4j.LogManager; ++import org.apache.logging.log4j.Logger; ++import java.io.File; ++import java.io.IOException; ++import java.text.DecimalFormat; ++import java.util.Optional; ++import java.util.concurrent.ExecutorService; ++import java.util.concurrent.Executors; ++import java.util.concurrent.ThreadFactory; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.concurrent.atomic.AtomicLong; ++import java.util.function.Supplier; ++ ++public class ThreadedWorldUpgrader { ++ ++ private static final Logger LOGGER = LogManager.getLogger(); ++ ++ private final ResourceKey dimensionType; ++ private final String worldName; ++ private final File worldDir; ++ private final ExecutorService threadPool; ++ private final DataFixer dataFixer; ++ private final RegistryAccess registryLookup; ++ private final Optional>> generatorKey; ++ private final boolean removeCaches; ++ private final boolean recreateRegionFiles; // TODO ++ ++ public ThreadedWorldUpgrader(final ResourceKey dimensionType, final String worldName, final File worldDir, final int threads, ++ final DataFixer dataFixer, final RegistryAccess registryLookup, final Optional>> generatorKey, ++ final boolean removeCaches, final boolean recreateRegionFiles) { ++ this.dimensionType = dimensionType; ++ this.worldName = worldName; ++ this.worldDir = worldDir; ++ this.threadPool = Executors.newFixedThreadPool(Math.max(1, threads), new ThreadFactory() { ++ private final AtomicInteger threadCounter = new AtomicInteger(); ++ ++ @Override ++ public Thread newThread(final Runnable run) { ++ final Thread ret = new Thread(run); ++ ++ ret.setName("World upgrader thread for world " + ThreadedWorldUpgrader.this.worldName + " #" + this.threadCounter.getAndIncrement()); ++ ret.setUncaughtExceptionHandler((thread, throwable) -> { ++ LOGGER.fatal("Error upgrading world", throwable); ++ }); ++ ++ return ret; ++ } ++ }); ++ this.dataFixer = dataFixer; ++ this.registryLookup = registryLookup; ++ this.generatorKey = generatorKey; ++ this.removeCaches = removeCaches; ++ this.recreateRegionFiles = recreateRegionFiles; ++ } ++ ++ public void convert() { ++ final File worldFolder = LevelStorageSource.getStorageFolder(this.worldDir.toPath(), this.dimensionType).toFile(); ++ final DimensionDataStorage worldPersistentData = new DimensionDataStorage(new File(worldFolder, "data"), this.dataFixer, this.registryLookup); ++ ++ final File regionFolder = new File(worldFolder, "region"); ++ ++ LOGGER.info("Force upgrading {}", this.worldName); ++ LOGGER.info("Counting regionfiles for {}", this.worldName); ++ final File[] regionFiles = regionFolder.listFiles((final File dir, final String name) -> { ++ return WorldUpgrader.REGEX.matcher(name).matches(); ++ }); ++ if (regionFiles == null) { ++ LOGGER.info("Found no regionfiles to convert for world {}", this.worldName); ++ return; ++ } ++ LOGGER.info("Found {} regionfiles to convert", regionFiles.length); ++ LOGGER.info("Starting conversion now for world {}", this.worldName); ++ ++ // Only used for profiling, let's fill it anyways just in case ++ final RegionStorageInfo storageInfo = new RegionStorageInfo( ++ this.worldName, ++ ResourceKey.create(Registries.DIMENSION, this.dimensionType.location()), ++ "region" ++ ); ++ ++ final WorldInfo info = new WorldInfo(() -> worldPersistentData, ++ new ChunkStorage(storageInfo, regionFolder.toPath(), this.dataFixer, false), this.removeCaches, this.dimensionType, this.generatorKey); ++ ++ long expectedChunks = (long)regionFiles.length * (32L * 32L); ++ ++ for (final File regionFile : regionFiles) { ++ final ChunkPos regionPos = RegionFileStorage.getRegionFileCoordinates(regionFile.toPath()); ++ if (regionPos == null) { ++ expectedChunks -= (32L * 32L); ++ continue; ++ } ++ ++ this.threadPool.execute(new ConvertTask(info, regionPos.x >> 5, regionPos.z >> 5)); ++ } ++ this.threadPool.shutdown(); ++ ++ final DecimalFormat format = new DecimalFormat("#0.00"); ++ ++ final long start = System.nanoTime(); ++ ++ while (!this.threadPool.isTerminated()) { ++ final long current = info.convertedChunks.get(); ++ ++ LOGGER.info("{}% completed ({} / {} chunks)...", format.format((double)current / (double)expectedChunks * 100.0), current, expectedChunks); ++ ++ try { ++ Thread.sleep(1000L); ++ } catch (final InterruptedException ignore) {} ++ } ++ ++ final long end = System.nanoTime(); ++ ++ try { ++ info.loader.close(); ++ } catch (final IOException ex) { ++ LOGGER.fatal("Failed to close chunk loader", ex); ++ } ++ LOGGER.info("Completed conversion. Took {}s, {} out of {} chunks needed to be converted/modified ({}%)", ++ (int)Math.ceil((end - start) * 1.0e-9), info.modifiedChunks.get(), expectedChunks, format.format((double)info.modifiedChunks.get() / (double)expectedChunks * 100.0)); ++ } ++ ++ private static final class WorldInfo { ++ ++ public final Supplier persistentDataSupplier; ++ public final ChunkStorage loader; ++ public final boolean removeCaches; ++ public final ResourceKey worldKey; ++ public final Optional>> generatorKey; ++ public final AtomicLong convertedChunks = new AtomicLong(); ++ public final AtomicLong modifiedChunks = new AtomicLong(); ++ ++ private WorldInfo(final Supplier persistentDataSupplier, final ChunkStorage loader, final boolean removeCaches, ++ final ResourceKey worldKey, Optional>> generatorKey) { ++ this.persistentDataSupplier = persistentDataSupplier; ++ this.loader = loader; ++ this.removeCaches = removeCaches; ++ this.worldKey = worldKey; ++ this.generatorKey = generatorKey; ++ } ++ } ++ ++ private static final class ConvertTask implements Runnable { ++ ++ private final WorldInfo worldInfo; ++ private final int regionX; ++ private final int regionZ; ++ ++ public ConvertTask(final WorldInfo worldInfo, final int regionX, final int regionZ) { ++ this.worldInfo = worldInfo; ++ this.regionX = regionX; ++ this.regionZ = regionZ; ++ } ++ ++ @Override ++ public void run() { ++ final int regionCX = this.regionX << 5; ++ final int regionCZ = this.regionZ << 5; ++ ++ final Supplier persistentDataSupplier = this.worldInfo.persistentDataSupplier; ++ final ChunkStorage loader = this.worldInfo.loader; ++ final boolean removeCaches = this.worldInfo.removeCaches; ++ final ResourceKey worldKey = this.worldInfo.worldKey; ++ ++ for (int cz = regionCZ; cz < (regionCZ + 32); ++cz) { ++ for (int cx = regionCX; cx < (regionCX + 32); ++cx) { ++ final ChunkPos chunkPos = new ChunkPos(cx, cz); ++ try { ++ // no need to check the coordinate of the chunk, the regionfilecache does that for us ++ ++ CompoundTag chunkNBT = (loader.read(chunkPos).join()).orElse(null); ++ ++ if (chunkNBT == null) { ++ continue; ++ } ++ ++ final int versionBefore = ChunkStorage.getVersion(chunkNBT); ++ ++ chunkNBT = loader.upgradeChunkTag(worldKey, persistentDataSupplier, chunkNBT, this.worldInfo.generatorKey, chunkPos, null); ++ ++ boolean modified = versionBefore < SharedConstants.getCurrentVersion().getDataVersion().getVersion(); ++ ++ if (removeCaches) { ++ final CompoundTag level = chunkNBT.getCompound("Level"); ++ modified |= level.contains("Heightmaps"); ++ level.remove("Heightmaps"); ++ modified |= level.contains("isLightOn"); ++ level.remove("isLightOn"); ++ } ++ ++ if (modified) { ++ this.worldInfo.modifiedChunks.getAndIncrement(); ++ loader.write(chunkPos, chunkNBT); ++ } ++ } catch (final Exception ex) { ++ LOGGER.error("Error upgrading chunk {}", chunkPos, ex); ++ } finally { ++ this.worldInfo.convertedChunks.getAndIncrement(); ++ } ++ } ++ } ++ } ++ } ++} +diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java +index 244a19ecd0234fa1d7a6ecfea20751595688605d..5443013060b62e3bfcc51cddca96d1c0bc59fe72 100644 +--- a/src/main/java/net/minecraft/server/Main.java ++++ b/src/main/java/net/minecraft/server/Main.java +@@ -392,6 +392,15 @@ public class Main { + return new WorldLoader.InitConfig(worldloader_d, Commands.CommandSelection.DEDICATED, serverPropertiesHandler.functionPermissionLevel); + } + ++ // Paper start - fix and optimise world upgrading ++ public static void convertWorldButItWorks(net.minecraft.resources.ResourceKey dimensionType, net.minecraft.world.level.storage.LevelStorageSource.LevelStorageAccess worldSession, ++ DataFixer dataFixer, RegistryAccess registryLookup, Optional>> generatorKey, boolean removeCaches, boolean recreateRegionFiles) { ++ int threads = Runtime.getRuntime().availableProcessors() * 3 / 8; ++ final io.papermc.paper.world.ThreadedWorldUpgrader worldUpgrader = new io.papermc.paper.world.ThreadedWorldUpgrader(dimensionType, worldSession.getLevelId(), worldSession.levelDirectory.path().toFile(), threads, dataFixer, registryLookup, generatorKey, removeCaches, recreateRegionFiles); ++ worldUpgrader.convert(); ++ } ++ // Paper end - fix and optimise world upgrading ++ + public static void forceUpgrade(LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, boolean eraseCache, BooleanSupplier continueCheck, RegistryAccess dynamicRegistryManager, boolean recreateRegionFiles) { + Main.LOGGER.info("Forcing world upgrade! {}", session.getLevelId()); // CraftBukkit + WorldUpgrader worldupgrader = new WorldUpgrader(session, dataFixer, dynamicRegistryManager, eraseCache, recreateRegionFiles); +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 40551f2bb4e5511d1feefe87244905f1ee84b511..1c19ee8106e7a08ab1043376ffe6d8e40d3d8f76 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -599,11 +599,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { +- return true; +- }, iregistrycustom_dimension, this.options.has("recreateRegionFiles")); +- } ++ // Paper - fix and optimise world upgrading; move down + + PrimaryLevelData iworlddataserver = worlddata; + boolean flag = worlddata.isDebugWorld(); +@@ -618,6 +614,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop worldKey = ResourceKey.create(Registries.DIMENSION, dimensionKey.location()); + + if (dimensionKey == LevelStem.OVERWORLD) { +diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java +index 0a8eeebb2d702ebcefd9f26cc0f41d1eab497902..b4ef3ad2c17168085372f1fe46809f02d9dfe74a 100644 +--- a/src/main/java/net/minecraft/world/level/Level.java ++++ b/src/main/java/net/minecraft/world/level/Level.java +@@ -178,6 +178,15 @@ public abstract class Level implements LevelAccessor, AutoCloseable { + public final Map explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions + public java.util.ArrayDeque redstoneUpdateInfos; // Paper - Faster redstone torch rapid clock removal; Move from Map in BlockRedstoneTorch to here + ++ // Paper start - fix and optimise world upgrading ++ // copied from below ++ public static ResourceKey getDimensionKey(DimensionType manager) { ++ return ((org.bukkit.craftbukkit.CraftServer)org.bukkit.Bukkit.getServer()).getHandle().getServer().registryAccess().registryOrThrow(net.minecraft.core.registries.Registries.DIMENSION_TYPE).getResourceKey(manager).orElseThrow(() -> { ++ return new IllegalStateException("Unregistered dimension type: " + manager); ++ }); ++ } ++ // Paper end - fix and optimise world upgrading ++ + public CraftWorld getWorld() { + return this.world; + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +index 249705ec1b8b692ef1d7fec34a04918afe6486bc..f6e3b745fc417354380d4a969f83aee430bad785 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java ++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +@@ -69,6 +69,29 @@ public class RegionFileStorage implements AutoCloseable { + } + + // Paper start ++ @Nullable ++ public static ChunkPos getRegionFileCoordinates(Path file) { ++ String fileName = file.getFileName().toString(); ++ 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 ChunkPos(x << 5, z << 5); ++ } catch (NumberFormatException ex) { ++ return null; ++ } ++ } ++ + public synchronized RegionFile getRegionFileIfLoaded(ChunkPos chunkcoordintpair) { + return this.regionCache.getAndMoveToFirst(ChunkPos.asLong(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ())); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index ac9873c430a3ece5c4b5f5305496639a54ce35f5..0a27ee956a623f6af3cd0f7d9dc1393fb2526412 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -1366,9 +1366,7 @@ public final class CraftServer implements Server { + worlddata.checkName(name); + worlddata.setModdedInfo(this.console.getServerModName(), this.console.getModdedStatus().shouldReportAsModified()); + +- if (this.console.options.has("forceUpgrade")) { +- net.minecraft.server.Main.forceUpgrade(worldSession, DataFixers.getDataFixer(), this.console.options.has("eraseCache"), () -> true, iregistrycustom_dimension, this.console.options.has("recreateRegionFiles")); +- } ++ // Paper - fix and optimise world upgrading; move down + + long j = BiomeManager.obfuscateSeed(worlddata.worldGenOptions().seed()); // Paper - use world seed + List list = ImmutableList.of(new PhantomSpawner(), new PatrolSpawner(), new CatSpawner(), new VillageSiege(), new WanderingTraderSpawner(worlddata)); +@@ -1379,6 +1377,13 @@ public final class CraftServer implements Server { + biomeProvider = generator.getDefaultBiomeProvider(worldInfo); + } + ++ // Paper start - fix and optimise world upgrading ++ if (this.console.options.has("forceUpgrade")) { ++ net.minecraft.server.Main.convertWorldButItWorks( ++ actualDimension, worldSession, DataFixers.getDataFixer(), iregistrycustom_dimension, worlddimension.generator().getTypeNameForDataFixer(), this.console.options.has("eraseCache"), this.console.options.has("recreateRegionFiles") ++ ); ++ } ++ // Paper end - fix and optimise world upgrading + ResourceKey worldKey; + String levelName = this.getServer().getProperties().levelName; + if (name.equals(levelName + "_nether")) { diff --git a/patches/server/1034-Send-full-pos-packets-for-hard-colliding-entities.patch b/patches/server/1034-Send-full-pos-packets-for-hard-colliding-entities.patch deleted file mode 100644 index 6a98c63b52..0000000000 --- a/patches/server/1034-Send-full-pos-packets-for-hard-colliding-entities.patch +++ /dev/null @@ -1,23 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Tue, 16 Feb 2021 00:16:56 -0800 -Subject: [PATCH] Send full pos packets for hard colliding entities - -Prevent collision problems due to desync (i.e boats) - -Configurable under -`send-full-pos-for-hard-colliding-entities` - -diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java -index 22eec853588ded2d255ab69d408f8e987832abe2..4f103f731623a8570238a6867fda1c5f83fca4e4 100644 ---- a/src/main/java/net/minecraft/server/level/ServerEntity.java -+++ b/src/main/java/net/minecraft/server/level/ServerEntity.java -@@ -183,7 +183,7 @@ public class ServerEntity { - long i1 = this.positionCodec.encodeZ(vec3d); - boolean flag6 = k < -32768L || k > 32767L || l < -32768L || l > 32767L || i1 < -32768L || i1 > 32767L; - -- if (!flag6 && this.teleportDelay <= 400 && !this.wasRiding && this.wasOnGround == this.entity.onGround()) { -+ if (!flag6 && this.teleportDelay <= 400 && !this.wasRiding && this.wasOnGround == this.entity.onGround()&& !(io.papermc.paper.configuration.GlobalConfiguration.get().collisions.sendFullPosForHardCollidingEntities && this.entity.hardCollides())) { // Paper - send full pos for hard colliding entities to prevent collision problems due to desync - if ((!flag2 || !flag3) && !(this.entity instanceof AbstractArrow)) { - if (flag2) { - packet1 = new ClientboundMoveEntityPacket.Pos(this.entity.getId(), (short) ((int) k), (short) ((int) l), (short) ((int) i1), this.entity.onGround()); diff --git a/patches/server/1035-API-for-checking-sent-chunks.patch b/patches/server/1035-API-for-checking-sent-chunks.patch new file mode 100644 index 0000000000..6187f99887 --- /dev/null +++ b/patches/server/1035-API-for-checking-sent-chunks.patch @@ -0,0 +1,62 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Flo0 +Date: Mon, 8 Apr 2024 16:43:16 +0200 +Subject: [PATCH] API for checking sent chunks + + +diff --git a/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java +index ee58c67cb2bd78159cce19ec75f13dc6168a0e7a..149cfb0587299f72fcfddf395fb71b70438986c1 100644 +--- a/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java ++++ b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java +@@ -1048,6 +1048,11 @@ public class RegionizedPlayerChunkLoader { + + // now all tickets should be removed, which is all of our external state + } ++ ++ // For external checks ++ public it.unimi.dsi.fastutil.longs.LongOpenHashSet getSentChunksRaw() { ++ return this.sentChunks; ++ } + } + + // TODO rebase into util patch +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 33c553d1558b3cdfac22948f3878e1a31d70f02b..eddbbd0e9be3cb81d1030c0c9da829b9193ebc16 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -3449,6 +3449,35 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + } + // Paper end + ++ // Paper start - Add chunk view API ++ @Override ++ public Set getSentChunkKeys() { ++ org.spigotmc.AsyncCatcher.catchOp("accessing sent chunks"); ++ return it.unimi.dsi.fastutil.longs.LongSets.unmodifiable( ++ this.getHandle().chunkLoader.getSentChunksRaw().clone() ++ ); ++ } ++ ++ @Override ++ public Set getSentChunks() { ++ org.spigotmc.AsyncCatcher.catchOp("accessing sent chunks"); ++ final it.unimi.dsi.fastutil.longs.LongOpenHashSet rawChunkKeys = this.getHandle().chunkLoader.getSentChunksRaw(); ++ final it.unimi.dsi.fastutil.objects.ObjectOpenHashSet chunks = new it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<>(rawChunkKeys.size()); ++ final org.bukkit.World world = this.getWorld(); ++ ++ final it.unimi.dsi.fastutil.longs.LongIterator iter = this.getHandle().chunkLoader.getSentChunksRaw().longIterator(); ++ while (iter.hasNext()) chunks.add(world.getChunkAt(iter.nextLong(), false)); ++ ++ return it.unimi.dsi.fastutil.objects.ObjectSets.unmodifiable(chunks); ++ } ++ ++ @Override ++ public boolean isChunkSent(final long chunkKey) { ++ org.spigotmc.AsyncCatcher.catchOp("accessing sent chunks"); ++ return this.getHandle().chunkLoader.getSentChunksRaw().contains(chunkKey); ++ } ++ // Paper end ++ + public Player.Spigot spigot() + { + return this.spigot; diff --git a/patches/server/1035-Fix-and-optimise-world-force-upgrading.patch b/patches/server/1035-Fix-and-optimise-world-force-upgrading.patch deleted file mode 100644 index 8afbafd466..0000000000 --- a/patches/server/1035-Fix-and-optimise-world-force-upgrading.patch +++ /dev/null @@ -1,395 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Thu, 20 May 2021 07:02:22 -0700 -Subject: [PATCH] Fix and optimise world force upgrading - -The WorldUpgrader class was incorrectly modified by -CB. It will store an IChunkLoader instance for all -dimension types in the world, but obviously with how -CB shifts around worlds only one dimension type exists -per world. But this would be OK if CB did this -change correctly. All IChunkLoader instances -will point to the same regionfiles. And all -IChunkLoader instances are going to be read from. - -This problem hasn't really been reported because -it relies on the persistent legacy data to be converted -as well to cause corruption. Why? Because the legacy -data is also shared, it will result in different -outputs from conversion (as once conversion for legacy -persistent data takes place, it is REMOVED - so the next -convert will _not_ have the data). Which means different -sizes on disk. Which means different regionfile sector -allocations. Which means there are 3 different possible -regionfile sector allocations in memory, and none of them -are going to be correct. - -I've fixed this by writing a world upgrader suited to -CB's changes to world folder format. It was brain dead -easy to add threading, so I did. - -== AT == -public net.minecraft.util.worldupdate.WorldUpgrader REGEX - -diff --git a/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java b/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e049fbe4038aaea896f45b11ce9ce8f05922c898 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java -@@ -0,0 +1,222 @@ -+package io.papermc.paper.world; -+ -+import com.mojang.datafixers.DataFixer; -+import com.mojang.serialization.MapCodec; -+import net.minecraft.SharedConstants; -+import net.minecraft.core.RegistryAccess; -+import net.minecraft.core.registries.Registries; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.resources.ResourceKey; -+import net.minecraft.util.worldupdate.WorldUpgrader; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.chunk.ChunkGenerator; -+import net.minecraft.world.level.chunk.storage.ChunkStorage; -+import net.minecraft.world.level.chunk.storage.RegionFileStorage; -+import net.minecraft.world.level.chunk.storage.RegionStorageInfo; -+import net.minecraft.world.level.dimension.LevelStem; -+import net.minecraft.world.level.storage.DimensionDataStorage; -+import net.minecraft.world.level.storage.LevelStorageSource; -+import org.apache.logging.log4j.LogManager; -+import org.apache.logging.log4j.Logger; -+import java.io.File; -+import java.io.IOException; -+import java.text.DecimalFormat; -+import java.util.Optional; -+import java.util.concurrent.ExecutorService; -+import java.util.concurrent.Executors; -+import java.util.concurrent.ThreadFactory; -+import java.util.concurrent.atomic.AtomicInteger; -+import java.util.concurrent.atomic.AtomicLong; -+import java.util.function.Supplier; -+ -+public class ThreadedWorldUpgrader { -+ -+ private static final Logger LOGGER = LogManager.getLogger(); -+ -+ private final ResourceKey dimensionType; -+ private final String worldName; -+ private final File worldDir; -+ private final ExecutorService threadPool; -+ private final DataFixer dataFixer; -+ private final RegistryAccess registryLookup; -+ private final Optional>> generatorKey; -+ private final boolean removeCaches; -+ private final boolean recreateRegionFiles; // TODO -+ -+ public ThreadedWorldUpgrader(final ResourceKey dimensionType, final String worldName, final File worldDir, final int threads, -+ final DataFixer dataFixer, final RegistryAccess registryLookup, final Optional>> generatorKey, -+ final boolean removeCaches, final boolean recreateRegionFiles) { -+ this.dimensionType = dimensionType; -+ this.worldName = worldName; -+ this.worldDir = worldDir; -+ this.threadPool = Executors.newFixedThreadPool(Math.max(1, threads), new ThreadFactory() { -+ private final AtomicInteger threadCounter = new AtomicInteger(); -+ -+ @Override -+ public Thread newThread(final Runnable run) { -+ final Thread ret = new Thread(run); -+ -+ ret.setName("World upgrader thread for world " + ThreadedWorldUpgrader.this.worldName + " #" + this.threadCounter.getAndIncrement()); -+ ret.setUncaughtExceptionHandler((thread, throwable) -> { -+ LOGGER.fatal("Error upgrading world", throwable); -+ }); -+ -+ return ret; -+ } -+ }); -+ this.dataFixer = dataFixer; -+ this.registryLookup = registryLookup; -+ this.generatorKey = generatorKey; -+ this.removeCaches = removeCaches; -+ this.recreateRegionFiles = recreateRegionFiles; -+ } -+ -+ public void convert() { -+ final File worldFolder = LevelStorageSource.getStorageFolder(this.worldDir.toPath(), this.dimensionType).toFile(); -+ final DimensionDataStorage worldPersistentData = new DimensionDataStorage(new File(worldFolder, "data"), this.dataFixer, this.registryLookup); -+ -+ final File regionFolder = new File(worldFolder, "region"); -+ -+ LOGGER.info("Force upgrading {}", this.worldName); -+ LOGGER.info("Counting regionfiles for {}", this.worldName); -+ final File[] regionFiles = regionFolder.listFiles((final File dir, final String name) -> { -+ return WorldUpgrader.REGEX.matcher(name).matches(); -+ }); -+ if (regionFiles == null) { -+ LOGGER.info("Found no regionfiles to convert for world {}", this.worldName); -+ return; -+ } -+ LOGGER.info("Found {} regionfiles to convert", regionFiles.length); -+ LOGGER.info("Starting conversion now for world {}", this.worldName); -+ -+ // Only used for profiling, let's fill it anyways just in case -+ final RegionStorageInfo storageInfo = new RegionStorageInfo( -+ this.worldName, -+ ResourceKey.create(Registries.DIMENSION, this.dimensionType.location()), -+ "region" -+ ); -+ -+ final WorldInfo info = new WorldInfo(() -> worldPersistentData, -+ new ChunkStorage(storageInfo, regionFolder.toPath(), this.dataFixer, false), this.removeCaches, this.dimensionType, this.generatorKey); -+ -+ long expectedChunks = (long)regionFiles.length * (32L * 32L); -+ -+ for (final File regionFile : regionFiles) { -+ final ChunkPos regionPos = RegionFileStorage.getRegionFileCoordinates(regionFile.toPath()); -+ if (regionPos == null) { -+ expectedChunks -= (32L * 32L); -+ continue; -+ } -+ -+ this.threadPool.execute(new ConvertTask(info, regionPos.x >> 5, regionPos.z >> 5)); -+ } -+ this.threadPool.shutdown(); -+ -+ final DecimalFormat format = new DecimalFormat("#0.00"); -+ -+ final long start = System.nanoTime(); -+ -+ while (!this.threadPool.isTerminated()) { -+ final long current = info.convertedChunks.get(); -+ -+ LOGGER.info("{}% completed ({} / {} chunks)...", format.format((double)current / (double)expectedChunks * 100.0), current, expectedChunks); -+ -+ try { -+ Thread.sleep(1000L); -+ } catch (final InterruptedException ignore) {} -+ } -+ -+ final long end = System.nanoTime(); -+ -+ try { -+ info.loader.close(); -+ } catch (final IOException ex) { -+ LOGGER.fatal("Failed to close chunk loader", ex); -+ } -+ LOGGER.info("Completed conversion. Took {}s, {} out of {} chunks needed to be converted/modified ({}%)", -+ (int)Math.ceil((end - start) * 1.0e-9), info.modifiedChunks.get(), expectedChunks, format.format((double)info.modifiedChunks.get() / (double)expectedChunks * 100.0)); -+ } -+ -+ private static final class WorldInfo { -+ -+ public final Supplier persistentDataSupplier; -+ public final ChunkStorage loader; -+ public final boolean removeCaches; -+ public final ResourceKey worldKey; -+ public final Optional>> generatorKey; -+ public final AtomicLong convertedChunks = new AtomicLong(); -+ public final AtomicLong modifiedChunks = new AtomicLong(); -+ -+ private WorldInfo(final Supplier persistentDataSupplier, final ChunkStorage loader, final boolean removeCaches, -+ final ResourceKey worldKey, Optional>> generatorKey) { -+ this.persistentDataSupplier = persistentDataSupplier; -+ this.loader = loader; -+ this.removeCaches = removeCaches; -+ this.worldKey = worldKey; -+ this.generatorKey = generatorKey; -+ } -+ } -+ -+ private static final class ConvertTask implements Runnable { -+ -+ private final WorldInfo worldInfo; -+ private final int regionX; -+ private final int regionZ; -+ -+ public ConvertTask(final WorldInfo worldInfo, final int regionX, final int regionZ) { -+ this.worldInfo = worldInfo; -+ this.regionX = regionX; -+ this.regionZ = regionZ; -+ } -+ -+ @Override -+ public void run() { -+ final int regionCX = this.regionX << 5; -+ final int regionCZ = this.regionZ << 5; -+ -+ final Supplier persistentDataSupplier = this.worldInfo.persistentDataSupplier; -+ final ChunkStorage loader = this.worldInfo.loader; -+ final boolean removeCaches = this.worldInfo.removeCaches; -+ final ResourceKey worldKey = this.worldInfo.worldKey; -+ -+ for (int cz = regionCZ; cz < (regionCZ + 32); ++cz) { -+ for (int cx = regionCX; cx < (regionCX + 32); ++cx) { -+ final ChunkPos chunkPos = new ChunkPos(cx, cz); -+ try { -+ // no need to check the coordinate of the chunk, the regionfilecache does that for us -+ -+ CompoundTag chunkNBT = (loader.read(chunkPos).join()).orElse(null); -+ -+ if (chunkNBT == null) { -+ continue; -+ } -+ -+ final int versionBefore = ChunkStorage.getVersion(chunkNBT); -+ -+ chunkNBT = loader.upgradeChunkTag(worldKey, persistentDataSupplier, chunkNBT, this.worldInfo.generatorKey, chunkPos, null); -+ -+ boolean modified = versionBefore < SharedConstants.getCurrentVersion().getDataVersion().getVersion(); -+ -+ if (removeCaches) { -+ final CompoundTag level = chunkNBT.getCompound("Level"); -+ modified |= level.contains("Heightmaps"); -+ level.remove("Heightmaps"); -+ modified |= level.contains("isLightOn"); -+ level.remove("isLightOn"); -+ } -+ -+ if (modified) { -+ this.worldInfo.modifiedChunks.getAndIncrement(); -+ loader.write(chunkPos, chunkNBT); -+ } -+ } catch (final Exception ex) { -+ LOGGER.error("Error upgrading chunk {}", chunkPos, ex); -+ } finally { -+ this.worldInfo.convertedChunks.getAndIncrement(); -+ } -+ } -+ } -+ } -+ } -+} -diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java -index 244a19ecd0234fa1d7a6ecfea20751595688605d..5443013060b62e3bfcc51cddca96d1c0bc59fe72 100644 ---- a/src/main/java/net/minecraft/server/Main.java -+++ b/src/main/java/net/minecraft/server/Main.java -@@ -392,6 +392,15 @@ public class Main { - return new WorldLoader.InitConfig(worldloader_d, Commands.CommandSelection.DEDICATED, serverPropertiesHandler.functionPermissionLevel); - } - -+ // Paper start - fix and optimise world upgrading -+ public static void convertWorldButItWorks(net.minecraft.resources.ResourceKey dimensionType, net.minecraft.world.level.storage.LevelStorageSource.LevelStorageAccess worldSession, -+ DataFixer dataFixer, RegistryAccess registryLookup, Optional>> generatorKey, boolean removeCaches, boolean recreateRegionFiles) { -+ int threads = Runtime.getRuntime().availableProcessors() * 3 / 8; -+ final io.papermc.paper.world.ThreadedWorldUpgrader worldUpgrader = new io.papermc.paper.world.ThreadedWorldUpgrader(dimensionType, worldSession.getLevelId(), worldSession.levelDirectory.path().toFile(), threads, dataFixer, registryLookup, generatorKey, removeCaches, recreateRegionFiles); -+ worldUpgrader.convert(); -+ } -+ // Paper end - fix and optimise world upgrading -+ - public static void forceUpgrade(LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, boolean eraseCache, BooleanSupplier continueCheck, RegistryAccess dynamicRegistryManager, boolean recreateRegionFiles) { - Main.LOGGER.info("Forcing world upgrade! {}", session.getLevelId()); // CraftBukkit - WorldUpgrader worldupgrader = new WorldUpgrader(session, dataFixer, dynamicRegistryManager, eraseCache, recreateRegionFiles); -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 40551f2bb4e5511d1feefe87244905f1ee84b511..1c19ee8106e7a08ab1043376ffe6d8e40d3d8f76 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -599,11 +599,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { -- return true; -- }, iregistrycustom_dimension, this.options.has("recreateRegionFiles")); -- } -+ // Paper - fix and optimise world upgrading; move down - - PrimaryLevelData iworlddataserver = worlddata; - boolean flag = worlddata.isDebugWorld(); -@@ -618,6 +614,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop worldKey = ResourceKey.create(Registries.DIMENSION, dimensionKey.location()); - - if (dimensionKey == LevelStem.OVERWORLD) { -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index 0a8eeebb2d702ebcefd9f26cc0f41d1eab497902..b4ef3ad2c17168085372f1fe46809f02d9dfe74a 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -178,6 +178,15 @@ public abstract class Level implements LevelAccessor, AutoCloseable { - public final Map explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions - public java.util.ArrayDeque redstoneUpdateInfos; // Paper - Faster redstone torch rapid clock removal; Move from Map in BlockRedstoneTorch to here - -+ // Paper start - fix and optimise world upgrading -+ // copied from below -+ public static ResourceKey getDimensionKey(DimensionType manager) { -+ return ((org.bukkit.craftbukkit.CraftServer)org.bukkit.Bukkit.getServer()).getHandle().getServer().registryAccess().registryOrThrow(net.minecraft.core.registries.Registries.DIMENSION_TYPE).getResourceKey(manager).orElseThrow(() -> { -+ return new IllegalStateException("Unregistered dimension type: " + manager); -+ }); -+ } -+ // Paper end - fix and optimise world upgrading -+ - public CraftWorld getWorld() { - return this.world; - } -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -index 249705ec1b8b692ef1d7fec34a04918afe6486bc..f6e3b745fc417354380d4a969f83aee430bad785 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -@@ -69,6 +69,29 @@ public class RegionFileStorage implements AutoCloseable { - } - - // Paper start -+ @Nullable -+ public static ChunkPos getRegionFileCoordinates(Path file) { -+ String fileName = file.getFileName().toString(); -+ 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 ChunkPos(x << 5, z << 5); -+ } catch (NumberFormatException ex) { -+ return null; -+ } -+ } -+ - public synchronized RegionFile getRegionFileIfLoaded(ChunkPos chunkcoordintpair) { - return this.regionCache.getAndMoveToFirst(ChunkPos.asLong(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ())); - } -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index ac9873c430a3ece5c4b5f5305496639a54ce35f5..0a27ee956a623f6af3cd0f7d9dc1393fb2526412 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -@@ -1366,9 +1366,7 @@ public final class CraftServer implements Server { - worlddata.checkName(name); - worlddata.setModdedInfo(this.console.getServerModName(), this.console.getModdedStatus().shouldReportAsModified()); - -- if (this.console.options.has("forceUpgrade")) { -- net.minecraft.server.Main.forceUpgrade(worldSession, DataFixers.getDataFixer(), this.console.options.has("eraseCache"), () -> true, iregistrycustom_dimension, this.console.options.has("recreateRegionFiles")); -- } -+ // Paper - fix and optimise world upgrading; move down - - long j = BiomeManager.obfuscateSeed(worlddata.worldGenOptions().seed()); // Paper - use world seed - List list = ImmutableList.of(new PhantomSpawner(), new PatrolSpawner(), new CatSpawner(), new VillageSiege(), new WanderingTraderSpawner(worlddata)); -@@ -1379,6 +1377,13 @@ public final class CraftServer implements Server { - biomeProvider = generator.getDefaultBiomeProvider(worldInfo); - } - -+ // Paper start - fix and optimise world upgrading -+ if (this.console.options.has("forceUpgrade")) { -+ net.minecraft.server.Main.convertWorldButItWorks( -+ actualDimension, worldSession, DataFixers.getDataFixer(), iregistrycustom_dimension, worlddimension.generator().getTypeNameForDataFixer(), this.console.options.has("eraseCache"), this.console.options.has("recreateRegionFiles") -+ ); -+ } -+ // Paper end - fix and optimise world upgrading - ResourceKey worldKey; - String levelName = this.getServer().getProperties().levelName; - if (name.equals(levelName + "_nether")) { diff --git a/patches/server/1036-API-for-checking-sent-chunks.patch b/patches/server/1036-API-for-checking-sent-chunks.patch deleted file mode 100644 index 6187f99887..0000000000 --- a/patches/server/1036-API-for-checking-sent-chunks.patch +++ /dev/null @@ -1,62 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Flo0 -Date: Mon, 8 Apr 2024 16:43:16 +0200 -Subject: [PATCH] API for checking sent chunks - - -diff --git a/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java -index ee58c67cb2bd78159cce19ec75f13dc6168a0e7a..149cfb0587299f72fcfddf395fb71b70438986c1 100644 ---- a/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java -+++ b/src/main/java/io/papermc/paper/chunk/system/RegionizedPlayerChunkLoader.java -@@ -1048,6 +1048,11 @@ public class RegionizedPlayerChunkLoader { - - // now all tickets should be removed, which is all of our external state - } -+ -+ // For external checks -+ public it.unimi.dsi.fastutil.longs.LongOpenHashSet getSentChunksRaw() { -+ return this.sentChunks; -+ } - } - - // TODO rebase into util patch -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index 33c553d1558b3cdfac22948f3878e1a31d70f02b..eddbbd0e9be3cb81d1030c0c9da829b9193ebc16 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -@@ -3449,6 +3449,35 @@ public class CraftPlayer extends CraftHumanEntity implements Player { - } - // Paper end - -+ // Paper start - Add chunk view API -+ @Override -+ public Set getSentChunkKeys() { -+ org.spigotmc.AsyncCatcher.catchOp("accessing sent chunks"); -+ return it.unimi.dsi.fastutil.longs.LongSets.unmodifiable( -+ this.getHandle().chunkLoader.getSentChunksRaw().clone() -+ ); -+ } -+ -+ @Override -+ public Set getSentChunks() { -+ org.spigotmc.AsyncCatcher.catchOp("accessing sent chunks"); -+ final it.unimi.dsi.fastutil.longs.LongOpenHashSet rawChunkKeys = this.getHandle().chunkLoader.getSentChunksRaw(); -+ final it.unimi.dsi.fastutil.objects.ObjectOpenHashSet chunks = new it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<>(rawChunkKeys.size()); -+ final org.bukkit.World world = this.getWorld(); -+ -+ final it.unimi.dsi.fastutil.longs.LongIterator iter = this.getHandle().chunkLoader.getSentChunksRaw().longIterator(); -+ while (iter.hasNext()) chunks.add(world.getChunkAt(iter.nextLong(), false)); -+ -+ return it.unimi.dsi.fastutil.objects.ObjectSets.unmodifiable(chunks); -+ } -+ -+ @Override -+ public boolean isChunkSent(final long chunkKey) { -+ org.spigotmc.AsyncCatcher.catchOp("accessing sent chunks"); -+ return this.getHandle().chunkLoader.getSentChunksRaw().contains(chunkKey); -+ } -+ // Paper end -+ - public Player.Spigot spigot() - { - return this.spigot; diff --git a/patches/server/1036-Properly-handle-pdc-and-custom-tags-in-ItemMeta.patch b/patches/server/1036-Properly-handle-pdc-and-custom-tags-in-ItemMeta.patch new file mode 100644 index 0000000000..b7399d5f48 --- /dev/null +++ b/patches/server/1036-Properly-handle-pdc-and-custom-tags-in-ItemMeta.patch @@ -0,0 +1,37 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Fri, 26 Apr 2024 11:38:28 -0700 +Subject: [PATCH] Properly handle pdc and custom tags in ItemMeta + + +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java +index 549d06b83b195c0a0207d18850b98d1e6e611460..bf0870590fcf5c502cd986047efec9f754faf9a7 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java +@@ -369,6 +369,8 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { + this.customTag = customData.copyTag(); + if (this.customTag.contains(CraftMetaItem.BUKKIT_CUSTOM_TAG.NBT)) { + CompoundTag compound = this.customTag.getCompound(CraftMetaItem.BUKKIT_CUSTOM_TAG.NBT); ++ this.customTag.remove(CraftMetaItem.BUKKIT_CUSTOM_TAG.NBT); // Paper - remove PDC from custom tag ++ if (this.customTag.isEmpty()) this.customTag = null; // Paper - remove PDC from custom tag + Set keys = compound.getAllKeys(); + for (String key : keys) { + this.persistentDataContainer.put(key, compound.get(key).copy()); +@@ -573,6 +575,8 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { + ByteArrayInputStream buf = new ByteArrayInputStream(Base64.getDecoder().decode(custom)); + try { + this.customTag = NbtIo.readCompressed(buf, NbtAccounter.unlimitedHeap()); ++ this.customTag.remove(CraftMetaItem.BUKKIT_CUSTOM_TAG.NBT); // Paper - ensure PDC isn't in custom tag ++ if (this.customTag.isEmpty()) this.customTag = null; // Paper - ensure PDC isn't in custom tag + } catch (IOException ex) { + Logger.getLogger(CraftMetaItem.class.getName()).log(Level.SEVERE, null, ex); + } +@@ -795,6 +799,8 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { + + if (this.customTag != null) { + itemTag.put(CraftMetaItem.CUSTOM_DATA, CustomData.of(this.customTag)); ++ this.customTag.remove(CraftMetaItem.BUKKIT_CUSTOM_TAG.BUKKIT); ++ if (this.customTag.isEmpty()) this.customTag = null; + } + } + diff --git a/patches/server/1037-handle-converting-old-serialized-names-to-new-names.patch b/patches/server/1037-handle-converting-old-serialized-names-to-new-names.patch new file mode 100644 index 0000000000..50366dacd0 --- /dev/null +++ b/patches/server/1037-handle-converting-old-serialized-names-to-new-names.patch @@ -0,0 +1,32 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Fri, 26 Apr 2024 11:38:40 -0700 +Subject: [PATCH] handle converting old serialized names to new names + + +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java +index bf0870590fcf5c502cd986047efec9f754faf9a7..dabbdadf3824d1132bd4aa6d2f2360c270fa299a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java +@@ -661,7 +661,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { + enchantKey = "SWEEPING_EDGE"; + } + +- Enchantment enchantment = Enchantment.getByName(enchantKey); ++ Enchantment enchantment = Enchantment.getByName(org.bukkit.craftbukkit.legacy.FieldRename.convertEnchantmentName(org.bukkit.craftbukkit.util.ApiVersion.CURRENT, enchantKey)); // Paper - convert enchantment names + if ((enchantment != null) && (entry.getValue() instanceof Integer)) { + enchantments.put(enchantment, (Integer) entry.getValue()); + } +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaSpawnEgg.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaSpawnEgg.java +index a6d2370113eb44c0863b7837362dbb350f5057c6..21dbce78b1a0d42d20c9e7f87f8bee1c18c1353f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaSpawnEgg.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaSpawnEgg.java +@@ -138,7 +138,7 @@ public class CraftMetaSpawnEgg extends CraftMetaItem implements SpawnEggMeta { + + String entityType = SerializableMeta.getString(map, CraftMetaSpawnEgg.ENTITY_ID.BUKKIT, true); + if (entityType != null) { +- this.spawnedType = EntityType.fromName(entityType); ++ this.spawnedType = EntityType.fromName(org.bukkit.craftbukkit.legacy.FieldRename.convertEntityTypeName(org.bukkit.craftbukkit.util.ApiVersion.CURRENT, entityType)); // Paper - handle old entity type field names + } + } + -- cgit v1.2.3